Patrick's SharePoint Blog

SharePoint's Booming world

Posts Tagged ‘Populate Dynamically’

Adding a custom company menu tab with dynamic menu on the ribbon

Posted by Patrick Boom on May 25, 2010

The move to include the ribbon in SharePoint provides additional challenges to site designers. They have to include it somehow in their designs, as the ribbon is here to stay. One of our clients had an own navigation menu that he wanted to integrate with the ribbon. The problem is that this menu is dynamic and can change at any moment in time. And the SharePoint 2010 ribbon is static by nature. It is defined in XML files on startup.

That made me wonder whether it would be possible to dynamically add menu items to controls, tabs and groups. I then stumbled upon a blog post by a company called ICC here that described a method of dynamic flyout anchors. Another post described here by Tom Wilson described how we could change the look and feel of the ribbon to fit the internal company design.

I basically used the method described by ICC to dynamically construct my menu items on a separate tab. The source of the menu is a web service that provides the menu items in XML. An XSL transformation then creates the necessary xml definition for SharePoint, disconnecting the web service from technology specific implementations. This resulted in a combination of custom defined tabs, server side command handlers, page components and the works, meaning quite a comprehensive piece of code and a lot for this article. Although I have tried to be as complete as possible, I might have described some parts in less detail then I should. If so, feel free to leave a comment. Some stuff that will be covered in this one example:

  1. Creating Tabs and Groups on the ribbon
  2. Populating FlyoutAnchor dynamically
  3. Registering commands on the server
  4. Calling server side code using ICallbackEventHandler interface

Quite some stuff to cover here. Let us first take a look at an architectural picture on what we are trying to accomplish here:

Define the Tab, Group and Control elements.

 So, let us start coding. Open Visual Studio 2010 and create a new empty SharePoint project. Add a new empty element and call it MyCompany. Also add a mapping to the Layouts folder by right clicking the project and select Add à SharePoint Mapped Layouts folder. This will host our custom JavaScript files. Open the Elements.xml file in the MyCompany empty element and populate it with the following XML:

<?xml version=1.0encoding=utf-8?>
<Elements xmlns=http://schemas.microsoft.com/sharepoint/>
<Control 
 
Id=AdditionalPageHead
 
Sequence=200
 
ControlClass=$SharePoint.Project.FileNameWithoutExtension$.CompanyTabLoader
 
ControlAssembly=$SharePoint.Project.AssemblyFullName$>
</Control>
<CustomAction
  Id=Boom.Ribbon.EnterpriseTabExample
  Title=Enterprise Menu
  Location=CommandUI.Ribbon>
<CommandUIExtension>
 
<CommandUIDefinitions>
   
<CommandUIDefinition Location=Ribbon.Tabs._children>
     
<Tab
        Id=Boom.Ribbon.EnterpriseTab
        Sequence=250
        Title=Enterprise Menu>
        
<Scaling Id=Boom.Ribbon.Enterprise.Scaling>
         
<MaxSize Id=Boom.Ribbon.Enterprise.Scaling.MyCompany.MaxSizeSequence=20GroupId=Boom.Ribbon.Enterprise.MyCompanySize=LargeMedium />
         
<MaxSize Id=Boom.Ribbon.Enterprise.Scaling.MyJob.MaxSizeSequence=40GroupId=Boom.Ribbon.Enterprise.MyJobSize=LargeMedium />
          
<MaxSize Id=Boom.Ribbon.Enterprise.Scaling.MyHR.MaxSizeSequence=40GroupId=Boom.Ribbon.Enterprise.MyHR” Size=LargeMedium />
         
<Scale Id=Boom.Ribbon.Enterprise.Scaling.MyCompany.MediumSmallSequence=100GroupId=Boom.Ribbon.Enterprise.MyCompanySize=MediumSmall />
         
<Scale Id=Boom.Ribbon.Enterprise.Scaling.MyJob.MediumSmallSequence=120GroupId=Boom.Ribbon.Enterprise.MyJobSize=MediumSmall />
         
<Scale Id=Boom.Ribbon.Enterprise.Scaling.MyHR.MediumSmallSequence=120GroupId=Boom.Ribbon.Enterprise.MyHRSize=MediumSmall />
        </Scaling>
       
<Groups Id=Boom.Ribbon.Enterprise.Groups>
         
