Tuesday, March 4, 2014

Displaying The List Of Custom Actions And Custom Action Groups

copyright from: pholpar.wordpress.com
As you surely knows, user actions provide an excellent way to extend SharePoint UI. They were there in the WSS 3.0 version as well, although the support was limited to the declarative feature elements. In SharePoint 2010 there is an option to add / remove user actions using SharePoint Designer or custom code as well.
Recently I was to query for some custom actions in SharePoint 2010. You can find code for that on the web, but it is easy to create our own version as well.
On the server side the SPSiteSPWeb and SPList classes has their UserCustomActions properties (type of SPUserCustomActionCollection that is basically a list ofSPUserCustomAction instances), and on the client side the case is similar, having SiteWeb and List classes and their UserCustomActions properties (type ofUserCustomActionCollection, a list of UserCustomAction instances).
Side note 1 (for advanced readers): These custom actions are stored in the CustomActions table of the content database, and queried through theproc_GetCustomActionsFromScope stored procedure. This SP is called from the LoadUserCustomActionsFromDataSource method of SPUserCustomActionCollection class that is invoked from the Ensure method. The Ensure method is called from the constructors of SPUserCustomActionCollection class that is invoked in the getter of theUserCustomActions property of the appropriate SPSiteSPWeb or SPList object.
Side note 2 (less technical): Seeing the User prefix in the above mentioned property and class names (like UserCustomActionsSPUserCustomActionCollection) raised doubt about whether really these objects can give the solution for my requirement. I had to list built-in custom actions, not user defined ones.
Back to our code, it probably should look like this one on the server side to list custom actions for site / web / list:
  1. SPList list = web.Lists["CustomList"];
  2. web.Site.UserCustomActions.ToList().ForEach(
  3.     customAction => Console.WriteLine("Site custom action title: '{0}', description: '{1}'", customAction.Title, customAction.Description));
  4. web.UserCustomActions.ToList().ForEach(
  5.     customAction => Console.WriteLine("Web custom action title: '{0}', description: '{1}'", customAction.Title, customAction.Description));
  6. list.UserCustomActions.ToList().ForEach(
  7.     customAction => Console.WriteLine("List custom action title: '{0}', description: '{1}'", customAction.Title, customAction.Description));
And on the client side the code is very similar:
  1. ClientContext clientContext = new ClientContext("http://sp2010");
  2. Site site = clientContext.Site;
  3. Web web = clientContext.Web;
  4. List list = web.Lists.GetByTitle("CustomList");
  5. clientContext.Load(site, s => s.UserCustomActions);
  6. clientContext.Load(web, w => w.UserCustomActions);
  7. clientContext.Load(list, l => l.UserCustomActions);
  8. clientContext.ExecuteQuery();
  9. site.UserCustomActions.ToList().ForEach(
  10.     customAction => Console.WriteLine("Site custom action title: '{0}', description: '{1}'", customAction.Title, customAction.Description));
  11. web.UserCustomActions.ToList().ForEach(
  12.     customAction => Console.WriteLine("Web custom action title: '{0}', description: '{1}'", customAction.Title, customAction.Description));
  13. list.UserCustomActions.ToList().ForEach(
  14.     customAction => Console.WriteLine("List custom action title: '{0}', description: '{1}'", customAction.Title, customAction.Description));
I’ve checked both codes, but as I expected, they did not provide the solution I needed, only the few custom actions created earlier by me were listed.
So where to go next? I had to dig deeper…
It was not hard to find the internal SPCustomActionElement class (Microsoft.SharePoint namespace in the Microsoft.SharePoint assembly), and on that track I was able to get to the internal QueryForCustomActions method of the  internal SPElementProvider class (same namespace and assembly as above). There are two overloads for  theQueryForCustomActions  method, the first has this parameter pattern:
SPWeb web, SPList list, string scope, string location, string groupId
The second one has an extra bool parameter called ignoreRights. This version used internally by the first one passing the last parameter as false. Now I did not want to trick with permissions, so I chose the simpler first version.
Side note 3: There are a lot of useful methods in SPElementProvider, so I suggest you to have a closer look at this class. In this post I will use only QueryForCustomActions  and QueryForCustomActionGroups methods.
The following code demonstrates how to call QueryForCustomActions  using reflection to display non-user custom action information. The sample provided is far to be called performance optimized, but it is a good starting point to understand the method of working with internal classes and methods.
The DisplayCustomActions method calls the QueryForCustomActions  method and iterates through the result, displaying info through the DisplayCustomAction method. This method receives an Object that should be type of the internal SPCustomActionElement (we can’t declare its type at design time, since it is internal), and displays its string-based properties specified in the propsToDisplay array.
  1. private void DisplayCustomActions(SPWeb web, SPList list, String scope, String location, String groupId)
  2. {
  3.     // hack to get the Microsoft.SharPoint assembly
  4.     Assembly sharePointAssembly = typeof(SPWeb).Assembly;
  5.     // and a reference to the type of the SPElementProvider internal class
  6.     Type spElementProviderType = sharePointAssembly.GetType("Microsoft.SharePoint.SPElementProvider");
  7.  
  8.     ConstructorInfo ci_SPElementProvider = spElementProviderType.GetConstructor(BindingFlags.Public | BindingFlags.Instance,
  9.          nullnew Type[0], null);
  10.  
  11.     if (ci_SPElementProvider != null)
  12.     {
  13.         // spElementProvider will be of type internal class
  14.         // Microsoft.SharePoint.SPElementProvider
  15.         // defined in Microsoft.SharePoint assembly
  16.         Object spElementProvider = ci_SPElementProvider.Invoke(null);
  17.  
  18.         if (spElementProvider != null)
  19.         {
  20.             // we call
  21.             // internal List QueryForCustomActions(SPWeb web, SPList list, string scope, string location, string groupId)
  22.  
  23.             MethodInfo mi_QueryForCustomActions = spElementProviderType.GetMethod("QueryForCustomActions",
  24.                     BindingFlags.NonPublic | BindingFlags.Instance, null,
  25.                     new Type[] { typeof(SPWeb), typeof(SPList), typeof(String), typeof(String), typeof(String) }, null
  26.                     );
  27.             if (mi_QueryForCustomActions != null)
  28.             {
  29.                 // result is List
  30.                 IEnumerable customActions = (IEnumerable)mi_QueryForCustomActions.Invoke(spElementProvider,
  31.                     new Object[] { web, list, scope, location, groupId });
  32.                 customActions.Cast<Object>().AsQueryable().ToList().ForEach(
  33.                     customAction => DisplayCustomAction(customAction,
  34.                     "Title""Description""GroupId""Location"));
  35.             }
  36.         }
  37.     }
  38. }
  39.  
  40. private void DisplayCustomAction(object customAction, params String[] propsToDisplay)
  41. {
  42.     // hack to get the Microsoft.SharPoint assembly
  43.     Assembly sharePointAssembly = typeof(SPWeb).Assembly;
  44.     // and a reference to the type of the SPCustomActionElement internal class
  45.     Type spCustomActionElementType = sharePointAssembly.GetType("Microsoft.SharePoint.SPCustomActionElement");
  46.  
  47.     // runtime check the type of the parameter
  48.     if (customAction.GetType() == spCustomActionElementType)
  49.     {
  50.         List<String> propValues = new List<String>();
  51.         propsToDisplay.ToList().ForEach(propToDisplay =>
  52.             {
  53.                 System.Reflection.PropertyInfo pi = spCustomActionElementType.GetProperty(
  54.                     propToDisplay, BindingFlags.Public | BindingFlags.Instance);
  55.                 if (pi != null)
  56.                 {
  57.                     propValues.Add(String.Format("{0}: {1}", propToDisplay, pi.GetValue(customAction, null)));
  58.                 }
  59.             }
  60.         );
  61.         if (propValues.Count > 0)
  62.         {
  63.             Console.WriteLine(String.Format(String.Join("; ", propValues.ToArray())));
  64.         }
  65.         
  66.     }
  67. }
The second code example is for the user action groups, it is very similar to the first code block. In this case we display TitleRequiredAdmin (the admin level required for this element) and Id (this one is not defined on the SPCustomActionGroupElement level, but inherited from the Microsoft.SharePoint.Administration.SPElementDefinition base class).
  1. private void DisplayCustomActionGroups(SPWeb web, String scope, String location)
  2. {
  3.     // hack to get the Microsoft.SharPoint assembly
  4.     Assembly sharePointAssembly = typeof(SPWeb).Assembly;
  5.     // and a reference to the type of the SPElementProvider internal class
  6.     Type spElementProviderType = sharePointAssembly.GetType("Microsoft.SharePoint.SPElementProvider");
  7.  
  8.     ConstructorInfo ci_SPElementProvider = spElementProviderType.GetConstructor(BindingFlags.Public | BindingFlags.Instance,
  9.          nullnew Type[0], null);
  10.  
  11.     if (ci_SPElementProvider != null)
  12.     {
  13.         // spElementProvider will be of type internal class
  14.         // Microsoft.SharePoint.SPElementProvider
  15.         // defined in Microsoft.SharePoint assembly
  16.         Object spElementProvider = ci_SPElementProvider.Invoke(null);
  17.  
  18.         if (spElementProvider != null)
  19.         {
  20.             // we call
  21.             // internal List QueryForCustomActionGroups(SPWeb web, SPList list, string scope, string location, string groupId)
  22.  
  23.             MethodInfo mi_QueryForCustomActionGroups = spElementProviderType.GetMethod("QueryForCustomActionGroups",
  24.                     BindingFlags.NonPublic | BindingFlags.Instance, null,
  25.                     new Type[] { typeof(SPWeb), typeof(String), typeof(String) }, null
  26.                     );
  27.             if (mi_QueryForCustomActionGroups != null)
  28.             {
  29.                 // result is List
  30.                 IEnumerable customActionGroups = (IEnumerable)mi_QueryForCustomActionGroups.Invoke(spElementProvider,
  31.                     new Object[] { web, scope, location });
  32.                 customActionGroups.Cast<Object>().AsQueryable().ToList().ForEach(
  33.                     customActionGroup => DisplayCustomActionGroup(customActionGroup,
  34.                     "Title""Id""RequiredAdmin"));
  35.             }
  36.         }
  37.     }
  38. }
  39.  
  40. private void DisplayCustomActionGroup(object customActionGroup, params String[] propsToDisplay)
  41. {
  42.     // hack to get the Microsoft.SharPoint assembly
  43.     Assembly sharePointAssembly = typeof(SPWeb).Assembly;
  44.     // and a reference to the type of the SPCustomActionGroupElement internal class
  45.     Type spCustomActionGroupElementType = sharePointAssembly.GetType("Microsoft.SharePoint.SPCustomActionGroupElement");
  46.  
  47.     // runtime check the type of the parameter
  48.     if (customActionGroup.GetType() == spCustomActionGroupElementType)
  49.     {
  50.         List<String> propValues = new List<String>();
  51.         propsToDisplay.ToList().ForEach(propToDisplay =>
  52.         {
  53.             System.Reflection.PropertyInfo pi = spCustomActionGroupElementType.GetProperty(
  54.                 propToDisplay, BindingFlags.Public | BindingFlags.Instance);
  55.             if (pi != null)
  56.             {
  57.                 propValues.Add(String.Format("{0}: {1}", propToDisplay, pi.GetValue(customActionGroup, null)));
  58.             }
  59.         }
  60.         );
  61.         if (propValues.Count > 0)
  62.         {
  63.             Console.WriteLine(String.Format(String.Join("; ", propValues.ToArray())));
  64.         }
  65.  
  66.     }
  67. }
And here is a short example about the usage of the above methods:
  1. DisplayCustomActionGroups(web, null"Microsoft.SharePoint.SiteSettings");
  2. DisplayCustomActions(web, nullnull"Microsoft.SharePoint.SiteSettings""SiteAdministration");
  3. DisplayCustomActions(web, list, "Site""CommandUI.Ribbon"null);
Unfortunately, the above described approach is not usable from client side code.
Note, that although you can specify scope, location, and group information, you can pass null for example for scope and location. In this case the custom actions are not filtered for that value.
For a list of possible values for location and group IDs, see the page on MSDN:
Regarding the scope parameter, I found, that when you set something totally wrong, like “MyScope”, then the following exception is thrown (you should check it in theInnerException property of the top level exception System.Reflection.TargetInvocationException):
ArgumentException "Invalid feature scope ‘MyScope’. Valid values are Farm, WebApplication, Site, or Web."
The exception is thrown, because validation in the StringToScope method in SPFeature class failed. Valid values are there:
"Farm", "WebApplication", "WssWebApplication", "Site", "Web"
However, unless you set “Site” or “Web” as scope, you get another ArgumentException exception with a simple Message property saying "scope". It comes fromStringToUserCustomActionScope method in SPUserCustomAction class, where valid values are only:
"Site", "Web", "List"

So it means we are limited here to “Site” or “Web” scoped custom actions. But what is then that SPList parameter when calling QueryForCustomActions? As far as I see from the reflected code, it is used only to aggregate user custom action into the result and a permission check. How to get then list scoped custom actions, ECBs, etc.? Well, I think it should be another post…

No comments: