Tuesday, July 29, 2014

Adding a "FAKE" Edit Control Block to Business Data List Web Part

A Fake Edit Control Block (ECB)

Yes, it's fake! But works just the same!

I needed to utilize BCS in SharePoint 2010 for a recent project that consisted of several tables and a few stored procedures that tied everything together to create a single "Advertisement" object. I use object as a different word for entity to avoid confusion! Basically an advertisement in this case has several fields that are grouped into related sets and each set is a table in the database. But this is just the backend and not as important as the SharePoint side of things.

In the case of the Business Data webparts, there is a particular web part that has some nice features and that is the Business Data List (BDL). It offers connect-ability to other Business Data webparts and has the ability to allow users to search for data based on filters. This was the main feature that we needed for this system. That was why it was chosen over a standard list view (xsltlistview). Yes, a standard list view does allow you to create views that you can filter, but the goal was to not have a lot of users creating a lot of different views on the list. The (BDL) allows both searching the database with custom filters that in our case were created in the stored procedures and wired up in the external content type, and filtering those results further just like a regular list view.

However, there is one feature that the (BDL) does not provide as easily as the listview, and that is a true Edit Control Block (ECB). Later, I will show my solution, but for full disclosure, the webpart does have a similar feature to an (ECB) and that feature is what was "hijacked" to make this work. Pictures should make this easier to see!!

So there you see that you can just add an action! So then you will see a screen with the following:


So you can set the Action Name and these properties. These settings for this demo are not really important because I needed something this does not provide. Here, you can only set a url and though you can pass a parameter such as ID, you can not really set a good popup dialog because it treats it like a new action no matter if it is the same page or set to new window. It's okay because we are going to make this do what we want!





So once you save this, you will then be able to move on to editing the (BDL) webpart to make it do the cool stuff!











So, if you have not added this webpart, you can add it to a page. Once you are in edit mode, you can edit the web part and select the external content type. The real neat thing here is that you can edit the view as show to the left. This will allow you to select the fields just like a regular list view.









So, notice that I have set in this case the "RequirementName" as the first field, and more importantly, that I set it as the "Title" field. This is denoted by the red circle. This is the piece that brings the coolness back because now, it will use this field to attach the custom action we defined earlier!






And there it is, you see that if you hover directly over the item, it will draw an icon similar to a listview. It is not exactly the same, but it functions the same way in that it will now display a dropdown when clicked. When I reached this point, I knew I just had to have something to work with so I started following this rabbit down the hole to get to what I needed. The dropdown is shown next.





So, it does indeed draw a dropdown with our defined custom action. If you stopped here, it would do exactly what was defined in the action, and that may be enough. However, it is just not quite evil enough for me!

So I dug into the dev tools in IE and Chrome to see how this box gets drawn. It took me a few minutes to notice that the first piece is actually done in the XSL for the webpart! And, this webpart gives us the ability to change it! That was the first change I needed to make. It is actually not too much to change here. Also, for FYI purposes, some of my screenshots are from different sites so there may be slight differences in field names that you see!

So here is the first section of the XSL. Basically I added a new parameter (curid) for the ID field. For me its just easier to do it this way. I added this to 2 places in the code as pointed out below:



<xsl:template name="dvt_1.rowview">
    <xsl:param name="curid" select="@ID" />
    <tr>
      <td class="ms-vb" width="1">
        <xsl:choose>
          <xsl:when test="$dvt_1_automode = '1'">
            <xsl:call-template name="dvt_1.automode">
              <xsl:with-param name="KeyField"></xsl:with-param>
              <xsl:with-param name="KeyValue" select="@*[name()=$ColumnKey]" />
              <xsl:with-param name="Mode">select</xsl:with-param>
            </xsl:call-template>
          </xsl:when>
          <xsl:otherwise>
            <span ddwrt:amkeyfield="" ddwrt:amkeyvalue="''" ddwrt:ammode="select" />
          </xsl:otherwise>
        </xsl:choose>
      </td>
      <td class="ms-vb">
        <xsl:attribute name="style">
          <xsl:choose>
            <xsl:when test="$dvt_1_form_selectkey = @*[name()=$ColumnKey]">color:blue</xsl:when>
            <xsl:otherwise />
          </xsl:choose>
        </xsl:attribute>
        <xsl:variable name="output">
          <xsl:variable name="rowId" select="@*[name()=$ColumnKey]" />
          <xsl:choose>
            <xsl:when test="$IsMenuVisible">
              <div class="ms-vb-title">
                <table height="100%" cellspacing="0" class="ms-unselectedtitle" onmouseover="MMU_EcbTableMouseOverOut(this, true)" hoverActive="ms-selectedtitle" hoverInactive="ms-unselectedtitle" oncontextmenu="this.click(); return false;" menuformat="ArrowOnHover">
                  <xsl:attribute name="downArrowTitle">
                    <xsl:value-of select="$OpenMenuToolTip" />
                  </xsl:attribute>
                  <xsl:attribute name="id">
                    <xsl:value-of select="$rowId" />
                    <xsl:text>t</xsl:text>
                  </xsl:attribute>
                  <xsl:attribute name="onclick">
                    <xsl:call-template name="OpenActionsMenu">
                      <xsl:with-param name="method">showActionMenu</xsl:with-param>
                      <xsl:with-param name="id" select="$rowId" />
                      <xsl:with-param name="curid" select="$curid" />
                      <xsl:with-param name="menuText">
                        <xsl:variable name="fieldValue">
                          <xsl:call-template name="LFtoBR">
                            <xsl:with-param name="input">
                              <xsl:value-of select="@RequirementName" />
                            </xsl:with-param>
                          </xsl:call-template>
                        </xsl:variable>
                        <xsl:copy-of select="$fieldValue" />
                      </xsl:with-param>
                    </xsl:call-template>
                  </xsl:attribute>
                  <tr>
                    <td class="ms-vb">
                      <a onclick="event.cancelBubble=true">
                        <xsl:attribute name="onkeydown">
                          <xsl:call-template name="OpenActionsMenu">
                            <xsl:with-param name="method">actionMenuOnKeyDown</xsl:with-param>
                            <xsl:with-param name="id" select="$rowId" />
                            <xsl:with-param name="curid" select="$curid" />
                            <xsl:with-param name="menuText">
                              <xsl:variable name="fieldValue">
                                <xsl:call-template name="LFtoBR">
                                  <xsl:with-param name="input">
                                    <xsl:value-of select="@RequirementName" />
                                  </xsl:with-param>
                                </xsl:call-template>
                              </xsl:variable>
                              <xsl:copy-of select="$fieldValue" />
                            </xsl:with-param>
                          </xsl:call-template>
                        </xsl:attribute>
                        <xsl:variable name="fieldValue">
                          <xsl:call-template name="LFtoBR">
                            <xsl:with-param name="input">
                              <xsl:value-of select="@RequirementName" />
                            </xsl:with-param>
                          </xsl:call-template>
                        </xsl:variable>
                        <xsl:copy-of select="$fieldValue" />
                        <img src="/_layouts/images/blank.gif" border="0" alt="" />
                      </a>
                    </td>
                    <td class="ms-menuimagecell" style="visibility:hidden">
                      <xsl:attribute name="id">
                        <xsl:value-of select="$rowId" />
                        <xsl:text>ti</xsl:text>
                      </xsl:attribute>
                      <img src="/_layouts/images/downarrw.gif" width="13">
                        <xsl:attribute name="alt">
                          <xsl:value-of select="$OpenMenuToolTip" />
                        </xsl:attribute>
                        <xsl:attribute name="title">
                          <xsl:value-of select="$OpenMenuToolTip" />
                        </xsl:attribute>
                      </img>
                    </td>
                  </tr>
                </table>
              </div>
              <span />
            </xsl:when>
            <xsl:otherwise>
              <xsl:variable name="fieldValue">
                <xsl:call-template name="LFtoBR">
                  <xsl:with-param name="input">
                    <xsl:value-of select="@RequirementName" />
                  </xsl:with-param>
                </xsl:call-template>
              </xsl:variable>
              <xsl:copy-of select="$fieldValue" />
            </xsl:otherwise>
          </xsl:choose>
        </xsl:variable>
        <xsl:copy-of select="$output" />
      </td>

So there is the first part! Basically I added the xsl-with-param for the curid to both calls to the "OpenActionsMenu" template.

This causes the system to pass the ID and not the BCDIdentity to the function because the ID field is what the stored procedures use and it is the primary key in the database. So then I needed to figure out what was next. This next piece of XSL is the updated "OpenActionsMenu" template.



<xsl:template name="OpenActionsMenu">
    <xsl:param name="method" />
    <xsl:param name="id" />
    <xsl:param name="curid" />
    <xsl:param name="menuText" />
    <xsl:value-of select="$method" />
    <xsl:text>('</xsl:text>
    <xsl:value-of select="$jsMenuLoadingMessage" />
    <xsl:text>','</xsl:text>
    <xsl:value-of select="ddwrt:EcmaScriptEncode($menuText)" />
    <xsl:text>',false,'</xsl:text>
    <xsl:value-of select="$jsMenuApplication" />
    <xsl:text>','</xsl:text>
    <xsl:value-of select="$jsMenuEntityNamespace" />
    <xsl:text>','</xsl:text>
    <xsl:value-of select="$jsMenuEntityName" />
    <xsl:text>','</xsl:text>
    <xsl:value-of select="ddwrt:EcmaScriptEncode($curid)" />
    <xsl:text>', event);</xsl:text>
  </xsl:template>

So this is the last part of the XSL. Notice that I used the $curid parameter instead of id. As stated, this is necessary for this to work. So now that the XSL is updated, I had to then find the next step.

In a normal list view, SharePoint had a few ways to add the ECB or ListItemMenu to the view. There was also a way to override or add to it using SharePoint Designer or adding a javascript function. You can see this in this POST.

However, in this webpart, this functionality no longer works. This webpart does not use those core javascript functions the same way. What was I going to do? Did I run away screaming!? Of course not! I knew that there was something being used to do this so I just had to find it. And luckily for me, there was indeed some javascript being used.

This webpart uses a file called wssactionmenu.js from the "HIVE" to draw the action menu dropdown. The basic functions of the file are to use an ajax like request to poll the external content type to find out what actions are setup and then draw these actions on the page inside the dropdown box. And due to how the file gets loaded, all we have to do, is override the functions in a different file and add this to our page!

So bring on a content editor webpart (CEWP)!! I did this in 2 stages to make it easier for me and to separate some functions. First, I created a new javascript file called CustomActionMenu.js

It looks like this:


  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
var tableIds= new Array();
var isplanner = false;

$(document).ready(function () {
    $().SPServices({
        operation: "GetGroupCollectionFromUser",
        userLoginName: $().SPServices.SPGetCurrentUser(),
        async: false,
        completefunc: function (xData, Status) {
            if ($(xData.responseXML).find("Group[Name='PLANNERS']").length == 1) {
                isplanner = true;
            }
            else {
                isplanner = false;
            }
        }
    });
});

function OpenMenu(menuId,aId)
{
    MMU_Open(document.getElementById(menuId),document.getElementById(aId),null,false,null);
}

function showActionMenu(loadingMsg,menuText,showMenuImage,appName, entityNamespace, entityName, itemID, event)
{
    displayActionMenu(loadingMsg,menuText,showMenuImage, null, null, null, null, null, appName, entityNamespace, entityName, itemID, event);
}

function displayActionMenu(loadingMsg, menuText, showMenuImage, parentAppName, parentEntityNamespace, parentEntityName, parentSpecificFinderName, parentAssociationName, appName, entityNamespace, entityName, itemID, event)
{
    if (!event) event = window.event;
    var prefix = 'prefix_' + itemID;
    var zid = itemID;
    var tableId = prefix + '_t';
    tableIds[tableIds.length] = tableId;
    var menuId = 'menu_' + prefix;
    var aId = prefix;
    var imageId = prefix + '_ti';
    var menuItemId = 'menuitem_' + prefix;
    var placeHolderButtonHtml = '';
    var effectiveMenuText = '';
    if (menuText != null) {
        effectiveMenuText = menuText;
    }
    if ((menuText != null) && (menuText != '')) {
        placeHolderButtonHtml =
            '<table height="100%" cellspacing="0" class="ms-unselectedtitle" id="' + tableId + '" ' +
            'foa="' + aId + '" onmouseover="MMU_EcbTableMouseOverOut(this, true)" hoverActive="ms-selectedtitle" ' +
            'hoverInactive="ms-unselectedtitle" oncontextmenu="this.click(); return false;" ' +
            'onclick="try { MMU_Open(byid(\'' + menuId + '\'), MMU_GetMenuFromClientId(\'' + aId + '\'),event,false, null, 0); } catch (ex) { alert(\'An unhandled exception occurred: \' + ex); }" ' +
            'menuformat="ArrowOnHover" downArrowTitle="Open Menu">' +
            '<tr><td class="ms-vb"><a id="' + aId + '" onclick="event.cancelBubble=true" onfocus="MMU_EcbLinkOnFocusBlur(byid(\'' + menuId + '\'),this,true);" ' +
            'onkeydown="return MMU_EcbLinkOnKeyDown(byid(\'' + menuId + '\'),this,event);" ' +
            'tabindex="0" menuTokenValues="MENUCLIENTID=' + aId + ',TEMPLATECLIENTID=' + menuId + '"> ';
        if (showMenuImage) {
            placeHolderButtonHtml = placeHolderButtonHtml + '<img border="0" align="absmiddle" src="/_layouts/images/bizdataactionicon.gif" alt=""> ';
        }
        placeHolderButtonHtml = placeHolderButtonHtml + effectiveMenuText +
            '<img src="/_layouts/images/blank.gif" border="0" alt=""/></a></td> ' +
            '<td class="ms-menuimagecell" style="visibility:hidden" id="' + imageId + '"> ' +
            '<img src="/_layouts/images/downarrw.gif" width="13" alt=""/></td></tr></table> ';
    }
    else {
        placeHolderButtonHtml =
            '<span id="' + tableId +
            '" class="ms-SPLink ms-HoverCellInactive" onmouseover="this.className=\'ms-SPLink ms-HoverCellActive\'" ' +
            'onmouseout="this.className=\'ms-SPLink ms-HoverCellInactive\'" ' + 
            'onclick=" MMU_Open(document.getElementById(\'' + menuId + '\'), document.getElementById(\'' + aId + '\'))">' + 
            '<a id="' + aId + '" accesskey="" style="cursor:hand;white-space:nowrap;" ' +
            'onkeydown="MMU_EcbLinkOnKeyDown(document.getElementById(\'' + menuId + '\'), this);" ' + 
            'onclick=" MMU_Open(document.getElementById(\'' + menuId + '\'), this, event);" tabindex="0" ' + 
            'menuTokenValues="TEMPLATECLIENTID=' + menuId + ',MENUCLIENTID=' + tableId + '"> ';
        if (showMenuImage) {
            placeHolderButtonHtml = placeHolderButtonHtml + '<img border="0" align="absmiddle" src="/_layouts/images/bizdataactionicon.gif" alt="">';
        }
        placeHolderButtonHtml = placeHolderButtonHtml + effectiveMenuText +
            '</a><img src="/_layouts/images/menudark.gif" alt="" /></span>';
    }
    var placeHolderMenuHtml = '<span style="display: none"><menu class=ms-SrvMenuUI id="' + menuId + '">';
    placeHolderMenuHtml += '<ie:menuitem type="option" iconsrc="/_layouts/images/mp216.gif" onmenuclick="DoAction(\'View\', ' + zid + ');" ' +
        'text="View Item" title="View" menugroupid="3000"></ie:menuitem>';
    if (isplanner == true) {
        placeHolderMenuHtml += '<ie:menuitem type="option" iconsrc="/_layouts/images/edit.gif" onmenuclick="DoAction(\'Edit\', ' + zid + ');" ' + 
            'text="Edit Item" title="Edit" menugroupid="3000"></ie:menuitem>' +
            '<ie:menuitem type="option" iconsrc="/_layouts/images/delete.gif" onmenuclick="DoAction(\'Delete\', ' + zid + ');" ' +
            'text="Delete Item" title="Edit" menugroupid="3000"></ie:menuitem>';
    }
    placeHolderMenuHtml += '</menu></span>';
    var targetEl = event.srcElement ? event.srcElement : event.target;
    var buttonDiv = GetParentNode(targetEl, 'div');
    var menuDiv = buttonDiv.nextSibling;
    buttonDiv.innerHTML = placeHolderButtonHtml;
    menuDiv.innerHTML = placeHolderMenuHtml;
    OpenMenu(menuId, aId);
}

function GetParentNode(el,tag)
{
    var parent = (el.parentNode)?el.parentNode:el.parentElement;
    while (parent != null) {
     if (parent.tagName.toLowerCase() == tag) { return parent; }
     parent = (parent.parentNode)?parent.parentNode:parent.parentElement;
    }
    return null;
}

function actionMenuOnKeyDown(loadingMsg,menuText,showMenuImage,appName, entityNamespace, entityName, itemID, event)
{
    if (!event) event = window.event;
    if ((event.shiftKey && (GetKeyCode(event)==13)) || ((event.altKey) && (GetKeyCode(event)==40)))
    {
        displayActionMenu(loadingMsg,menuText,showMenuImage, null, null, null, null, null, appName, entityNamespace, entityName, itemID, event);
    }   
}

function GetKeyCode(e)
{
    return e.keyCode?e.keyCode:e.which;
}

So that is the magic! The first thing that I do in this case is use a function from the AWESOME SPServices library found here. The "GetGroupCollectionFromUser" function will test if the current user is a member of the passed in group. If so, the "isplanner" variable is set to true. This is used to display the "Edit" and "Delete" options in the dropdown.

If you go back and look at the XSL, you will see that we set this up to call a function called "showActionMenu". I took this code from the wssactionmenu.js file and modified it to do what I wanted. So this function just in turn calls the "displayActionMenu" and passes in the variables from the other function. The key variable for us here is the itemID variable as this is what we passed from the modified XSL code as the id of the item.

The function builds the HTML of the button (arrow) and dropdown menu. Here we are using the ie:menuitem structure that is used on a lot of other menus. It is a simple structure that allows us to pass in an image and the text for the item along with a link or javascript function to do when clicked. In this case, I am using a javascript function. This function is on the HTML file of the next piece of how I did the CEWP. You can see it below.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<style type="text/css">
    .ms-menutoolbar { display: none; }
</style>
<script type="text/javascript" src="../AdvertisementAssets/js/jquery.SPServices-2014.01.min.js"></script>
<script type="text/javascript" src="../AdvertisementAssets/js/CustomActionMenu.js"></script>
<script type="text/javascript">
    var test;

    $(document).ready(function () {
        test = new String(window.location);
        if (test.indexOf("showall") > 0) {
            $("input[id*='BdwpFilterValue0']").val("*");
            $("a[id*='_BdwpFilterFindLink']").click();
        }
    });

    function DoAction(action, id) {
        switch (action) {
            case "View":
                CNRFCDialog('ViewAdvertisement.aspx?ItemID=' + id, 'View Advertisement', '650', '750', 'RefreshCallback');
                break;

            case "Edit":
                CNRFCDialog('EditAdvertisement.aspx?ItemID=' + id, 'Edit Advertisement', '650', '750', 'RefreshCallback');
                break;

            case "Delete":
                if (window.confirm("Are you sure you want to delete the Advertisement?")) {
                    waitDialog = SP.UI.ModalDialog.showWaitScreenWithNoClose('Updating Data...', 'Please wait while the Advertisement is Deleted...', 76, 400);
                    ctx = new SP.ClientContext.get_current();
                    list = ctx.get_web().get_lists().getByTitle("BCSRequirements");
                    listItem = list.getItemById(id);
                    listItem.deleteObject();
                    ctx.executeQueryAsync(DeleteItemSucceeded, DeleteItemFailed);
                }
                break;
        }
    }

    function DeleteItemSucceeded(sender, args) {
        waitDialog.get_html().getElementsByTagName('TD')[1].innerHTML = 'Advertisement Deleted...';
        setTimeout(function () {
            waitDialog.close();
            $("a[id*='_BdwpFilterFindLink']").click();
        }, 1000);
    }

    function DeleteItemFailed(sender, args) {
        var errorstring = "<h3>There was an error Deleting the advertisement.</h3><br/><ul>";
        errorstring += "<li>Please try again.</li><li>If the problem persists, please notify the help desk.</li></ul>";
        $("#errorcontainer").html(errorstring).show();
        if (typeof console != "undefined") {
            console.log(args.get_message());
        }
    }
</script>
<div id="errorcontainer" style="display:none;"></div>

This is the file that loads the CustomActionMenu.js file and reacts to the menu items when clicked. Basically the function will just open a dialog to the View or Edit forms that I have customized, or if the user selects Delete, it will use the client object model to delete the item. There is a neat little jQuery snippet here on line 44 that after the item is deleted, will basically click the filter link to refresh the items searched showing the user that the item was deleted. 


And of course, this is what the dropdown looks like:


So there it is! A user friendly "FAKE" Edit Control Block on a Business Data List webpart. It is security trimmed to a point and can be customized to add any other functionality that you would want.

Until Next Time!!

Thursday, January 9, 2014

Creating Ribbon Tabs and Buttons in Visual Studio Part 1


A few weeks ago I published a post on this using pure javascript code directly from a page. That code was in the site assets library and added to a Content Editor Web Part (CEWP). You can check that post out here.

The main goal of that post was to prove that it is possible to create a custom tab in javascript. Even though the base method for doing this uses an OOB SharePoint javascript function, it does not use the more “approved” method for creating custom ribbon components. It does allow you to create content in the ribbon area using jQuery/javascript that you would not otherwise be able to do using the “approved” methods. That will be touched on more in a future post possibly part 2 of that initial one.

As stated, there is an “approved” (or maybe “supported” is a better term for it) method for creating custom ribbon components. The ribbon is mostly “drawn” from declarative XML referred to as Ribbon XML and there is a lot of documentation that covers it on MSDN. This declarative approach makes it easy to create fairly complex tabs and controls. Much of the OOB ribbon is defined in the Program Files\Common Files\microsoft shared\Web Server Extensions\14\TEMPLATE\GLOBAL\XML\CMDUI.XML file. I have referenced this file to create several different ribbon components. (Note that for SharePoint 2013 this would be in the 15 folder)


If you take a look at the ribbon, you can see that there are several types of controls that you can create. There are 2 main tab types. A regular tab and what is called a contextual tab. A contextual tab is a tab that is only available when an object on a page is selected. This tab contains sub tabs with groups of buttons representing commands for customizing/formatting the selected object. An example from PowerPoint is below:





As you can see, this allows us to create some very nice controls based on what we want to do. So lets create something so that we can see this stuff in action.


We are going to create a new solution in Visual Studio. I am using Visual Studio 2012. For my example I also used some OOB images from the layouts/images folder. I copied the MENUOUTL.GIF file and used Adobe After Effects to change the color just to make a different icon for contrast. You can use any icon you like of course!


So start Visual Studio and create a new Project as shown below:

VSNewProject1

Click OK and then select the type of solution. This brings up the next screen where I chose a farm solution.

VSNewProject2

Click Finish to get into the solution.

Once in the solution, you will need to create an Empty Element. Right click on the C# project as highlighted below:

VSNewElement1

This will bring up a selection menu where you will select “Add” and then “New Item” as shown below:

VSNewElement2

Which will then bring up the next screen:

VSNewElement3

Give it a good name and as you can see, I named it "E3D_Email_Tab2". This will create an elements.xml file. This is where the Ribbon XML code is declared.

VSElementsFile1

Of course this is an empty file and we need something to add here! Copy in the below code and I will go over it.

1 <?xml version="1.0" encoding="utf-8"?> 2 <Elements xmlns="http://schemas.microsoft.com/sharepoint/"> 3 <CustomAction 4 RegistrationType="ContentType" RegistrationId="0x01" 5 Id="E3D.Utilities.Mail2" 6 Sequence="1010" 7 Location="CommandUI.Ribbon"> 8 <CommandUIExtension> 9 <CommandUIDefinitions> 10 <CommandUIDefinition 11 Location="Ribbon.Tabs._children"> 12 <Tab 13 Id="E3D.Utilities.Tab2" 14 Title="E3D Utilities 2" 15 Description="This tab holds custom commands for EMail and other operations." 16 Sequence="2500"> 17 <Scaling 18 Id="E3D.Utilities.Tab.Scaling"> 19 <MaxSize 20 Id="E3D.Utilities.Tab.One.Scaling.MaxSize" 21 GroupId="E3D.Utilities.Tab.GroupOne" 22 Size="TwoLarge"/> 23 <Scale 24 Id="E3D.Utilities.Tab.One.Scaling.Scale" 25 GroupId="E3D.Utilities.Tab.GroupOne" 26 Size="TwoLarge" /> 27 </Scaling> 28 <Groups Id="E3D.Utilities.Tab.Groups"> 29 <Group 30 Id="E3D.Utilities.Tab.GroupOne" 31 Description="" 32 Title="E-Mail" 33 Sequence="10" 34 Template="E3D.Utilities.RibbonTemplate.Outlook"> 35 <Controls Id="E3D.Utilities.Tab.GroupOne.Controls"> 36 <SplitButton 37 Id="E3D.Utilities.Tab.GroupOne.EmailAllUsers" 38 Command="E3D.Utilities.EmailAllCC" 39 Sequence="10" 40 ToolTipTitle="Email All Options." 41 ToolTipDescription="Email All Options." 42 Image16by16="/_layouts/images/outl.gif" 43 Image32by32="~sitecollection/_layouts/Images/menuoutl.gif" 44 LabelText="Email All:" 45 PopulateDynamically="false" 46 PopulateOnlyOnce="true" 47 TemplateAlias="customOne"> 48 <Menu Id="E3D.Utilities.Tab.GroupOne.AllEmailDropDown.Menu"> 49 <MenuSection Id="E3D.Utilities.Tab.GroupOne.AllEmailDropDown.Menu.MenuSectionOne"> 50 <Controls Id="E3D.Utilities.Tab.GroupOne.AllEmailDropDown.Menu.Controls"> 51 <Button 52 Id="E3D.Utilities.Tab.GroupOne.AllEmailDropDown.Menu.CC" 53 Command="E3D.Utilities.EmailAllCC2" 54 LabelText="Email All as CC" /> 55 <Button 56 Id="E3D.Utilities.Tab.GroupOne.AllEmailDropDown.Menu.BCC" 57 Command="E3D.Utilities.EmailAllBCC2" 58 LabelText="Email All as BCC"/> 59 </Controls> 60 </MenuSection> 61 </Menu> 62 </SplitButton> 63 <SplitButton 64 Id="E3D.Utilities.Tab.GroupOne.EmailSelectedUsers" 65 Command="E3D.Utilities.EmailSelectedCC" 66 Sequence="20" 67 ToolTipTitle="Email Selected Options." 68 ToolTipDescription="Email Selected Options." 69 Image16by16="/_layouts/images/outl.gif" 70 Image32by32="~sitecollection/_layouts/Images/menuoutl2.jpg" 71 LabelText="Email Selected:" 72 PopulateDynamically="false" 73 PopulateOnlyOnce="true" 74 TemplateAlias="customTwo"> 75 <Menu Id="E3D.Utilities.Tab.GroupOne.SelectedEmailDropDown.Menu"> 76 <MenuSection Id="E3D.Utilities.Tab.GroupOne.SelectedEmailDropDown.Menu.MenuSectionOne"> 77 <Controls Id="E3D.Utilities.Tab.GroupOne.SelectedEmailDropDown.Menu.Controls"> 78 <Button 79 Id="E3D.Utilities.Tab.GroupOne.SelectedEmailDropDown.Menu.CC" 80 Command="E3D.Utilities.EmailSelectedCC2" 81 LabelText="Email Selected as CC" /> 82 <Button 83 Id="E3D.Utilities.Tab.GroupOne.SelectedEmailDropDown.Menu.BCC" 84 Command="E3D.Utilities.EmailSelectedBCC2" 85 LabelText="Email Selected as BCC"/> 86 </Controls> 87 </MenuSection> 88 </Menu> 89 </SplitButton> 90 </Controls> 91 </Group> 92 </Groups> 93 </Tab> 94 </CommandUIDefinition> 95 <CommandUIDefinition Location="Ribbon.Templates._children"> 96 <GroupTemplate Id="E3D.Utilities.RibbonTemplate.Outlook"> 97 <Layout 98 Title="TwoLarge" 99 LayoutTitle="TwoLarge"> 100 <Section Alignment="Top" Type="OneRow"> 101 <Row> 102 <ControlRef DisplayMode="Large" TemplateAlias="customOne" /> 103 <ControlRef DisplayMode="Large" TemplateAlias="customTwo" /> 104 </Row> 105 </Section> 106 </Layout> 107 </GroupTemplate> 108 </CommandUIDefinition> 109 </CommandUIDefinitions> 110 <CommandUIHandlers> 111 <CommandUIHandler 112 Command="E3D.Utilities.EmailAllCC2" 113 CommandAction="javascript: var notifythem = ''; this.notifythem = SP.UI.Notify.addNotification('This would email everyone!', false);" /> 114 <CommandUIHandler 115 Command="E3D.Utilities.EmailSelectedCC2" 116 CommandAction="javascript: var notifythem = ''; this.notifythem = SP.UI.Notify.addNotification('This would email only a select few!', false);" /> 117 <CommandUIHandler 118 Command="E3D.Utilities.EmailAllBCC2" 119 CommandAction="javascript: var notifythem = ''; this.notifythem = SP.UI.Notify.addNotification('This would blindly do some emailing!', false);" /> 120 <CommandUIHandler 121 Command="E3D.Utilities.EmailSelectedBCC2" 122 CommandAction="javascript: var notifythem = ''; this.notifythem = SP.UI.Notify.addNotification('So would this one!', false);" /> 123 </CommandUIHandlers> 124 </CommandUIExtension> 125 </CustomAction> 126 </Elements> 127

As you can see, that is a lot of XML, but not too much!

The first thing that is defined is that this is a CustomAction. We give this some attributes as follows:

  • RegistrationType: This defines what type of item we are registering the custom action to. (Ex. Content Type)
  • RegistrationId: This defines the id of the item. (Ex. 0x01 = default item content type id)
  • Id: This is a unique identifier for the custom action
  • Sequence: Defines the order
  • Location: Defines where the custom action is placed. These can be found here for 2010 and here for 2013.

Then we move to the real meat of the XML by defining a CommandUIExtension with some CommandUIDefinitions. A CommandUIDefinition has the Location parameter as well denoting where it will be placed.

Tab: For this I defined a Tab item with the attributes as shown in the above XML. Again a unique id is given and also a Title that defines what is displayed in the tab as seen in the image shown below.

Scaling: The next thing I defined was the scaling. This is used to define what happens to your controls as the size of the ribbon changes. In my example, I chose to do nothing as there are only 2 controls. Again, there are several options here as defined in the RibbonXML documentation.

Groups: A place to add the groups for this single tab.

Group: This defines a single group with the attributes as shown.

Controls: This is where the real fun begins and you define the controls you want to use. In this example I used 2 SplitButton controls. This is simply a button with a dropdown menu.

Some nice attributes are defined in the controls and one of the nice ones is the Image32by32 and Image16by16. These define the location of the images that will be used by the controls. These attributes are available in many of the supported ribbon controls.

Another important attribute to include is the Command attribute of your control. This is the name or id of the command. This will then be used to define what to when that item is used.

There is also the TemplateAlias attribute. This is a somewhat unique identifier that is used later when defining the layout of the group.

And for that segue lets get to the next CommandUIDefiniton which is the Templates which is used to setup the group layouts, sections and rows as seen in the above XML. And notice the TemplateAlias names match.

After we have defined what controls we want and have decided where they go and what they look like, it is time to define what happens when the control is used. For this we define a CommandUIHandlers section with a CommandUIHandler for each command we want to handle. There are a few ways to do this. First, you define the Command you want to handle and then have the option of defining a CommandAction. In general this is some javascript code that does some action. If what you are doing is complex code, you may want to move the javascript to a different file. This file could be in the master page, or you could use a CommandAction to load javascript file. This will be explained in the next post.

So, lets look at what deploying this yields.

Remember I changed one of my icons for contrast as shown below.

Ribbon1

I already had another solution which is why there are 2 custom tabs, but as you can see, they are pretty cool. You can take this pretty far if you want to really create some nice stuff. My next post on this topic will include adding controls to existing ribbons and groups and maybe a few more types of controls. Stay tuned, and as always, let me know what you think and if there is something you would like me to post about.