Creating custom listitem menu’s (ECB)

This week I was working on a custom packaging solution for Look And Feel. A colleague of mine needed to add menu items to the List Item menu's of the Style library, the masterpage library and the site collection images. Our first thought was to use CustomActions in a feature to implement this, but soon we found some problems:

Register only on the specified libraries

First of all it is impossible to define a custom action only for the lists that we needed. The RegistrationID property of the CustomAction element gives some form of control, but not as specific as we needed. For more info on Custom Actions see the following articles:

Rights parameter insufficient

Secondly we needed to only show the menu when a user was part of a certain group. CustomActions allow you to specify the rights the user must have in order to see a certain menu-item. Again this was of no value to us because there were several groups with the same rights, but we only wanted the menu-items for a certain group.

ControlAssembly not allowed on Listitem ECB

Our last attempt was to write a custom control that renders the menu-item. If we were able to create a server-side control that could check whether we were on the right list and find out about the user roles…we were off to go…but after some reading we found out that the ControlAssembly option is not available for listitem menu's (see Jan Tielen's blog) since the menu is rendered entirely by javascript.

Solution

I started looking at the core.js (/_layouts/1033/core.js) which contains the menu code and found the functions & mechanisms to create menu items. If you click on a menu-item the menu is rendered by the CreateMenuEx function. This function basically calls several other functions which are all responsible for a subset of menu-items. One of these functions is AddDocLibMenuItems (m,ctx). The AddDocLibMenuItems function takes two parameters. The first parameter, called m, represents the menu object itself; the second parameter, ctx, provides HTTP context information about the web request. I didn't want to edit the core.js file since it is a system file, so I came up with the following solution:

//Store reference to original function
var original_AddDocLibMenuItems = AddDocLibMenuItems;

//Override the original function
AddDocLibMenuItems = function(m,ctx) {
 strDisplayText="Test";
 strAction="alert('ok')";
 strImagePath="";
  
 CAMOpt(m, strDisplayText, strAction, strImagePath);
 CAMSep(m);
  
 //Call the original function
 original_AddDocLibMenuItems(m,ctx);
}

The code is inserted on the listview form pages of the Style library, the masterpage library and the site collection images using a content editor webpart. What it does is override the AddDocLibMenuItems function and assign the original function to a variable. This is necessary to be able to call the original function once we injected our own code. Using this mechanism there is no need to make changes to the core.js file!

Adding a menu item to the menu requires just one function call:

CAMOpt(m, strDisplayText, strAction, strImagePath);

The CAMOpt function takes four parameters: the menu object to add the new item to, the display text of the menu item, the javascript action to perform when the item is clicked and a path to an image file to associate with the item. A call to the CAMSep function adds the separator bar to the menu. Both these functions are defined in the menu.js file on the SharePoint server.

Now if you know all about the wonderful SPAPI libraries to use the SharePoint webservices in javascript, then you know you might implement whatever you want and create advanced menuitems. My complete solution:

<script type="text/javascript" src="/_layouts/js/jquery-1.3.2.min.js"></script>
<script type="text/javascript" src="/_layouts/js/SPAPI_Core.js"></script>
<script type="text/javascript" src="/_layouts/js/SPAPI_Lists.js"></script>
<script type="text/javascript" src="/_layouts/js/SPAPI_UserGroup.js"></script>
<script type="text/javascript" src="/_layouts/js/SPAPI_UserProfile.js"></script>

<script type="text/javascript">
$(document).ready(function () {
 var userName = getCurrentUserName();
   if (userName != null){
        var accountName = getAccountName(userName);
        if(accountName != null) {
          if(isMemberOfGroup(accountName,"Blog - Test site Members")) {
    showLink();
   }
        }
      
     }   
});



//Helper functions
function showLink() {
 //Store reference to original function
 var original_AddDocLibMenuItems = AddDocLibMenuItems;

 //Override the original function
 AddDocLibMenuItems = function(m,ctx) {
    strDisplayText="Test";
    strAction="alert('ok')";
    strImagePath="";
  
    CAMOpt(m, strDisplayText, strAction, strImagePath);
    CAMSep(m);
  
    //Call the original function
    original_AddDocLibMenuItems(m,ctx);
 }
}

function isMemberOfGroup(accountName,groupName) {
   var usergroup = new SPAPI_UserGroup('http://moss/sites/blog');
        var items = usergroup.getGroupCollectionFromUser(accountName);
    
        if(items.status == 200)
 {
  if($(items.responseXML).find("Group[Name='" + groupName + "']").size() > 0)
  {
   return true;
  } 
  else 
  {
   return false;
  }
        } else {
  return false;
 }
        
}

function getAccountName(userName)
{
     var profile = new SPAPI_UserProfile('http://moss/sites/blog')
     var p = profile.getUserProfileByName(userName);

     if (p.status == 200)
     {
         var properties = p.responseXML.getElementsByTagName('PropertyData');
        var propertyValues = new Array();

        for (var i=0; i < properties.length; i++)
        {
          var propName = properties[i].getElementsByTagName('Name')[0].childNodes[0].nodeValue;
          propertyValues[propName] = properties[i].getElementsByTagName('Value');
        }
        return propertyValues['AccountName'][0].childNodes[0].nodeValue;
  
     }
     else
     {
         return null;
     }
}


function getCurrentUserName()
{
    var lists = new SPAPI_Lists('http://moss/sites/blog')
    var items = lists.getListItems(
    'User Information List',
    '',
    '<Query><Where><Eq><FieldRef Name="ID"/><Value Type="Counter">' + _spUserId + '</Value></Eq></Where></Query>',  // query
    '<ViewFields><FieldRef Name="Name"/></ViewFields>',
    1,  // rowLimit
    ''  // queryOptions
    );
    
    if (items.status == 200)
    {
        var rows = items.responseXML.getElementsByTagName('z:row');
        if (rows.length == 1)
        {
            return rows[0].getAttribute('ows_Name');
        }
        else
        {
            return null;
        }
    }
    else
    {
        return null;
    }
} 
</script>

To create an easy deployable solution you might want to opt to insert this code through a delegate control and just perform some additional checks on the location. I know this is not the most beautiful solution, but based on the design of these menus I can't think of a better way to implement this. Of course, all suggestions are more than welcome!