Patrick's SharePoint Blog

SharePoint's Booming world

Posts Tagged ‘Ribbon Extension’

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.

Advertisements

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

Extending the Ribbon with a Send Unique Link Button

Posted by Patrick Boom on April 19, 2010

In one of my previous posts (located here), I discussed the Unique Document ID feature that can be enabled on all document libraries within the site collection. In that post, I also mentioned that it was strange that the E-mail a Link button on the ribbon then still uses the actual url of the document, instead of the virtualized unique url of the document. Why would you have a unique url to a document that you cannot easily send?

So, I decided to add my own button to do so. In the end, it is not that difficult, but a lot of new concepts of SharePoint 2010 can be caught in this single venture. To name a few, Extending the Ribbon interface, the Client Object Model (javascript) and Custom Actions. So, without further due, let us continue.

The Approach

We need to accomplish several things to make all of this work.

  1. Add a button to the ribbon in the context of a document library.
  2. Assign a custom action to execute when the button is clicked.
  3. Obtain the selected item from the document library.
  4. Query the server to get the unique document url of the selected document
  5. Open a mail message containing a description (subject) and the unique url.
  6. Finally, make the button context ‘aware’, so that it is only enabled when a document is selected.

So, let us start with the first part, add a button to click.

Add a button to the ribbon

Let us examine the end state first. We would like a button to be added to the ribbon next to the original E-mail a Link button, like in the image below:

The ribbon is composed using a XML definition that contains three logical parts: Tabs, Groups and Controls. These are hierarchical, meaning that Tabs contain Groups and Groups contain Controls. A tab (for example Documents) or group (for example New) can be static (always available) or contextual.

In our case, we want to add our button (control) to the Share & Track group on the Documents tab. This marks our Location, which we will see later when we start coding. To be exact, the location of our control will be Ribbon.Documents.Share.Controls, where Controls is the container of the group Share on the tab Documents that is located on the ribbon. Sounds logical enough right?

So, when we want to add something on the ribbon (where we can also create our own tabs, groups and contextual controls), we first need to find out the location. Based on the convention, it would be easy enough to figure out, but sometimes the naming is not exactly equal to that displayed in the UI. The following msdn article mentions all default locations for the ribbon, but an excessive overview is beyond the scope of this blog. http://msdn.microsoft.com/en-us/library/bb802730.aspx

Let’s start coding 😉 Open Visual Studio 2010 (this can also be done with VS 2008, but you must then build the package yourself. Visual Studio has far better build-in support for SharePoint 2010. Create a new empty SharePoint project and give it a name, in my case I use Boom.EmailUniqueLinkButton.

Enter the url of the site you want to debug to and use deploy as a farm solution option. Once done, right click the created project and add a new item to the project. Use the Empty Element type and give it a meaningful name, in my case EmailUniqueLinkButton.

Right click again on the project and add SharePoint “Layouts” Mapped Folder. This folder will contain the resources that will be used by our button, in our case the javascript. Also notice that a subfolder with the name of the project is automatically added beneath the Layouts folder. If not, you should add it manually. Right click the feature beneath the feature node (feature 1) and select rename. Rename it to a meaningful name, in my case Boom.EmailUniqueLink. If desired, you can change the default title in the properties also. The default scope of our feature is Web. We will keep it at that scope. When done, our solution will look like below image.

The elements.xml file will contain the xml definition needed to add our button to the ribbon. Open the file and add the following node to the xml (as child of Elements):

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
   <CustomAction
      
Id="Ribbon.EmailUniqueLink"
      
Location="CommandUI.Ribbon"
      
RegistrationId="101"
      
RegistrationType="List">
      
<CommandUIExtension>
         
<CommandUIDefinitions>
         
</CommandUIDefinitions>
         
<CommandUIHandlers>
         
</CommandUIHandlers>
      
</CommandUIExtension>
  
</CustomAction>
</Elements>
 

This needs some clarification. Beneath Elements, we define a CustomAction, which has the following attributes:

  • Id – This will uniquely identify our action, can be anything, but is usually something that could easily be related to the ribbon.
  • Location – This indicates where we want to add our custom action, in our case to the CommandUI.Ribbon. Mark though that this is not yet the location of our button! That will follow later.
  • RegistrationType – Although marked in the documentation as optional, it is not. RegistrationType indicates the type to which this action is bound. Could be List, FileType, ContentType or ProgId. In out case, it is List.
  • RegistrationId – Although marked in the documentation as optional, it is not. RegistrationId indicates the type id to which this action is bound. In our case the list type id (101 = Document Library). In case it was a content type, this would contain the content type id.

Below the CustomAction element, we can define a CommandUIExtension, which contains a CommandUIDefinitions and CommandUIHandlers. The former specifies what will be added, the latter specifies what happens when the control is used (in our case clicked).

Include a CommandUIDefinition node below CommandUIDefinitions. Add an attribute Location. The location will dictate where the control will be added. In our case, Ribbon.Documents.Share.Controls._children. Note the ‘_children’ behind Controls, which indicates to SharePoint that a new child should be added to the Controls collection. Now, finally we can add our button. Include the following xml beneath the CommandUIDefinition node.


<Button
  
Id="Ribbon.Documents.Share.EmailUniqueLink"
  
Command="Ribbon.Documents.Share.EmailUniqueLink"
  
Sequence="15"
  
Image16by16="/_layouts/$Resources:core,Language;/images/formatmap16x16.png"
  
Image16by16Top="-16"
   Image16by16Left="-88"
  
Image32by32="/_layouts/$Resources:core,Language;/images/formatmap32x32.png"
   Image32by32Top="-128"
   Image32by32Left="-448"
  
Description="Sends the unique link to the document by email"
  
LabelText="E-mail Unique Link"
  
ToolTipTitle="E-mail Unique Link"
  
ToolTipDescription="Sends the unique link to the document by e-mail"
  
TemplateAlias="o1"
/>

Before we get to the attributes, I wanted to reuse the image that was used for the original E-mail button. Partly because it would save me work, but more over because I am terrible in creating images and icons 😉 Secondly, I needed to check the Sequence, which defines the order in which the controls are displayed on the ribbon. As I wanted my new button to appear next to the original one, the sequence should be higher than the first, but lower than the following control. To check this, we can take a look at the default Ribbon definition located at 14\Template\Global\XML\CMDUI.xml. Search for Ribbon.Documents.Share.Controls, which defines the controls in the group we want to add to. There you can find the original EmailItemLink button that is the basis of our control.

<Button
  
Id=Ribbon.Documents.Share.EmailItemLink
  
Sequence=10
  
Command=EmailLink
  
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=$Resources:core,cui_ButEmailLink;
  
ToolTipTitle=$Resources:core,cui_ButEmailLink;
  
ToolTipDescription=$Resources:core,cui_STT_ButEmailLinkDocument;
  
TemplateAlias=o1
/>

Three attributes are very important to notice. Sequence, Command and TemplateAlias. The sequence of the original E-mail button is 10. The next control in the list (AlertMe) has a sequence of 20. We therefore choose 15 as the sequence for our control, so that it would still be possible to squeeze items in to both left and right in the future. The Command attribute defines the name of the command that is executed when clicked. We will define that later. The TemplateAlias defines the template for handling scaling and sizing. In order for our control to act like the other controls in the group, we have to choose the same template. Finally, we copy over the image references so that we can use the images that came out of the box. So let us now discuss all the attributes:

  • Id – Marks the unique id of our control. Usually, the convention is to follow the location naming followed by a meaningful name of the control, in our case EmailUniqueLink.
  • Sequence – The order in which the control should be shown. In our case 15.
  • Command – The name of the command to execute. Again, could be anything, but has to be unique, which is why I again use the naming convention.
  • ImageXX – The several image attributes define the images to use for both 16 px and 32 px and their locations. I used the same as the original one taken from the CMDUI.xml file.
  • Description – Describes the control.
  • LabelText – Defines what is shown beneath the button in the UI.
  • ToolTipTitle/ToolTipDescription– Shows the title and description when hovering over the button.

Now we have the button defined, but unless we define the action, it will do nothing. So, add the following xml snippet in the CommandUIHandlers section.

<CommandUIHandler
  
Command=Ribbon.Documents.Share.EmailUniqueLink
  
CommandAction=javascript:EmailUniqueLink();
  
EnabledScript=javascript:EnableEmailUniqueLink();
/>

Note that the Command attribute equals that defined in the button command attribute. In the CommandAction attribute, we define the action to be taken when pressed. In our case, we call a javascript function called EmailUniqueLink that will do the hard work for us later on. You can also define the function and related code inline in this attribute, but I rather have it in a separate .js file, both for easy debugging and maintenance. The final attribute here is called EnabledScript which defines the script that determines whether the button is enabled or not. The called function has to return a boolean. We will define the function later.

We have now defined the button and the action to be taken, but not yet the location of the javascript functions that will be called. Right click the Boom.EmailUniqueLinkButton folder in the layouts folder of the solution explorer and add a .js file. You can call it anyway you want, just remember the name. I call it Boom.EmailUniqueLinkButton.js. We will talk about the code in there later on. Go back to the Elements.xml file. Add another CustomAction element beneath the Elements node.

<CustomAction
  
Id=Ribbon.Documents.Share.EmailUniqueLink.Script
  
Location=ScriptLink
  
ScriptSrc =/_layouts/Boom.EmailUniqueLinkButton/Boom.EmailUniqueLinkButton.js
/>

Again, the Id marks the unique identifier for this action. In the Location attribute, we specify ‘ScriptLink’ to indicate that it concerns an external file. In the ScriptSrc attribute, we specify the location of our just created javascript file.

Your Elements.xml file should now look like this:

<?xml version=1.0“vencoding=utf-8?>
<Elements xmlns=http://schemas.microsoft.com/sharepoint/>
<CustomAction
  
Id=Ribbon.EmailUniqueLink
  
Location=CommandUI.Ribbon
  
RegistrationId=101
  
RegistrationType=List>
  
<CommandUIExtension>
     
<CommandUIDefinitions>
        
<CommandUIDefinition
           
Location=Ribbon.Documents.Share.Controls._children>
           
<Button
              
Id=Ribbon.Documents.Share.EmailUniqueLink
              
Command=Ribbon.Documents.Share.EmailUniqueLink
              
Sequence=15
              
Image16by16=/_layouts/$Resources:core,Language;/images/formatmap16x16.png
               Image16by16Top=-16
               Image16by16Left=-88
               
Image32by32=/_layouts/$Resources:core,Language;/images/formatmap32x32.png
               Image32by32Top=-128
               Image32by32Left=-448
              
Description=Sends the unique link to the document by e-mail
              
LabelText=E-mail Unique Link
               
ToolTipTitle=E-mail Unique Link
               
ToolTipDescription=Sends the unique link to the document by e-mail
              
TemplateAlias=o1/>
         
</CommandUIDefinition>
     
</CommandUIDefinitions>
     
<CommandUIHandlers>
        
<CommandUIHandler
           
Command=Ribbon.Documents.Share.EmailUniqueLink
           
CommandAction=javascript:EmailUniqueLink();
           
EnabledScript=javascript:EnableEmailUniqueLink();/>
      
</CommandUIHandlers>
   </CommandUIExtension>
</CustomAction>
<CustomAction
  
Id=Ribbon.Documents.Share.EmailUniqueLink.Script
  
Location=ScriptLink
  
ScriptSrc =/_layouts/Boom.EmailUniqueLinkButton/Boom.EmailUniqueLinkButton.js/>
</Elements>

We have now defined our button and performed the necessary wiring to make it respond to actions. Now let’s look at the code that will use the Client Object Model to get the unique document id link. Don’t worry, we are almost there 😉

Using the Client Object Model to get List Item properties

SharePoint 2010 adds the Client Object Model to our toolbox, which we can use through native code, Silverlight or javascript. (ECMAScript). Discussing the inner workings of the client object model is beyond the scope of this blog. We will only use the model to get the properties of the list item.

Many of the internet examples cover the Silverlight or native side of the client object model. I will use the ECMAScript method to get the properties of the list item and use it to generate the mail message.

So let us take a look at the javascript code. Open the added js file and include function bodies for the methods defined in the Elements.xml file, like below snippet.
function EmailUniqueLink() {
}

// Delegate that is called when server operation is complete upon success.
function onQuerySucceeded(sender, args) {
}

// Delegate that is called when server operation is completed with errors.
function onQueryFailed(sender, args) {
}

// Method to enable/disable the e-mail unique button on the ribbon.
function EnableEmailUniqueLink() {
}

// This method will contain most of the code needed to request the unique url to the document

The first method contains most of the code. The onQuerySucceeded and onQueryFailed methods are delegates that are needed because the client object model in javascript works asynchronously. The last method is needed to specify when the button is enabled.

When using the client object model, we always have to obtain the client context first. The client context contains the proxies that are used to connect to the server. Second thing we need to know is that the client object model works with batches. This means that we load the object we want returned in the context and then execute it first. Include the following code snippet in the EmailUniqueLink method.


// First get the context and web
var ctx = SP.ClientContext.get_current();
this.web = ctx.get_web();// Get the current selected list, then load the list using the getById method of Web (SPWeb)
var listId = SP.ListOperation.Selection.getSelectedList();
var sdlist = this.web.get_lists().getById(listId);

In this code snippet, we first obtain the client context and load the web. Then we use the SP.ListOperation.Selection object to get the current list. This will return the GUID of the list, which we then use to get the list reference through the web object. Please mark though that the web and list objects are not populated yet. We again use the Sp.ListOperation.Selection object to get the current selected item in the list, which will return an object containing an id property. Using that id, we can request the list item from the list. To have the list item populated and thus get our unique document id, we need to load it in the context and then execute the query. As said, this can only be done asynchronously. This is done by calling executeQueryAsync and pass the success and fail methods respectively. Another thing to note here is that we use this when we need to access the variable outside the method. Include the following snippet in the onQuerySucceeded method.


// Request url by using the get_item method. It will return the Url field type, which has a Url property.
var url = this.listItem.get_item('_dlc_DocIdUrl').get_url();
// Request the name of the document.
var title = this.listItem.get_item('FileLeafRef');
// Open a new e-mail in the default mail program.
window.open('mailto:?subject=Emailing%3A%20'+ title + '&body=' + url);


Once the asynchronous call is done, we can access the properties of the listitem by calling the get_item() method passing the name of the field you wish to return, in this case the ‘_dlc_DocIdUrl‘ that contains the unique link. Because that returns a SPFieldUrl type like object, we call get_url() to get the actual url. To complete the mail message, we also request the name of the file and then call the mailto: to open the message in an e-mail client.
In this case, we requested the entire listitem object from the server. We can also specify which properties to return so we can limit bandwidth use and increase performance. The way to do this is by adding the desired properties as parameters in the load method of the context, in our case _dlc_DocIdUrl and FileLeafRef, like below snippet.


// Only request name and DocIdUrl

ctx.load(this.listItem, '_dlc_DocIdUrl', 'FileLeafRef');


I prefer to load the entire object during debugging, so I can inspect the entire object (using developer tools >IE7).

// Get the currently selected item of the list. This will return a dicustonary with an id field
var items = SP.ListOperation.Selection.getSelectedItems(ctx);
var mijnid = items[0];

// Request the list item from the server using the getItemById method. This will load all properties.
// If needed, one could pre-request the fields to be loaded to preserve bandwidth.
this.listItem = sdlist.getItemById(mijnid.id);
// load the item in the context for batch operation.
ctx.load(this.listItem);

//Execute the actual script on the server side. Specify delegates to handle the response.
ctx.executeQueryAsync(Function.createDelegate(this, this.onQuerySucceeded), Function.createDelegate(this, this.onQueryFailed));

The failed body is not of much interest, although you could add code there to shown that it went wrong and why, as it gets the arguments containing the error passed. Add the following snippet to the failed method.


alert('failed ' + args.toString());


That is all that is needed to create the e-mail and send the unique url. Last method only enables the button when there is actually an item selected. Add the following snippet to the EnableEmailUniqueLink method:


// request number of selected items.
var items = SP.ListOperation.Selection.getSelectedItems();
var count = CountDictionary(items);
// only return true is a single item is selected.
return (count == 1);


Above is quite self-explanatory I guess. I get the number of selected items and only return true if the number of selected items equals 1. The entire javascript file should now resemble the following:


/*
====================================================================================================================
File: Boom.EmailUniqueLinkButton.js
Description: Contains supporting javascript functions to allow the unique link of a document to be sent by e-mail
Date:19-04-2010
Author: Patrick Boom
====================================================================================================================
*/

// This method will contain most of the code needed to request the unique url to the document
function EmailUniqueLink() {
   // First get the context and web
   var ctx = SP.ClientContext.get_current();
   this.web = ctx.get_web();
   // Get the current selected list, then load the list using the getById method of Web (SPWeb)
   var listId = SP.ListOperation.Selection.getSelectedList();
   var sdlist = this.web.get_lists().getById(listId);
   // Get the currently selected item of the list. This will return a dicustonary with an id field
   var items = SP.ListOperation.Selection.getSelectedItems(ctx);
   var mijnid = items[0];
   // Request the list item from the server using the getItemById method. This will load all properties.  
   // If needed, one could pre-request the fields to be loaded to preserve bandwidth.
   this.listItem = sdlist.getItemById(mijnid.id);
   // load the item in the context for batch operation.
   ctx.load(this.listItem);
   //Execute the actual script on the server side. Specify delegates to handle the response.
   ctx.executeQueryAsync(Function.createDelegate(this, this.onQuerySucceeded), Function.createDelegate(this, this.onQueryFailed));
}

// Delegate that is called when server operation is complete upon success.
function onQuerySucceeded(sender, args) {
   // Request url by using the get_item method. It will return the Url field type, which has a Url property.
   var url = this.listItem.get_item('_dlc_DocIdUrl').get_url();

   // Request the name of the document.

   var title = this.listItem.get_item('FileLeafRef');

   // Open a new e-mail in the default mail program.
   window.open('mailto:?subject=Emailing%3A%20' + title + '&body=' + url);
}

// Delegate that is called when server operation is completed with errors.
function onQueryFailed(sender, args) {
   alert('failed ' + args.toString());
}

// Method to enable/disable the e-mail unique button on the ribbon.

function EnableEmailUniqueLink() {
   // request number of selected items.
   var items = SP.ListOperation.Selection.getSelectedItems();
   var count = CountDictionary(items);

   // only return true is a single item is selected.

   return (count == 1);
}

Only thing left to do is compile and deploy the code. If you followed all steps correctly you should see a button next to the E-mail a Link button and when you click it (and you have the Document ID feature enabled in the Site features), an e-mail should open with the unique link in the body, like below image:

That’s it, watch the fruit of all you hard labor. In this post, we covered quite some material, like customizing the ribbon and using the client object model to examine the selected list item. Hope you like it! You can download the entire VS solution from here. Leave a comment if you have a couple of minutes. Much appreciated!

See ya…

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

 
%d bloggers like this: