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!!