Second Life of a Hungarian SharePoint Geek

April 14, 2014

How to Extend the SharePoint Ribbon to Enable Pushing Down Permissions to Child Items

Filed under: JavaScript, Permissions, Ribbon, Security, SP 2010 — Tags: , , , , — Peter Holpar @ 20:03

If you are familiar with the security options of the NTFS file system you should know the dialog below as well (accessible via the Security tab of the folder properties / Advanced button / Change Permissions… button):

image

By selecting the highlighted checkbox you can push down the permissions set on the current folder to all child items (folders and files). Unfortunately, there is no such option in the standard SharePoint toolbox. As you can see on the screenshot below, the Inheritance group of the Permission Tools on the ribbon includes the options Manage Parent and Stop Inheriting Permissions, but no way to force the inheritance of the current permissions to the child items.

image

In my recent posts I discussed how to check from code whether an site / list has child items with unique permissions, and how to to push down permissions from code.

In this post I will illustrate, how to create a Ribbon extension that enables the missing functionality from SharePoint.

In our CustomAction elements we register the loader ScriptBlock of the custom PageComponent (implemented in InheritPermissions.js, it will be loaded only we are on the permissions page, users.aspx). Next, we register our button (Ribbon.Permission.Manage.InheritDown) in the Inheritance button group (Ribbon.Permission.Parent, defined in 14\TEMPLATE\GLOBAL\XML\CMDUI.XML).

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  3.   <CustomAction Location="ScriptLink" ScriptBlock="function startScript() { document.write('&lt;script type=&quot;text/javascript&quot; src=&quot;/_layouts/InheritPermissions/InheritPermissions.js&quot;&gt;&lt;/' + 'script&gt;'); } if (document.location.pathname.toLowerCase().indexOf('_layouts/user.aspx') > -1 ) { ExecuteOrDelayUntilScriptLoaded(startScript, 'sp.js'); }" Sequence="1000"/>
  4.   
  5.   <CustomAction Location="CommandUI.Ribbon">
  6.     <CommandUIExtension>
  7.       <CommandUIDefinitions>
  8.         <CommandUIDefinition Location="Ribbon.Permission.Parent.controls._children">
  9.           <Button
  10.               Id="Ribbon.Permission.Manage.InheritDown"
  11.               Command="Ribbon.Permission.Manage.InheritDown"
  12.               Sequence="50"
  13.               Image16by16="/_layouts/images/InheritPermissions/InheritDown16x16.png"
  14.               Image32by32="/_layouts/images/InheritPermissions/InheritDown32x32.png"
  15.               LabelText="Inherit to Child Objects"
  16.               ToolTipTitle="Inherit to Child Objects"
  17.               ToolTipDescription="Reset permissions on child objects to inherit the permissions of this object.&lt;br&gt;Any custom permissions configured on child objects will be lost.&lt;br&gt;This tool is available only to farm administrators."
  18.               TemplateAlias="o1"/>
  19.         </CommandUIDefinition>
  20.       </CommandUIDefinitions>
  21.     </CommandUIExtension>
  22.   </CustomAction>
  23.  
  24. </Elements>

The event handler functionality custom PageComponent is implemented in InheritPermissions.js. If the command ‘Ribbon.Permission.Manage.InheritDown’ is invoked, we make a page postback with the event target ‘inheritDown’:

__doPostBack(‘inheritDown’, ”);

we process the postback later in our WebControl. Similarly, whether the page component should handle the command ‘Ribbon.Permission.Manage.InheritDown’ is determined by the value of the permInheritEnabled variable, the value is injected by our WebControl as well (see the RegisterStartupScript in the OnInit method of the web control later).

  1. Type.registerNamespace('CustomPermission.UI');
  2.  
  3. CustomPermission.UI.PageComponent = function CustomPermissionUI_PageComponent() {
  4.     CustomPermission.UI.PageComponent.initializeBase(this);
  5. }
  6.  
  7. CustomPermission.UI.PageComponent.initialize = function () {
  8.     var ribbonPageManager = SP.Ribbon.PageManager.get_instance();
  9.     if (null !== ribbonPageManager) {
  10.         ribbonPageManager.addPageComponent(this.instance);
  11.         ribbonPageManager.get_focusManager().requestFocusForComponent(this.instance);
  12.     }
  13. }
  14. CustomPermission.UI.PageComponent.refreshRibbonStatus = function () {
  15.     SP.Ribbon.PageManager.get_instance().get_commandDispatcher().executeCommand(Commands.CommandIds.ApplicationStateChanged, null);
  16. }
  17.  
  18. CustomPermission.UI.PageComponent.prototype = {
  19.     init: function () { },
  20.     getFocusedCommands: function () {
  21.         return ['Ribbon.Permission.Manage.InheritDown'];
  22.     },
  23.  
  24.     getGlobalCommands: function () {
  25.         return ['Ribbon.Permission.Manage.InheritDown'];
  26.     },
  27.  
  28.  
  29.     canHandleCommand: function (commandId) {
  30.         if (commandId === 'Ribbon.Permission.Manage.InheritDown') {
  31.             return permInheritEnabled;
  32.         }
  33.     },
  34.  
  35.     handleCommand: function (commandId, properties, sequence) {
  36.         if (commandId === 'Ribbon.Permission.Manage.InheritDown') {
  37.             var doInherit = confirm('You are about to inherit permissions from his object to all of the child objects having custom permissions. Any custom permissions on the child objects will be lost.');
  38.             if (doInherit) {
  39.                 __doPostBack('inheritDown', '');
  40.             }
  41.         }
  42.  
  43.     },
  44.  
  45.     getId: function () {
  46.         return "CustomPermission.UI.PageComponent";
  47.     }
  48. }
  49.  
  50. CustomPermission.UI.PageComponent.registerClass('CustomPermission.UI.PageComponent', CUI.Page.PageComponent);
  51.  
  52. CustomPermission.UI.PageComponent.instance = new CustomPermission.UI.PageComponent();
  53. CustomPermission.UI.PageComponent.initialize();
  54.  
  55. SP.SOD.notifyScriptLoadedAndExecuteWaitingJobs("InheritPermissions.js");

The InheritPermissions WebControl is injected into the pages via an AdditionalPageHead delegate control.

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  3.   <Control
  4.     ControlAssembly="$SharePoint.Project.AssemblyFullName$"
  5.     ControlClass="InheritPermissions.InheritPermissions"
  6.     Sequence="50" Id="AdditionalPageHead"/>
  7. </Elements>

We include our control into the Safe Control Entries:

image

We activate the functionality of the control, if the host page is a permission page (e.g. it is derived from the CBaseAclPage class).

  1. class InheritPermissions : WebControl
  2. {
  3.     bool _isInheritButtonEnabled = false;
  4.  
  5.     protected override void OnInit(EventArgs e)
  6.     {
  7.         base.OnInit(e);
  8.  
  9.         CBaseAclPage baseAclPage = this.Page as CBaseAclPage;
  10.         if (baseAclPage != null)
  11.         {
  12.             baseAclPage.ParseAclObjFromRequest();
  13.  
  14.             SPSecurableObject securable = this.Securable;
  15.  
  16.             if (securable != null)
  17.             {
  18.  
  19.                 if ((this.Page.IsPostBack) && (this.Page.Request["__EVENTTARGET"] == "inheritDown"))
  20.                 {
  21.                     InheritPermissionsDown(securable);
  22.                 }
  23.  
  24.                 _isInheritButtonEnabled = ((SPFarm.Local.CurrentUserIsAdministrator(true)) && (securable.HasSubItemWithUniquePermissions()));
  25.  
  26.                 // inject a JavaScript variable into the page to support client side PageComponent
  27.                 // see canHandleCommand method of CustomPermission.UI.PageComponent (InheritPemissions.js)
  28.                 string enabledScript = (_isInheritButtonEnabled) ? "true" : "false";
  29.                 string script = string.Format(@"<script type=""text/javascript"">
  30.                                var permInheritEnabled = {0}
  31.                             </script>", enabledScript);
  32.                 this.Page.ClientScript.RegisterStartupScript(typeof(SPRibbon), "perminherit", script, false);
  33.             }
  34.         }
  35.     }
  36.  
  37.     protected override void OnPreRender(EventArgs e)
  38.     {
  39.         if (!_isInheritButtonEnabled)
  40.         {
  41.             SPRibbon ribbon = SPRibbon.GetCurrent(this.Page);
  42.             ribbon.TrimById("Ribbon.Permission.Manage.InheritDown");
  43.         }
  44.     }
  45.  
  46.     private SPSecurableObject Securable
  47.     {
  48.         get
  49.         {
  50.             SPSecurableObject securable = null;
  51.             UserRoles userRoles = this.Page as UserRoles;
  52.  
  53.             if (userRoles != null)
  54.             {
  55.                 securable = userRoles.m_Securable;
  56.             }
  57.  
  58.             return securable;
  59.         }
  60.     }
  61.  
  62.     // see further details in the code download…
  63.     
  64. }

We reuse the former code samples mentioned above: code details about how to inherit down permissions are discussed in this post, how to detect items with unique permission is shown in this post.

In the ParseAclObjFromRequest method we invoke the protected method with the same name of the parent page of CBaseAclPage type, thus achieve we a reference in this.Securable to the securable object (web site, list, folder, etc.) the permissions were set on.

We push the permissions down to child object if the page was posted back with the ‘inheritDown’ event target.

The value of the _isInheritButtonEnabled determines if the PageComponent implemented in InheritPermissions.js support the ‘Ribbon.Permission.Manage.InheritDown’ command. The value depends on two factors: it is enabled if the current user is a farm administrator and if the securable object has sub items with unique permissions. See my notes in the former post regarding the shortcomings of the check of unique permissions on child items. Even this check is not perfect we apply the same logic to remain consistent with the standard features / messages.

In my first attempt I invoked the InheritPermissionsDown method from the OnPreRender method and not from the OnInit method. However, the standard JavaScript functions of the permissions page were already registered by this time, so the message ‘Some content on this site has unique permissions which are not controlled from this page.’ were displayed even if we just pushed down the permissions right now. I found this behavior pretty disturbing. Including the logic in the OnInit method we push the permissions down before the standard scripts get registered (see the SetExceptionStatus method of the UserRoles class, derived from the CBaseAclPage type, invoked from InitPage invoked from OnLoad), so there is no inconsistent message displayed.

The button should be hidden if the user is no farm admin or if the item has no subitem with unique permissions (in this case the value of the _isInheritButtonEnabled  is false). We should invoke the TrimById method of the current ribbon instance, but in this case from the OnPreRender method.

Let’s see the new button in action. It is displayed on the ribbon if the current item has child items with unique permissions :

image

If the user clicks on the button, a warning is displayed. After confirmation of the action, the permissions are pushed down to child items.

image

You can download the sample solution from here.

Advertisements

April 30, 2012

Customizing SharePoint ribbon using a designer tool

Filed under: Reviews, Ribbon, SP 2010, Tools — Tags: , , , — Peter Holpar @ 15:00

Last summer I had a lot of work (and a few posts) related to SharePoint ribbon. At that time I wished to have a tool that enables developers to design their ribbon extensions in a more graphical way than editing raw XML files. Unfortunately, I found no such tool so had to do the job the hard way.

A few month ago the solution came from a web site that was a bit unexpected for me in this context. Although I used the product Add-in Express for Internet Explorer to create some IE add-in (like this or this) from managed code, it was a positive surprise that the same company offers a RAD (rapid application development)  solution for designing SharePoint ribbons as well.

With installing the Ribbon Designer for SharePoint and Office 365 you get a Visual Studio  project and item template to extend and boost your SharePoint solutions.

In the past months I spent several hours playing with this tool and found its features can help the everyday life of a SharePoint designer / developer.

There are a lot of technical stuff on the company website, so this time I would like only to share my experience based on the tests and work I did in the past months with the tool. As in the case of any judgments, this one might be subjective as well, so if you would like to have a first-hand experience with the product, you should download a free trial and evaluate it based on your own point of view.

Pros:

  • Straightforward installation.
  • Easy to create new (even context-sensitive) ribbon tabs and ribbon groups, existing ones can be extended.
  • Full support of ribbon controls.
  • Both client (JavaScript / jQuery: no page postback) and server side (ASP.NET with postback) events are supported.
  • Support for “global” (like SharePoint list item is selected in list view) and “local” (like your Ribbon button is clicked) events.
  • Visual editing (and preview) of ribbons, including images deployed through resource files.
  • Existing ribbon customization XMLs can be imported and improved.
  • Office 365 solutions are supported.
  • Integrates with the standard SharePoint solutions framework and with the standard Visual Studio tools.
  • Great support from the company (Developer’s guide, Forum, Learning Center with technical articles, sample projects and videos, etc.). I really needed this help to make the first steps with the tool.
  • Users of other Add-in Express products will find the design surface familiar.
  • Development is in-progress and the company seems to be quite flexible, that means if you think there might be a bug in the product or you miss a specific feature you have good chances to get what you need in a limited timeframe.
  • Source code is available (only in Professional Edition!).

Cons:

  • If you are already familiar with “traditional” SharePoint ribbon customization method, you have to learn a new approach. After a lot of work I did formerly with XMLs I found it is not always trivial to adapt my mind to the graphical interface.
  • Lack of community support. If you have a common customization problem it is more likely that you find a solution on the web for the “traditional” way.
  • Custom assembly (AddinExpress.SharePoint.Ribbon.dll) must be deployed to the server. Most of the time it is a no-issue, but might be a show-stopper at some companies.
  • Although specific XML-based solutions may be imported, it does not cover 100% of the existing solutions. Migration to the other direction (for example, if you start with the designer, but later decide to choose the “classical” approach) might be problematic as well.

I can’t provide you any ROI or TCO numbers here. Whether a product is expensive or not depends on several factors, but I feel the standard edition is quite affordable, and for the double price of the standard edition one can achieve even the source code in the professional edition.

If you consider to do significant amount of SharePoint ribbon customization, or you are a newcomer on this field of development, and would like to do the job quickly without understanding first all the bells and whistles of the ribbon customization features, you should definitely consider this tool as an option.

In a forthcoming post I plan to provide a more technical overview through a real-world ribbon customization example solved in parallel with the classical XML approach and the RAD tool.

October 24, 2011

Creating a ToggleButton on SharePoint ribbon with alternating images

Filed under: jQuery, Ribbon, SP 2010 — Tags: , , — Peter Holpar @ 11:17

Although ToggleButton is a useful control on the ribbon of SharePoint 2010 user interface, unfortunately I found no built-in way to assign separate images to the on / off states of the button.

After playing awhile with the Internet Explorer Developer Tools (F12, see also the Tools menu in IE 8), I find out how to achieve that through the HTML object model built up by the ribbon infrastructure.

The following JavaScript code is from the command handler method of the custom ribbon components and assumes jQuery is already loaded.

Replace the commandId of the ToggleButton and the image properties (src and style attributes) to match your needs.

  1. if (commandId === 'Ribbon.Custom.List.MyFeature.SwitchOnOff')
  2.     var x = jQuery("a[id|='" + commandId + "']").find("img");
  3.  
  4.     if (properties.On) {
  5.         x.attr("src", "/_layouts/images/MyImages/On.png");
  6.         x.attr("style", "top: 0px; left: 0px;");
  7.     }
  8.     else {
  9.         x.attr("src", "/_layouts/images/MyImages/Off.png");
  10.         x.attr("style", "top: 0px; left: 0px;");
  11.     }

You can use separate image files, or a single one with different vertical / horizontal offset value.

I’ve defined a global switchedOn variable to track the actual status of the ToggleButton easier, and query that value from other JavaScript methods:

var switchedOn = false;

I hope this trick helps you to make your SharePoint UI a bit more dynamic.

October 16, 2011

Disabling item deletion at the SharePoint user interface

Filed under: Custom forms, ECB menu, JavaScript, Ribbon, SP 2010 — Tags: , , , , — Peter Holpar @ 21:58

Recently I worked on more projects where it was a nice-to-have requirement to disable deletion of specific items from a list.

Well, if you are at least a bit familiar with SharePoint your first idea might be the same as mine, that is to create a list item event receiver to capture ItemDeleting event and cancel it for your specific items. You are right, that should do the job, however I find this approach a bit ugly, as the users still have the option on the user interface to press the Delete Item button on the ribbon or select the Delete Item menu from the Edit Control Block (ECB) just to get that user-unfriendly warning from the event receiver cancellation to their face.

Wouldn’t it be much better to disable the corresponding user interface elements for the specific list items to prohibit deletion? As a safeguard, you can still apply an event receiver, but that would not be the standard way to stop users deleting the items from the browser.

That sounds great, but how to achieve the goal?

First, let’s see where that deletion options are for the user in a standard SharePoint web UI. The individual item can be deleted from the display and edit form ribbon and using the ECB menu of the selected item in a list view. Additionally, there is a Delete Item button on the ribbon of the list view that is to delete selected items.

In fact, disabling deletion on the UI of the display and edit form ribbon is pretty easy. You just need to use the TrimById method of the SPRibbon class in your form component passing the ID of the Delete Item button as the parameter, that is Ribbon.ListForm.Display.Manage.DeleteItem for the display form and  Ribbon.ListForm.Edit.Actions.DeleteItem for the edit form. If you need more info about how to create and inject your custom solution into the standard SharePoint form, read my former post.

In the overridden OnPreRender method of my customized ListFieldIterator I simply check if Delete Item button should be disabled, and if it should, then call the TrimById method using the right control ID.

  1. protected override void OnPreRender(EventArgs e)
  2. {
  3.     SPFormContext formContext = SPContext.Current.FormContext;
  4.  
  5.     if ((formContext.FormMode != SPControlMode.New) && (!IsDeletionEnabled))
  6.     {
  7.         String deleteButton = (formContext.FormMode == SPControlMode.Display) ?
  8.             "Ribbon.ListForm.Display.Manage.DeleteItem" : "Ribbon.ListForm.Edit.Actions.DeleteItem";
  9.         SPRibbon.GetCurrent(this.Page).TrimById(deleteButton);
  10.     }
  11. }

The IsDeletionEnabled is a custom property where we check (for example, based on list or list item properties or user permissions) if we would like to allow deletion or not.

  1. protected bool IsDeletionEnabled
  2. {
  3.     get
  4.     {
  5.         bool result = true;
  6.  
  7.         // your custom logic comes here
  8.         if (((String)Item["Title"]) == "Do not delete")
  9.         {
  10.             result = false;
  11.         }
  12.  
  13.         return result;
  14.     }
  15. }

That’s about the display / edit forms, let’s see the list view form, harder part of our goal.

As I’ve mentioned above there are two UI items to disable in the list view form, one is on the ribbon and another one in the ECB menu.

A serious difference between the UI components on the display / edit forms and the ones on the list view item forms is that in the first case we can check the condition and disable controls on the server side (we know which item the form is displayed for), while in the second case, user can select any items (or none of them) on the client side, so we should act accordingly on the client side without any postback of the page.

Unfortunately, I’ve started with the ribbon part and it caused me some headache and long hours spent unsuccessfully to play with ribbon extensions to check conditions and interact with the existing controls using jQuery and other tricks as well.

As none of my solutions for the list view ribbon worked perfectly, I switched to ECB.

When you need to work with the ECB menu, this series of post from Jan Tielens provides an excellent starting point. Although it is written originally for WSS 3.0, the content is still valid for SharePoint 2010 as well.

Since my goal was not to add custom items to the menu, but rather to delete an existing one, the trivial solution was to create my custom version of AddListMenuItems. The original version of this JavaScript method (assuming English – United States locale ID 1033) can be found in the 14\TEMPLATE\LAYOUTS\1033\CORE.JS file, but to have a more readable one, get it from CORE.debug.js instead.

Note: There are tons of posts on the web about how to inject JavaScript code into your page. Most of them favor to add a Content Editor Web Part to the page and paste your code into that. However, I feel this approach to be more appropriate for a  power-user, and usually add my scripts through a ScriptLink CustomAction or via an AdditionalPageHead. I found these methods are easier to integrate into and deploy from a Visual Studio 2010 solution, but they have a drawback that they do not affect only the target page. If you need a targeted solution you should first verify  in the code of the AdditionalPageHead if it is the right list then register the script using Page.ClientScript.RegisterClientScriptBlock, or through one of the several version of the static Register* method of ScriptLink  class.

  1. SPContext context = SPContext.Current;
  2.  
  3. if ((context != null) && (context.List != null) && (context.List.Title == "YourList"))
  4. {
  5.     // register your script here
  6. }

To target the script in the case of a ScriptLink CustomAction you can apply the method described in this post.

Back to the topic, whatever solution you choose to reference and deploy the JavaScript code, you should alter the copy of the original version of the AddListMenuItems method as described below. 

Note: It is important not to tamper with the original version as it affects all of your SharePoint sites and what might be even worse, it is not a supported modification.

Look up the following code section in the AddListMenuItems method. It is responsible for creation of the Delete Item menu item in ECB.

  1. if (currentItemID.indexOf(".0.") < 0 && HasRights(0x0, 0x8)
  2.       && !currentItemIsEventsExcp)
  3. {
  4.     if (ctx.listBaseType==4)
  5.         strDisplayText=L_DeleteResponse_Text;
  6.     else
  7.         strDisplayText=L_DeleteItem_Text;
  8.     strAction="DeleteListItem()";
  9.     strImagePath=ctx.imagesPath+"delitem.gif";
  10.     menuOption=CAMOpt(m, strDisplayText, strAction, strImagePath, null, 1180);
  11.     CUIInfo(menuOption, "Delete", ["Delete"]);
  12.     menuOption.id="ID_DeleteItem";
  13.     CUIInfo(menuOption, "Delete", ["Delete"]);
  14. }

Let’s modify that code a bit as shown here:

  1. if (currentItemID.indexOf(".0.") < 0 && HasRights(0x0, 0x8)
  2.       && !currentItemIsEventsExcp) {
  3.     if (allowDeletion(currentItemID)) {
  4.         if (ctx.listBaseType == 4)
  5.             strDisplayText = L_DeleteResponse_Text;
  6.         else
  7.             strDisplayText = L_DeleteItem_Text;
  8.         strAction = "DeleteListItem()";
  9.         strImagePath = ctx.imagesPath + "delitem.gif";
  10.         menuOption = CAMOpt(m, strDisplayText, strAction, strImagePath, null, 1180);
  11.         CUIInfo(menuOption, "Delete", ["Delete"]);
  12.         menuOption.id = "ID_DeleteItem";
  13.         CUIInfo(menuOption, "Delete", ["Delete"]);
  14.  
  15.     }
  16. }

Yes, the only difference is the extra condition that calls allowDeletion method passing the currentItemID parameter. The value of this variable is the ID of the actual list item.

How you implement allowDeletion in your JavaScript is up to your business requirements. Here are two examples of what can be achieved simply:

Disable deleting items having a specific ID. Probably not the most useful example but the simplest one for sure.

  1. function allowDeletion(itemID) {
  2.     return ((itemID == 1) || (itemID == 3));
  3. }

Disable deleting folders. Useful if you deployed a folder structure into your list and would like to prohibit users from destroying that.

  1. function allowDeletion(itemID) {
  2.  
  3.     var selectedItems = SP.ListOperation.Selection.getSelectedItems();
  4.     var result = false;
  5.  
  6.     for (var i = 0; i < selectedItems.length; i++) {
  7.  
  8.         if (selectedItems[i].id == itemID) {
  9.             // true (enable deletion), if it is a list item, not a folder
  10.             result = (selectedItems[i].fsObjType == 0);
  11.             break;
  12.         }
  13.     }
  14.  
  15.     return result;
  16.  
  17. }

If you need more sophisticated logic, like doing comparison with list item field values, you should apply some trick using WCF Data Services or even hacks to utilize the client object model but more about that in a forthcoming post.

And the most interesting part (at least, for me) of the story is coming now.

After deploying my script and testing the solution I realized that if you select an item that has its Delete Item menu removed from the ECB menu then Delete Item button on the ribbon is disabled. Nice! That is what I worked a lot earlier for without success. And that is the best that if you select multiple items from the list, and deletion is disabled at least for one of them the corresponding ribbon button is disabled as well.

To understand what happens behind the curtains I added some alert statements to my scripts (the simplest way of JavaScript debugging). It turned out, that the AddListMenuItems method (and our allowDeletion method as well) called once for each list item when they got selected first time. It means if you select multiple items by checking the topmost checkbox in the header, the methods are triggered for each items one by one. If a selected item is deselected, and selected the second (or even more) times, the methods are not triggered once more, so the state of the Delete Item menu won’t change. If it was removed (not added to be exact) the first time, it remains removed. If it was added the first time, it remain there. Of course, a page reload resets this kind of caching.

I removed (commented out) the script lines from this part of the function one by one, and found that the menu item actually gets added by this line:

menuOption = CAMOpt(m, strDisplayText, strAction, strImagePath, null, 1180);

However, if I remove this method call (actually, there are two of the same line there):

CUIInfo(menuOption, "Delete", ["Delete"]);

then the Delete Item ribbon button won’t be enabled, even if there are items to delete. In this case, the items can be still deleted through ECB’s Delete Item menu.

If you check the “signiture” of this method it is:

function CUIInfo(menuItem, command, enabledCommands)

That means it registers the “Delete” command for our menu item and sets the “Delete” command as an enabled command as well. I have not tracked the code any further, but it seems that the Delete Item button on the ribbon is enabled only if all of the selected list items have a menu item with enabled commands (CUIEnabledCommands property) containing the “Delete” command. Seems to be a very interesting infrastructure behind this.

Note: The method described here does not protect you from items being deleted in the Datasheet View or other customized views, not to mention deleting items from custom code or web service / WCF calls (like client object model WCF Data Services etc.).

July 26, 2011

How to add a button to the ribbon to update a field for selected list items using the client object model

I received a comment for one of my former posts asking how it is possible to add a custom button to the ribbon that allows users to set field values for selected list items. In this post I show you a solution for this request.

You can download the complete solution from here.

The solution contains a content type with a numerical field called Numeric. A test list schema is created and the content type is assigned to it. There is also a list instance for the test list with a few items.

The following custom action definition assigns the new ribbon button to our content type. Important to note that the hexadecimal digits in the ID of the content type must be written with all capital letters in the RegistrationId attribute.

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  3.   <CustomAction
  4.     Id="Ribbon.Library.Actions.ResetItems"
  5.     Location="CommandUI.Ribbon"
  6.     RegistrationType="ContentType"
  7.     RegistrationId="0x0100E36DB888927049468D7CBAF1C7B70582"
  8.     Title="Reset Selected Items Action">
  9.     <CommandUIExtension>
  10.       <CommandUIDefinitions>
  11.         <CommandUIDefinition
  12.           Location="Ribbon.ListItem.Manage.Controls._children">
  13.           <Button Id="Ribbon.ListItem.Manage.SetItemData"
  14.             Command="SetItemDataButtonCommand"
  15.             Image16by16="/_layouts/images/JCOMUpdateSelectedItems/exclamation.png"
  16.             LabelText="Reset Selected Items"
  17.             TemplateAlias="o2" />
  18.         </CommandUIDefinition>
  19.       </CommandUIDefinitions>
  20.       <CommandUIHandlers>
  21.         <CommandUIHandler
  22.           Command="SetItemDataButtonCommand"
  23.           CommandAction="javascript:updateSelectedItems();"
  24.           EnabledScript="javascript:isNonFolderSelected();" />
  25.       </CommandUIHandlers>
  26.     </CommandUIExtension>
  27.   </CustomAction>
  28. </Elements>

As you can see, the SetItemDataButtonCommand command has a CommandAction that is bound to the updateSelectedItems JavaScript method, and the EnabledScript attribute is set to the  isNonFolderSelected method.

This latter one ensures that the ribbon button will be disabled until at least one non-folder item is selected in the list. It can be achieved by iterating through the selected items and checking their fsObjType property, as illustrated by the following code:

  1. function isNonFolderSelected() {
  2.     var selectedItems = SP.ListOperation.Selection.getSelectedItems();
  3.     var result = false;
  4.  
  5.     for (var i = 0; i < selectedItems.length; i++) {
  6.         // it is a list item, not a folder
  7.         if (selectedItems[i].fsObjType == 0) {
  8.             result = true;
  9.             break;
  10.         }
  11.     }
  12.  
  13.     return result;
  14. }

If the user clicks on the button, the updateSelectedItems method is invoked. This method gets a reference for the current web and list objects, takes the selected items, and for each non-folder item sets the Numeric field to zero. Finally it calls executeQueryAsync to submit the changes.

  1. var inProgress = false;
  2. var selectedListId;
  3.  
  4. function updateSelectedItems() {
  5.     if (inProgress) {
  6.         alert("Another request is in progress. Try again later!");
  7.     }
  8.     else {
  9.         try {
  10.             var context = SP.ClientContext.get_current();
  11.             var web = context.get_web();
  12.             var selectedItems = SP.ListOperation.Selection.getSelectedItems();
  13.             this.selectedListId = SP.ListOperation.Selection.getSelectedList();
  14.             for (var i = 0; i < selectedItems.length; i++) {
  15.                 // it is a list item, not a folder
  16.                 if (selectedItems[i].fsObjType == 0) {
  17.                     var listItem = web.get_lists().getById(selectedListId).getItemById(selectedItems[i].id);
  18.                     context.load(listItem);
  19.                     listItem.set_item("Numeric", 0);
  20.                     listItem.update();
  21.  
  22.                 }
  23.             }
  24.  
  25.             context.executeQueryAsync(Function.createDelegate(this, itemsUpdated), Function.createDelegate(this, updateFailed));
  26.         }
  27.         catch (e) {
  28.             alert("Error: " + e);
  29.             inProgress = false;
  30.         }
  31.     }
  32. }
  33.  
  34. function updateFailed(sender, args) {
  35.     alert("Operation failed: " + args.get_message());
  36.     inProgress = false;
  37. }
  38.  
  39. function itemsUpdated() {
  40.     inProgress = false;
  41.     var convertedListId = selectedListId.toLowerCase().replace("-", "_").replace("{", "").replace("}", "");
  42.     var controlId = 'ctl00$m$g_' + convertedListId + '$ctl02';
  43.     __doPostBack(controlId, 'cancel');
  44.     //window.location.href = window.location.href;
  45. }

On successful execution of the request the itemsUpdated method is called. It updates the UI asynchronously as described in this post.

To enable asynchronous updates, theoretically you would have to ‘Enable Asynchronous Update’ at AJAX Options of the list web part, although based on my last tests the method works even if the asynchronous update is not allowed.

image

If you don’t need the asynchronously update, you can simply use this line to reload the whole page after Numeric values are set for selected items.

window.location.href = window.location.href;

image

Blog at WordPress.com.