Using the SP2010 Client Object Model to update a list item
In the previous post I wrote about having a custom ribbon button which handles a postback event. In the conclusion I also wrote that so far I haven't been able to figure out how to retrieve the selected item when the postback occurs. So I have been looking at alternatives to implement what I want, and decided to go with the new client object model so that I don't need a postback anymore.
First, we need to create a new empty SharePoint element in your SharePoint 2010 project in Visual Studio. The contents of the file is as following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | <?xml version="1.0" encoding="utf-8"?> <Elements xmlns="http://schemas.microsoft.com/sharepoint/"> <CustomAction Id="TicketResolvedRibbonCustomAction" RegistrationId="28582" RegistrationType="List" Location="CommandUI.Ribbon" Rights="ManageLists" Sequence="25" Title="Mark as Resolved"> <CommandUIExtension> <CommandUIDefinitions> <CommandUIDefinition Location="Ribbon.ListItem.Actions.Controls._children"> <Button Id="TicketResolvedRibbonButton" Alt="Mark this ticket as resolved." Description="Mark this ticket as resolved." Sequence="25" Command="ResolveTicketCommand" Image32by32="/_layouts/images/Homburg.TicketDesk/ticketresolved.png" LabelText="Mark as Resolved" TemplateAlias="o1" /> </CommandUIDefinition> </CommandUIDefinitions> <CommandUIHandlers> <CommandUIHandler Command="ResolveTicketCommand" EnabledScript="javascript: function enableResolveTicketButton() { var items = SP.ListOperation.Selection.getSelectedItems(); return (items.length == 1); } enableResolveTicketButton();" CommandAction="javascript:TicketDeskMarkTicketAsResolved('{SelectedItemId}');" /> </CommandUIHandlers> </CommandUIExtension> </CustomAction> <Control Id="AdditionalPageHead" ControlSrc="~/_controltemplates/TicketResolvedRibbonDelegate.ascx" Sequence="25"/> </Elements> |
So, compared to the element in my previous post, you'll now notice that there is a CommandUIHandlers element and I've specified a couple of things. First, I want to make sure that the button is only available when an item is actually selected. That's where the EnabledScript attribute is for.
The CommandAction attribute specifies the JavaScript method that will be called when the button is clicked. In this case, it's a method that's defined in the user control that's linked at the bottom of the XML definition. Now you can use some predefined parameters in your method calls, one of them being the ID of the current selected item: {SelectedItemId}.
Next, we need the user control, so create a new user control in the SharePoint CONTROLTEMPLATES folder in Visual Studio. In this case, we won't be needing anything in the code-behind file, everything occurs in the ASCX file itself.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | <SharePoint:ScriptLink Name="SP.js" runat="server" LoadAfterUI="true" Localizable="false" /> <SharePoint:FormDigest ID="FormDigest1" runat="server" /> <script language="ecmascript" type="text/ecmascript"> var ticketsList; var web; var context; var ticketId; var ticketItem; var ticketStatusField = "TicketStatus"; var ticketDoneStatus = "Done"; var ticketResolvedStatus = "OK"; function TicketDeskMarkTicketAsResolved(itemId) { ticketId = itemId; context = new SP.ClientContext.get_current(); web = context.get_web(); ticketsList = web.get_lists().getByTitle("Tickets"); ticketItem = ticketsList.getItemById(ticketId); // This will make sure the contents of the list and list item are actually loaded context.load(ticketsList); context.load(ticketItem); context.executeQueryAsync(OnTicketsListsLoaded); } function OnTicketsListsLoaded() { var currentStatus = ticketItem.get_item(ticketStatusField); // There's no need to continue this method when the status is already set to resolved or the ticket has been completed. if (currentStatus == ticketResolvedStatus || currentStatus == ticketDoneStatus) { return; } // Set the ticket status field to the Resolved value ticketItem.set_item(ticketStatusField, ticketResolvedStatus); ticketItem.update(); // Submit the query to the server context.load(ticketItem); context.executeQueryAsync(OnTicketUpdated, OnError); } function OnTicketUpdated(args) { // Nothing really needed here other than refreshing the page to see that the change has been made window.location.href = window.location.href; } function OnError(sender, args) { alert(args.get_message()); } </script> |
Going through the code, we see a couple of things. First we need a reference to SP.js, so we can actually use the client object model. The FormDigest tag is there so we can perform an update of a list item.
I'm declaring a couple of variables so I can use those in the various methods I have. The first method is the one that's being called from the XML element created in the first step.
In this method, I'm setting the context that's used to retrieve the web and list we're working with. Based on the ID that's supplied, I'm loading the list item on which the update should be performed. The list and list item are then loaded in the context after which a query is executed asynchronously, to retrieve the data. A callback method is defined for when the query has completed.
In the callback method, I'm doing a couple of checks to see whether an update is required. Then, by using set_item(), you can specify the field name and the value to update an item. The context is loaded again with the updated item and the update query is submitted to the server.
This is a very simple example of how to use the SP2010 client object model to perform an update on a list item. This can easily be extended with more complex business logic, to suit other needs.
Ribbon buttons with postback in SP2010
For this Ticket Desk project I'm doing, I wanted to extend the default Ribbon UI with a new button that quickly allows the application admins to quickly mark a ticket as resolved. This is just a status field on the list item itself and you could even argue to just edit the item. However, since there's a workflow involved and I want to hide the status field on the new and edit forms from ticket submitters, I'm providing the functionality via a ribbon button.
Now, in SharePoint 2007, I was used to doing something similar, but then I'd be using a custom action with a ControlAssembly and ControlClass specified. The class specified would just inherit from WebControl and I'd implement the IPostBackEventHandler interface and be on my way. I tried something similar in SP2010, however I got no dice. So, apparently things are a bit different in SP2010, let's find out what exactly.
First, let me outline what we need to get this to work:
- CustomAction element that defines the ribbon button
- User control which will load the required JavaScript and handle the postback
- JavaScript file containing the page component
Right, I'm going to assume you know how to set-up an SP2010 project in Visual Studio 2010 and how to add the different types of items. First, create a new Empty Element which will contain the following XML:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | <?xml version="1.0" encoding="utf-8"?> <Elements xmlns="http://schemas.microsoft.com/sharepoint/"> <CustomAction Id="TicketResolvedRibbonCustomAction" RegistrationId="28582" RegistrationType="List" Location="CommandUI.Ribbon" Rights="ManageLists" Sequence="25" Title="Mark as Resolved"> <CommandUIExtension> <CommandUIDefinitions> <CommandUIDefinition Location="Ribbon.ListItem.Actions.Controls._children"> <Button Id="TicketResolvedRibbonButton" Alt="Mark this ticket as resolved." Description="Mark this ticket as resolved." Sequence="25" Command="ResolveTicketCommand" Image32by32="/_layouts/images/TicketDesk/ticketresolved.png" LabelText="Mark as Resolved" TemplateAlias="o1" /> </CommandUIDefinition> </CommandUIDefinitions> </CommandUIExtension> </CustomAction> <Control Id="AdditionalPageHead" ControlSrc="~/_controltemplates/TicketResolvedRibbonDelegate.ascx" Sequence="25"/> </Elements> |
Basically, this XML element defines that we have a custom action, which is bound to a List with template ID "28582". This is a custom list definition that's deployed with my solution, but you can also bind this to a content type for example. Furthermore, the location is on the ribbon and the rights required here are ManageLists. The CommandUIDefinition element specifies that the location of the button will be the list item tab, in the Actions group. There is a command specified here as well, which we will later need in our code-behind of the user control.
Note that I'm not specifying any CommandUIHandlers here. This is not necessary since we're handling the event on the server-side here, instead of client-side. So, instead of the handler, we define a user control that will be loaded in the AdditionalPageHead section of the masterpage. The location is linked to the default SharePoint CONTROLTEMPLATES folder, for which you can create a mapping in Visual Studio.
Next, we need the page component JavaScript file. Create a SharePoint mapped folder in Visual Studio which links to LAYOUTS, if you haven't done that so far. In this folder, create a new JavaScript file called "PageComponent.js". The contents of the file is as following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | function ULS_SP() { if (ULS_SP.caller) { ULS_SP.caller.ULSTeamName = "Windows SharePoint Services 4"; ULS_SP.caller.ULSFileName = "/_layouts/TicketDesk/PageComponent.js"; } } Type.registerNamespace('RibbonCustomization'); // RibbonApp Page Component RibbonCustomization.PageComponent = function () { ULS_SP(); RibbonCustomization.PageComponent.initializeBase(this); } RibbonCustomization.PageComponent.initialize = function () { ULS_SP(); ExecuteOrDelayUntilScriptLoaded(Function.createDelegate(null, RibbonCustomization.PageComponent.initializePageComponent), 'SP.Ribbon.js'); } RibbonCustomization.PageComponent.initializePageComponent = function () { ULS_SP(); var ribbonPageManager = SP.Ribbon.PageManager.get_instance(); if (null !== ribbonPageManager) { ribbonPageManager.addPageComponent(RibbonCustomization.PageComponent.instance); ribbonPageManager.get_focusManager().requestFocusForComponent(RibbonCustomization.PageComponent.instance); } } RibbonCustomization.PageComponent.refreshRibbonStatus = function () { SP.Ribbon.PageManager.get_instance().get_commandDispatcher().executeCommand(Commands.CommandIds.ApplicationStateChanged, null); } RibbonCustomization.PageComponent.prototype = { getFocusedCommands: function () { ULS_SP(); return []; }, getGlobalCommands: function () { ULS_SP(); return getGlobalCommands(); }, isFocusable: function () { ULS_SP(); return true; }, receiveFocus: function () { ULS_SP(); return true; }, yieldFocus: function () { ULS_SP(); return true; }, canHandleCommand: function (commandId) { ULS_SP(); return commandEnabled(commandId); }, handleCommand: function (commandId, properties, sequence) { ULS_SP(); return handleCommand(commandId, properties, sequence); } } // Register classes RibbonCustomization.PageComponent.registerClass('RibbonCustomization.PageComponent', CUI.Page.PageComponent); RibbonCustomization.PageComponent.instance = new RibbonCustomization.PageComponent(); // Notify waiting jobs NotifyScriptLoadedAndExecuteWaitingJobs("/_layouts/TicketDesk/PageComponent.js"); |
Note that I'm referring to the "/_layouts/TicketDesk" location, since that's the name of my project. You may need to change the location here to match your situation. From what I've gathered so far, this is object-oriented JavaScript that actually powers the ribbon button and can be considered as a template required for ribbon buttons. You can create your own implementations for the methods defined in the prototype, but in this case it's not needed, so we're going to leave the file as it is.
Now, on to the user control. In Visual Studio, create a mapping to the SharePoint CONTROLTEMPLATES folder. Add a new user control item in this folder. The ASCX file itself doesn't require any contents, so we're gonna go ahead and move to the source file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | namespace TicketDesk.CONTROLTEMPLATES { public partial class TicketResolvedRibbonDelegate : UserControl, IPostBackEventHandler { protected void Page_Load(object sender, EventArgs e) { var commands = new List<IRibbonCommand> { // The command name here matches the command name of the ribbon button new SPRibbonPostBackCommand("ResolveTicketCommand", this, "TicketResolvedRibbonButtonEvent", null, "true") }; // Register ribbon scripts var spRibbonScriptManager = new SPRibbonScriptManager(); spRibbonScriptManager.RegisterGetCommandsFunction(Page, "getGlobalCommands", commands); spRibbonScriptManager.RegisterCommandEnabledFunction(Page, "canHandleCommand", commands); spRibbonScriptManager.RegisterHandleCommandFunction(Page, "handleCommand", commands); InitRibbonScriptManager(commands); } private void InitRibbonScriptManager(IEnumerable<IRibbonCommand> commands) { // Register scripts ScriptLink.RegisterScriptAfterUI(Page, "SP.Runtime.js", false, true); ScriptLink.RegisterScriptAfterUI(Page, "SP.js", false, true); // Register initialize function var manager = new SPRibbonScriptManager(); var methodInfo = typeof(SPRibbonScriptManager).GetMethod("RegisterInitializeFunction", BindingFlags.Instance | BindingFlags.NonPublic); methodInfo.Invoke(manager, new object[] { Page, "InitPageComponent", "/_layouts/TicketDesk/PageComponent.js", false, "RibbonCustomization.PageComponent.initialize()" }); // Register ribbon scripts manager.RegisterGetCommandsFunction(Page, "getGlobalCommands", commands); manager.RegisterCommandEnabledFunction(Page, "commandEnabled", commands); manager.RegisterHandleCommandFunction(Page, "handleCommand", commands); } public void RaisePostBackEvent(string eventArgument) { Page.Response.Write(eventArgument); } } } |
Upon loading of the user control, we're going to add a new ribbon command, of type SPRibbonPostBackCommand. The command name here matches the command name of the button specified in the XML element earlier. We're also specifying an event ID in the constructor, so when the postback occurs, we can make sure we're handling the proper event. You could also specify event arguments in the constructor here.
Next, we need to instantiate a new SPRibbonScriptManager object. We're using that to register the JavaScript methods that we've defined in the previous step. Note that the function names here refer to methods in the JS prototype.
The page component file now needs to be registered to SharePoint. For a reason completely beyond me, the method required to do so, is not publically accessible through the API, so we'll need reflection to that.
For sake of demonstration purposes, the RaisePostBackEvent method isn't doing a lot here other than writing the event arguments to the page. Deploy your project and go to the list where your ribbon button should be visible and click it. You should now see that a postback is taking place and the event arguments should be written on top of the page.
The only thing I haven't really figured out yet is how to actually pass the ID of the item you've selected, hopefully I'll be able to write something about that in a follow-up post.