<Group
            Id=Boom.Ribbon.Enterprise.MyCompany
            Title=My Company Menu
            Template=Ribbon.Templates.Flexible2
            Sequence=100>
           
<Controls Id=Boom.Ribbon.Enterprise.MyCompany.Controls>
             
<FlyoutAnchor
                Id=Boom.Ribbon.Enterprise.MyCompany.Menu

                Command=Boom.Ribbon.Enterprise.MyCompany.Menu

                Sequence=10

                Image16by16=/_layouts/$Resources:core,Language;/images/formatmap16x16.png
                Image16by16Top=-16
                Image16by16Left=-88

                Image32by32=/_layouts/$Resources:core,Language;/images/formatmap32x32.png
                Image32by32Top=-128
                Image32by32Left=-448

                LabelText=My Company Menu

                TemplateAlias=o1

                PopulateDynamically=true

                PopulateOnlyOnce=false

                PopulateQueryCommand=PopulateDymamicMenuItemsQueryCommand

                ToolTipTitle=FlyoutAnchor Dymamic

                ToolTipDescription=FlyoutAnchor with dymamic menu items />
             
</Controls>
           
</Group>
         
</Groups>
       
</Tab>
     
</CommandUIDefinition>
   
</CommandUIDefinitions>
   
<CommandUIHandlers>
     
<CommandUIHandler
        Command=Boom.Ribbon.Enterprise.MyCompany.Menu
       
CommandAction=“”
        EnabledScript=true />
     
<CommandUIHandler
        Command=DynamicButtonCommand

        CommandAction=JavaScript:alert(‘Dynamic Button ‘ + arguments[2].MenuItemId + ‘ clicked.’);

        EnabledScript=true />
    
</CommandUIHandlers>
  
</CommandUIExtension>
</CustomAction>
</Elements>
 

So, what did we just include? In this XML, we define a custom action that will add a Tab to our ribbon called Boom.Ribbon.EnterpriseTab. In there, we define some scaling on how the controls in the groups should be rendered and obviously, the groups itself. In this example, I only defined one group called MyCompany for simplicity, although I did added scaling for additional groups. For the purpose of this blog however, we keep it with one group. In the group, we define a single FlyoutAnchor control that will be the focus subject on this blog. For additional information about the Tab and Group definition, there are many articles out there that describe these. For instance, refer to the blogs by Chris O’Brien on this subject.

So let us take a closer look at the FlyoutAnchor node. The Id and Command attributes follow the naming convention (project-ribbon-tab-group) as best practice, although they can be anything you like. The following attributes are of more interest: PopulateDynamically, PopulateOnlyOnce and PopulateQueryCommand. These three attributes determine that the controls in this menu will be populated dynamically. PopulateOnlyOnce determines that this will only happen the first time it is loaded, which we set to false. The PopulateQueryCommand specifies the command that will be called to populate the control. The command has to set the properties.PopulationXml property with the exact declarative XML as we would use when not loading dynamically. We will get to that part later.

But, as one can see, the command mentioned in the PopulateQueryCommand is not registered in the CommandUIHandlers section of the XML. This is because we will do this registration on the server side. We do however register two commands that make the control enabled (Boom.Ribbon.Enterprise.MyCompany.Menu) and the command that each button will execute when clicked (DynamicButtonCommand). So, how do we now wire the commands to this definition? We will get to that when we solve another problem. The above declared tab will not show up in our interface if we deploy this solution now. That is because we have omitted the RegistrationId and RegistrationType attributes in our custom action, meaning it is not tied to any context yet. As we want the tab to appear always, we have to make sure that the tab is made visible when each page loads. We do this by inserting a custom control that will do this in the pre-render phase of the page, using the AdditionalPageHead control placeholder. And since we then have a control anyway, we can also use that to wire the commands of our tab. So, add a custom class to the project. I have not included it in its own directory, but you could. I called this class CompanyTabLoader.cs. How do we ensure this is loaded on each page? Well, that is accomplished by adding the Control element at the beginning of above XML (or end if you like).

Adding a Control to the AdditionalPageHead on each page

 If fact, this is quite simple to accomplish. The following declaration adds our control to the AdditionalPageHead section of the page:

<Control
 
Id=AdditionalPageHead
 
Sequence=200
 
ControlClass=$SharePoint.Project.FileNameWithoutExtension$.CompanyTabLoader
 
ControlAssembly=$SharePoint.Project.AssemblyFullName$>

Some nice bonus in above declaration. We use the VS 2010 placeholders for adding our complete assembly name upon deployment, so we do not have to fill that in ourselves ;-). By far, this is the easiest way to get code running on each page. As shown, this declaration tells SharePoint to include our custom control in each page by adding it to the AdditionalPageHead container. Now, we can add code to our custom control to complete the wiring of all our events.

Show the custom tab each time the page is loaded

 To make sure our tab is visible on each page we need to call some methods in the PreRender event of our control. Override the OnPreRender event of our base class and add the following code. (Make sure CompanyTabLoader.cs inhereits from System.Web.UI.WebControls.WebControl. Also add references to our project to the following assemblies: Microsoft.SharePoint.dll, System.Web.dll, Microsoft.Web.CommandUI.dll.

protected override void OnPreRender(EventArgs e)
{
  SPRibbon ribbon = Microsoft.SharePoint.WebControls.SPRibbon.GetCurrent(this.Page);

 
  if
(ribbon != null)
{
    const string initialTabId = “Boom.Ribbon.EnterpriseTab”;

    if (!ribbon.IsTabAvailable(initialTabId))
     
ribbon.MakeTabAvailable(initialTabId);
  
}
}

We first get a reference to the SPRibbon by calling the static GetCurrent method and passing the current page. We then call the MakeTabAvailable method of the SPRibbon class and pass the ID of the Tab to make available. If you do not have this method, then add the reference to the Microsoft.Web.CommandUI assembly. This will ensure our tab is shown on each page in the site collection. We can now proceed to creating our PageComponent JavaScript to handle the custom commands that we will create.

Registering and creating a PageComponent

 The page component is a JavaScript object that we will use to assign are commands to in code. This component will be the bridge between your ribbon and your commands. This component can be very confusing and complex; I know it was for me. But then again, I am no JavaScript guru. In any way, this component tends to be generic and can be extended to fit multiple needs and commands. Let me show you mine PageComponent.js in the basis. I have added this JavaScript file to the Layouts mapped folder in our solution, in its own subdirectory.

function ULS_SP() {
  if (ULS_SP.caller) {
   
ULS_SP.caller.ULSTeamName = “Windows SharePoint Services 4”;
   
ULS_SP.caller.ULSFileName = “/_layouts/Boom.DynamicMenuSample/PageComponent.js”;
  
}
}

Type.registerNamespace(‘Boom.DynamicMenuSample’);

// RibbonApp Page Component
Boom.DynamicMenuSample.PageComponent = function () {
 
ULS_SP();
 
Boom.DynamicMenuSample.PageComponent.initializeBase(this);
}

Boom.DynamicMenuSample.PageComponent.initialize = function () {
 
ULS_SP();
 
ExecuteOrDelayUntilScriptLoaded(Function.createDelegate(null, Boom.DynamicMenuSample.PageComponent.initializePageComponent), ‘SP.Ribbon.js’);
}

Boom.DynamicMenuSample.PageComponent.initializePageComponent = function () {
 
ULS_SP();
  var ribbonPageManager = SP.Ribbon.PageManager.get_instance();

  if (null !== ribbonPageManager) {
   
ribbonPageManager.addPageComponent(Boom.DynamicMenuSample.PageComponent.instance);
   
ribbonPageManager.get_focusManager().requestFocusForComponent(Boom.DynamicMenuSample.PageComponent.instance);
 
}
}

Boom.DynamicMenuSample.PageComponent.refreshRibbonStatus = function () {
 
SP.Ribbon.PageManager.get_instance().get_commandDispatcher().executeCommand(Commands.CommandIds.ApplicationStateChanged, null);
}

Boom.DynamicMenuSample.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
Boom.DynamicMenuSample.PageComponent.registerClass(‘Boom.DynamicMenuSample.PageComponent’, CUI.Page.PageComponent);
Boom.DynamicMenuSample.PageComponent.instance = new Boom.DynamicMenuSample.PageComponent();

// Notify waiting jobs
NotifyScriptLoadedAndExecuteWaitingJobs(“/_layouts/Boom.DynamicMenuSample/PageComponent.js”);

No real specifics here. As you can see, I declare a namespace and a class (prototype) to handle various events on the ribbon. Also some constructors (initializers) are declared here. The actual wiring to our custom controls will be done in code. You can reuse this page component for other extensions to the ribbon if desired.

Registering our commands and the PageComponent

 Now we need to register our page component and our custom commands with the page. We move back to our custom control called CompanyTabLoader.cs. In the OnPreRender event, we create a new generic list of IRibbonCommand that will hold our custom commands. We add a new SPRibbonCommand to that list that wires our PopulateDynamicMenuItemsQueryCommand to a command on the ribbon.

var commands = new List<IRibbonCommand>();

// register the command at the ribbon. Include the callback to the server to generate the xml
commands.Add(new SPRibbonCommand(“PopulateDymamicMenuItemsQueryCommand”, “CreateServerMenu(”,”); properties.PopulationXML = menuXml;”));

As you can see in above code sample, we create a new SPRibbonCommand object with two arguments. The first is the name of the command to register, in our case the command mentioned in the declarative XML above. The second argument specifies the code to execute when this command is called. We have included two statements here. First we call CreateServerMenu with two empty arguments. That will be our server side method that will be wired in a few moments. Secondly, we set the properties.PopulationXML to the result of the server call that is loaded in the menuXml variable. We now need to register this command collection (with only one command in our case) with our page component. That is done using the following code:

//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/Boom.DynamicMenuSample/PageComponent.js”, false, “Boom.DynamicMenuSample.PageComponent.initialize()”});


// Register ribbon scripts
manager.RegisterGetCommandsFunction(Page, “getGlobalCommands”, commands);
manager.RegisterCommandEnabledFunction(Page, “commandEnabled”, commands);
manager.RegisterHandleCommandFunction(Page, “handleCommand”, commands);

We create an instance of the SPRibbonScriptManager class and add the initialize function of our page component. After that, we register our commands with the three events of our page component that return an instance of the event, getGlobalCommands, commandEnabled and handleCommand. The last will be called when a command is executed on the ribbon in our page component. We should now wire our server side method mentioned in the command.

Add a callback eventhandler for server side processing

 For this, we implement the ICallbackEventHandler on our class. That will add two methods called GetCallbackResult and RaiseCallbackEvent. The latter will be called by the JavaScript, the first will be processed prior to returning to the client. In our example, we just add the menu XML needed for our FlyoutAnchor in the first method. Using variables, we can pass values from one method to another. For simplicity sake, I did not. Here is the contents of our GetCallbackResult method.

/// <summary>
/// This method will return the caller with the menu. It will now return a static menu, but you can call any webservice from here or the

/// RaiseCallbackEvent event and perform transformation there.

/// </summary>

/// <returns></returns>

public string GetCallbackResult()
{
  string dynamicMenuXml =
“<Menu Id=’Boom.Ribbon.Enterprise.MyCompany.Menu.Menu’>”
  
+ “<MenuSection Id=’Boom.Ribbon.Enterprise.MyCompany.Menu.Section1′ DisplayMode=’Menu16′>”
 
+ “<Controls Id=’Boom.Ribbon.Enterprise.MyCompany.Menu.Section1.Controls’>”;

  string
buttonXML = String.Format(

    “<Button Id=’DynamicButton{0}’ “
   
+ “Command=’DynamicButtonCommand’ “
    
+ “MenuItemId='{0}’ “
   
+ “LabelText=’My Custom menu 1′ “
   
+ “ToolTipTitle=’My Custom menu 1′ “
   
+ “ToolTipDescription=’Dynamic Button’ />”, 0);

   buttonXML = buttonXML + String.Format(

     “<Button Id=’DynamicButton{0}’ “
    
+ “Command=’DynamicButtonCommand’ “
    
+ “MenuItemId='{0}’ “
     
+ “LabelText=’My Custom menu 2′ “
    
+ “ToolTipTitle=’My Custom menu 2′ “
    
+ “ToolTipDescription=’Dynamic Button’ />”, 1);

   buttonXML = buttonXML + String.Format(
     “<Button Id=’DynamicButton{0}’ “
     + “Command=’DynamicButtonCommand’ “
    
+ “MenuItemId='{0}’ “
    
+ “LabelText=’My Custom menu 3′ “
    
+ “ToolTipTitle=’My Custom menu 3′ “
    
+ “ToolTipDescription=’Dynamic Button’ />”, 2);

   buttonXML = buttonXML + String.Format(
     “<Button Id=’DynamicButton{0}’ “
    
+ “Command=’DynamicButtonCommand’ “
    
+ “MenuItemId='{0}’ “
    
+ “LabelText=’My Custom menu 4′ “
    
+ “ToolTipTitle=’My Custom menu 4′ “
    
+ “ToolTipDescription=’Dynamic Button’ />”, 3);

  dynamicMenuXml += buttonXML;
 
dynamicMenuXml += “</Controls>” + “</MenuSection>” + “</Menu>”;
 
  return
dynamicMenuXml;
}

The method creates the menu XML as expected by the control. One could also call a web service in the RaiseCallbackEvent method to get the basic menu xml and execute a XSL transformation in this method to construct this XML. I kept it simple and just did it in code here. The objective (have it done on the server), was already achieved. Once here, you can do whatever you like. Now we should wire these methods to a callback reference, so that our JavaScript can call this.

Wire server side callback to JavaScript

 So, we return to our OnPreRender event. In there, we add the following:

// register the client callbacks so that the JavaScript can call the server.
ClientScriptManager cm = this.Page.ClientScript;

String cbReference = cm.GetCallbackEventReference(this, “arg”, “ReceiveServerMenu”, “”);

String callbackScript = “function CreateServerMenu(arg, context) {“ + cbReference + “; }”;
cm.RegisterClientScriptBlock(this.GetType(), “CreateServerMenu”, callbackScript, true);


//Register script files

ScriptLink.RegisterScriptAfterUI(Page, “/_layouts/Boom.DynamicMenuSample/Boom.DynamicMenuSample.js”, false, true);

We instantiate the ClientScriptManager and add a callback reference to our class. The third argument in this reference is the name of the JavaScript function that will receive the response from our server, in our case ReceiveServerMenu. We then construct the client side script that wires our CreateServerMenu function to the callback and register it all as a client script block. Pffff, what a lot of wiring 😉 But we are almost there. Just hang on for a couple of more lines 😉

Add the JavaScript that will receive the server response

 Final thing we should do is write the client side JavaScript that receives the response and assign the response to the menuXml variable, as mentioned in our custom command. For this, I have added yet another .js (Boom.DynamicMenuSample.js) file in our layouts folder, just to separate the page component from other scripts. To use this extra JavaScript file, we need to register it. This is done using the last line in the previous code sample. In the Boom.DynamicMenuSample.js file, add the following couple of (simple) lines :

// variable to hold the server menu
var menuXml;

// This function will receive the callback from the server with the menu items.
function ReceiveServerMenu(arg, context) {
 
menuXml = arg;
}

The menuXml variable is used to assign it to the properties.PopulationXML in our command declaration. The ReceiveServerMenu function is referenced in our callback reference declaration above. This is the callback function that will be called when the server completes its request. The return value of that function will be assigned to the ‘arg’ argument passed. In the method, we then assign the return value to the menuXml variable (which in turn will be assigned to the properties.PopulationXML in the command), which completes our circle.

Build and deploy

 Now that we have completed our coding, all we have to do is click and deploy and watch the fruits of our labor. If you are lucky, you will see something similar to below. Be patient on the first load though, as the web application still needs to spin up.

Some considerations

Now that we have completed this exercise, one could argue whether this would be the approach to create a menu. Some advantages:
Although I have not tried it, the controls that are dynamically created do not have to be buttons. It could also be FlyoutAnchor’s again, which creates nested menus.
Because we can customize also the look and feel of the ribbon, it would be possible to make it look like a normal menu, as seen many times on sites.
Menu items could easily be added or removed, without the need to redeploy code.

Disadvantages can primarily be found on networks with high latency, where this approach would not be valuable. The menu would simple take too long to load.
Secondly, this approach would be rather ‘chatty’, meaning a lot of request would go back and forth. Some kind of caching mechanism on the server side is preferred.

Anyway, it does show all the possibilities you have with the ribbon. Dynamic controls, server side processing, dynamic command registration on the server, declarative commands, tabs and groups. Have fun playing around with this.

The example project with code can be downloaded here.

UPDATE 24-7-2013: 

It seems like multiple people have issues with the pageComponent.js class as at regular times, it throws a JS error. This is also true for SP 2013. It occurs with getGlobalCommands() function. In this post people solved it by implementing the exact commands to execute. Unfortunately, I have no other solution.

Posted in SharePoint 2010 | Tagged: , , , , , , , , | 47 Comments »

 
%d bloggers like this: