Second Life of a Hungarian SharePoint Geek

January 9, 2013

Using IE as a local Host for SharePoint ECMAScript Client Object Model, the Office 365 version

In my recent post I wrote about how you can use the JavaScript Client Object Model (JSCOM) against a remote SharePoint 2010 server from a local HTML page. In the current post my goal is to demonstrate a similar technique, but in this case against the Office 365 Developer Preview, that is the cloud-based version of SharePoint 2013.

NOTE: Again, be aware that the methods described here don’t use the public, documented interfaces, so it is probably not supported by MS, and suggested to be used only as an experiment. It is what can be done, and not a best practice at all. There is no guarantee that it will work for you, especially after MS updates the version of Office 365. I publish this results as I believe one can learn a few tricks around the internals of JSCOM and communication between client and server.

In the previous post we authenticated our requests using the standard Windows-integrated authentication method. In the case of Office 365 that is rather different, as it requires claim-based authentication (see these articles on  CodeProject for an introduction on the theme, or this guide from Microsoft for a deep dive). You can find excellent examples for such authentication against Office 365 when using the Managed Client Object Model, including this code sample on MSDN. In the code from Sundararajan Narasiman and Wictor Wilén the Windows Identity Foundation (WIF) classes have been used. Doug Ware, another fellow MVP, published a similar solution, but without involving WIF into the game. You can even find an example for PHP, but for JavaScript I found no solution on the web.

The detailed process of the authentication is very well described on MSDN, so I don’t rehash all the steps here, just provide a quick overview to enable better understanding of the JavaScript code sample below.

1. We get the token from the security token service (STS) of MS Online.

2. "Login" to the actual O365 site using the token provided by STS in the former step. As a result of this step, we have the required cookies (FedAuth and rtFA) to be used automatically in the next steps. These cookies are set by Set-Cookie headers of the response and cached and reused by the browser for later requests targeting the same site.

3. Get the digest from the Sites web service and refresh the one stored in the local page.

4. Execute the JSCOM request (after setting the full URL)

As you can see, the last two steps are identical to the steps we performed in the case of on-premise SharePoint in the last post.

And here is the actual code to demonstrate the theory in practice. Don’t forget to set the URLs for JavaScript file references to match your site name, as well as the values of the JavaScript variables, like usr, pwd and siteFullUrl. BTW, it seems that one can access the SharePoint JavaScript files in the LAYOUTS folder without authentication, at least, I get no authentication prompt when I try to download one.

Code Snippet
  1. <script type="text/ecmascript" src="http://code.jquery.com/jquery-1.8.3.min.js"></script>
  2. <script type="text/ecmascript" src="https://yourdomain-my.sharepoint.com/_layouts/1033/init.js"></script>
  3. <script type="text/ecmascript" src="https://yourdomain-my.sharepoint.com/_layouts/MicrosoftAjax.js"></script>
  4. <script type="text/ecmascript" src="https://yourdomain-my.sharepoint.com/_layouts/CUI.js"></script>
  5. <script type="text/ecmascript" src="https://yourdomain-my.sharepoint.com/_layouts/1033/Core.js"></script>
  6. <script type="text/ecmascript" src="https://yourdomain-my.sharepoint.com/_layouts/SP.Core.js"></script>
  7. <script type="text/ecmascript" src="https://yourdomain-my.sharepoint.com/_layouts/SP.Runtime.js"></script>
  8. <script type="text/ecmascript" src="https://yourdomain-my.sharepoint.com/_layouts/SP.js"></script>
  9.  
  10. <input type="hidden" name="__REQUESTDIGEST" id="__REQUESTDIGEST" value="0x753C251974CACCF3B030F7FF1358D0E0229B6DE0B0D363A0272EDBF69FBE4225A2107BE0998E236C248D2116D0A47B0D1849248B558F420AB09BDE06CFCFDB56,07 Jan 2013 13:30:54 -0000" />
  11.  
  12. <script language="ecmascript" type="text/ecmascript">
  13.     var tokenReq = '<?xml version="1.0" encoding="utf-8"?>';
  14.     tokenReq += '<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance&quot; xmlns:xsd="http://www.w3.org/2001/XMLSchema&quot; xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">&#039;;
  15.     tokenReq += '  <soap:Body>';
  16.     tokenReq += '    <GetUpdatedFormDigestInformation xmlns="http://schemas.microsoft.com/sharepoint/soap/&quot; />';
  17.     tokenReq += '  </soap:Body>';
  18.     tokenReq += '</soap:Envelope>';
  19.  
  20.     // you should set these values according your actual request
  21.     var usr =  'username@yourdomain.onmicrosoft.com';
  22.     var pwd = 'password';
  23.     var siteFullUrl = "https://yourdomain-my.sharepoint.com&quot;;
  24.  
  25.     var loginUrl = siteFullUrl + "/_forms/default.aspx?wa=wsignin1.0";
  26.     var authReq =   '<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope&quot; xmlns:a="http://www.w3.org/2005/08/addressing&quot; xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">&#039;
  27.     authReq +=      '  <s:Header>'
  28.     authReq +=      '    <a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Action>&#039;
  29.     authReq +=      '    <a:ReplyTo>'
  30.     authReq +=      '      <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>&#039;
  31.     authReq +=      '    </a:ReplyTo>'
  32.     authReq +=      '    <a:To s:mustUnderstand="1">https://login.microsoftonline.com/extSTS.srf</a:To>&#039;
  33.     authReq +=      '    <o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">&#039;
  34.     authReq +=      '      <o:UsernameToken>'
  35.     authReq +=      '        <o:Username>' + usr + '</o:Username>'
  36.     authReq +=      '        <o:Password>' + pwd + '</o:Password>'
  37.     authReq +=      '      </o:UsernameToken>'
  38.     authReq +=      '    </o:Security>'
  39.     authReq +=      '  </s:Header>'
  40.     authReq +=      '  <s:Body>'
  41.     authReq +=      '    <t:RequestSecurityToken xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust"><wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">&#039;
  42.     authReq +=      '      <a:EndpointReference>'
  43.     authReq +=      '        <a:Address>' + loginUrl + '</a:Address>'
  44.     authReq +=      '      </a:EndpointReference>'
  45.     authReq +=      '      </wsp:AppliesTo>'
  46.     authReq +=      '      <t:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</t:KeyType>&#039;
  47.     authReq +=      '      <t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType>&#039;
  48.     authReq +=      '      <t:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</t:TokenType>'
  49.     authReq +=      '    </t:RequestSecurityToken>'
  50.     authReq +=      '  </s:Body>'
  51.     authReq +=      '</s:Envelope>';
  52.     
  53.     var lists;
  54.  
  55.     function startScript() {
  56.       getToken();
  57.     }
  58.  
  59.     // Step 1: we get the token from the STS
  60.     function getToken()
  61.     {
  62.         $.support.cors = true; // enable cross-domain query
  63.         $.ajax({
  64.             type: 'POST',
  65.             data: authReq,
  66.             crossDomain: true, // had no effect, see support.cors above
  67.             contentType: 'application/soap+xml; charset=utf-8',
  68.             url: 'https://login.microsoftonline.com/extSTS.srf&#039;,         
  69.             dataType: 'xml',
  70.             complete: function (result) {
  71.                 // extract the token from the response data
  72.                 // var token = $(result.responseXML).find("wsse\\:BinarySecurityToken").text(); // responseXML is undefined, we should work with responseText, because Content-Type: application/soap+xml; charset=utf-8
  73.                 var token = $(result.responseText).find("BinarySecurityToken").text();
  74.                 getFedAuthCookies(token);
  75.             },
  76.             error: function(XMLHttpRequest, textStatus, errorThrown) {
  77.                 alert(errorThrown);
  78.                 }
  79.         });
  80.     }
  81.  
  82.     // Step 2: "login" using the token provided by STS in step 1
  83.     function getFedAuthCookies(token)
  84.     {
  85.         $.support.cors = true; // enable cross-domain query
  86.         $.ajax({
  87.             type: 'POST',
  88.             data: token,
  89.             crossDomain: true, // had no effect, see support.cors above
  90.             contentType: 'application/x-www-form-urlencoded',
  91.             url: loginUrl,         
  92.          // dataType: 'html', // default is OK: Intelligent Guess (xml, json, script, or html)
  93.             complete: function (result) {  
  94.                 refreshDigest();
  95.             },
  96.             error: function(XMLHttpRequest, textStatus, errorThrown) {
  97.                 alert(errorThrown);
  98.             }
  99.         });
  100.     }
  101.  
  102.     // Step 3: get the digest from the Sites web service and refresh the one stored locally
  103.     function refreshDigest()
  104.     {
  105.         $.support.cors = true; // enable cross-domain query
  106.         $.ajax({
  107.                 type: 'POST',
  108.                 data: tokenReq,
  109.                 crossDomain: true, // had no effect, see support.cors above
  110.                 contentType: 'text/xml; charset="utf-8"',
  111.                 url: siteFullUrl + '/_vti_bin/sites.asmx',
  112.                 headers: {
  113.                     'SOAPAction': 'http://schemas.microsoft.com/sharepoint/soap/GetUpdatedFormDigestInformation&#039;,
  114.                     'X-RequestForceAuthentication': 'true'
  115.                 },
  116.                 dataType: 'xml',
  117.                 complete: function (result) {  
  118.                     $('#__REQUESTDIGEST').val($(result.responseXML).find("DigestValue").text());
  119.                     sendJSCOMReq();
  120.                 },
  121.                 error: function(XMLHttpRequest, textStatus, errorThrown) {
  122.                     alert(errorThrown);
  123.                 }
  124.         });
  125.     }
  126.  
  127.     // Step 4: execute the JSCOM request (after setting the full URL)
  128.     function sendJSCOMReq() {
  129.         try {
  130.  
  131.             var spPageContextInfo = {webServerRelativeUrl: "\u002fTestappforSharePoint", webAbsoluteUrl: "https:\u002f\u002fyour.sharepoint.com\u002fTestappforSharePoint", siteAbsoluteUrl: "https:\u002f\u002fyourdomain-f7079688f25f20.sharepoint.com", serverRequestPath: "\u002fTestappforSharePoint\u002fPages\u002fDefault.aspx", layoutsUrl: "_layouts\u002f15", webTitle: "Test app for SharePoint", webTemplate: "17", tenantAppVersion: "0", webLogoUrl: "\u002f_layouts\u002f15\u002fimages\u002fsiteIcon.png?rev=23", webLanguage: 1033, currentLanguage: 1033, currentUICultureName: "en-US", currentCultureName: "en-US", clientServerTimeDelta: new Date("2013-01-07T13:57:14.0337474Z") – new Date(), siteClientTag: "0$$15.0.4433.1011", crossDomainPhotosEnabled:true, webUIVersion:15, webPermMasks:{High:2147483647,Low:4294967295}, pagePersonalizationScope:1,userId:11, systemUserKey:"i:0h.f|membership|1003bffd844f8d57@live.com", alertsEnabled:true, siteServerRelativeUrl: "\u002f", allowSilverlightPrompt:'True'};
  132.  
  133.  
  134.             var siteRelativeUrl = "/";
  135.             var context = new SP.ClientContext(siteRelativeUrl);
  136.             context.$1P_0 = siteFullUrl;
  137.  
  138.             var web = context.get_web();
  139.             lists = web.get_lists();
  140.  
  141.             context.load(lists);
  142.             context.executeQueryAsync(Function.createDelegate(this, this.onQuerySucceeded), Function.createDelegate(this, this.onQueryFailed));  
  143.         } catch (err) {
  144.             var msg = "There was an error on this page.\n";
  145.             msg += "Error description: " + err.message + "\n";
  146.             alert(msg);
  147.         }
  148.     }
  149.  
  150.     // Step 5 (success): process response
  151.     function onQuerySucceeded(sender, args) {
  152.         var count = lists.get_count();
  153.         var listTitles = "Number of lists: " + count + ":\n";
  154.         for(var i=0;i<count; i++)
  155.         {
  156.             var list = lists.get_item(i);
  157.             listTitles += "  " + list.get_title() + "\n";
  158.         }
  159.         alert(listTitles);
  160.     }
  161.  
  162.     // Step 5 (failure): display error
  163.     function onQueryFailed(sender, args) {
  164.       alert("Request failed: "+ args.get_message());
  165.     }
  166.  
  167.     // start the custom script execution after the scripts and page are loaded
  168.     SP.SOD.executeOrDelayUntilScriptLoaded(function () {
  169.         $(document).ready(startScript);
  170.     }, "sp.js");
  171.  
  172. </script>

You might be wondering, how it is possible to come up with a solution like that. As a first step, I downloaded a few of the managed client object model / WIF samples mentioned above (thank you guys for sharing, you all made my life easier!), and created a simple test console application using them. I analyzed the network traffic (even it is HTTPS!) using Fiddler. Then I tried to understand (based on my knowledge about the authentication process), what happened and why (request data and headers, response data, cookies, etc.). Last (and probably the longest) step was an iteration of trial and error, when I was to reproduce the same network traffic using JavaScript / jQuery objects, step-by-step analyzing the results, comparing them to the original measurements captured for the test console application. So it took some time, but at the end I was quite happy with the results.

January 8, 2013

Using IE as a local Host for SharePoint ECMAScript Client Object Model

In the recent weeks I got an idea of a specific client-side HTML application (HTML file located on the local hard drive) that communicate with the SharePoint server. More on that idea later, as I hopefully achieve some results, but for now I would like to share a few interesting byproducts of my research.

This time I start with a few questions: what can we do, if we need a client application that manipulates SharePoint objects on a computer that doesn’t have (or can’t have) the managed client object model installed on it? Let’s say, you don’t have even the necessary .NET runtime version, or you don’t have Visual Studio, you have no former C# / PowerShell experience (or at least, you would not like to bother with them as you need a quickly editable application), so assume all you know is HTML and the good old JavaScript. You can’t deploy files to the SharePoint site, you don’t have even page edit permissions, so the standard way of injecting JavaScript through the Content Editor Web Part (CEWP) is not available to you. Using the REST API from your HTML pages through web requests could be an option, but it might be simply not powerful enough (especially in SharePoint 2010 version) to achieve your goals.

Using the ECMAScript Client Object Model (aka JavaScript Client Object Model or JSCOM) would be ideal to this task, but it has a significant out-of-the-box limitation: it was designed to work in the SharePoint site context it interacts with, that means, you can use only relative URLs when instantiating the SP.ClientContext object, or get the current context from the SharePoint page the script is used on, none of these is possible when working with a local HTML file.

In the last years I already delved quite deep into the client object model (both the managed and the JavaScript versions), and learned that even the out-of-the-box asynchronous calls can be altered to synchronous, so we should never give up.

In this post I show you a possible workaround for the problem in the case of an on-premise SharePoint 2010 that authenticates users through standard windows-integrated authentication. In the next post I plan to illustrate an even more complex solution for the case of the Office 365 Preview (that is a cloud-based SharePoint 2013).

As always, Fiddler proved again to be an invaluable tool analyzing the network traffic caused by the managed OM and trying to simulate the same for ECMAScript.

NOTE: As in the case of the former hacks, be aware that the methods described here don’t use the public, documented interfaces, so it is probably not supported by MS, and suggested to be used only as an experiment. There is no guarantee that it will work for you, especially if our environments are at different SP / cumulative update level.

In the progress of my work, I faced two major (and several minor) issues:

1. How to enforce using of absolute site URLs in JSCOM instead of the relative ones? It was the easier problem to solve as you see it soon.

2. How to achieve the right request digest token to get authorization to access the server side object from the JavaScript code.

Prerequisites to start the work with JSCOM in the local HTML files:

  • References to the SharePoint JavaScript libraries (and jQuery if you would like to work with that). These ones are included “automatically” when working with standard SharePoint pages, but in this case we should reference them (in the correct order!) as well. We could start our custom scripts only after our page and these libraries are already loaded.
  • A defined JavaScript variable (see more about that below) called _spPageContextInfo. That is in our case only a dummy placeholder (can be copied from the source of a SharePoint page) that the scripts depend on.
  • A HTML input field (by default it is a hidden one) called __REQUESTDIGEST that contains the authentication digest for the page. It can be copied from the source of a SharePoint page as well, but it has an expiration time. It is no problem in the case of a web page, as it is refreshed on the server site on page reloads, but it is not the case in the local file, so we should get always an actual one (see problem 2 above).

Regarding Issue 1: After a short investigation I found that the URL the client requests are sent to is determined by the $1P_0 property of the SP.ClientContext object. We should set our custom value right after initializing the context.

var siteRelativeUrl = "/";
var siteFullUrl = "http://intranet.contoso.com&quot;;
var context = new SP.ClientContext(siteRelativeUrl); // we set a dummy relative path that always exists
context.$1P_0 = siteFullUrl;

Setting the absolute URL is not enough for the successful request. Next step was to solve the issue of the request digest as without that our request would be rejected. It took me a while, but at the end it turned to be the same token as the one returned by the GetUpdatedFormDigestInformation method of the Sites web service. So all we have to do is to call this method with the properly formatted request, and replace the value of the __REQUESTDIGEST field before calling the actual JSCOM code.

On of the minor issues was, how to send POST requests to another domain (issue with cross-domain scripting), in this case to access the Sites web service. For GET requests there is JSONP (you can set crossDomain: true for your AJAX request in jQuery), but for POST, it is not available, crossDomain had simply no effect during my tests. In this situation I received an “Invalid argument.” error thrown in MicrosoftAjax.js when setting headers for the web requests.

Fortunately I found this jQuery option that solved this problem for me:

$.support.cors = true;

NOTE: When you open your HTML page not from the file system, but rather through another web server, you might be faced with the following prompt:

image

Choosing the default value (that is “No”) results in an “Access is denied.” error in MicrosoftAjax.js.

NOTE: For the sake of simplicity, I’ve uploaded a jQuery version (v1.8.2 to be exact, as the version number may be important) to my SharePoint server. In the real life one can reference for example http://code.jquery.com/jquery-latest.min.js instead of this.

Finally, here is the full source code of a “minimized” page that displays the number and the name of the lists on a remote SharePoint site. It is assumed that you are online (access to SP) and are (or can be) authenticated by the SharePoint server, having minimum read permissions on the site:

Code Snippet
  1. <script type="text/ecmascript" src="http://intranet.contoso.com/_layouts/jquery/jquery.min.js"></script>
  2. <script type="text/ecmascript" src="http://intranet.contoso.com/_layouts/1033/init.js"></script>
  3. <script type="text/ecmascript" src="http://intranet.contoso.com/_layouts/MicrosoftAjax.js"></script>
  4. <script type="text/ecmascript" src="http://intranet.contoso.com/_layouts/CUI.js"></script>
  5. <script type="text/ecmascript" src="http://intranet.contoso.com/_layouts/1033/Core.js"></script>
  6. <script type="text/ecmascript" src="http://intranet.contoso.com/_layouts/SP.Core.js"></script>
  7. <script type="text/ecmascript" src="http://intranet.contoso.com/_layouts/SP.Runtime.js"></script>
  8. <script type="text/ecmascript" src="http://intranet.contoso.com/_layouts/SP.js"></script>
  9.  
  10. <input type="hidden" name="__REQUESTDIGEST" id="__REQUESTDIGEST" value="0x0B5280FBC219C9A7E41746D9EA6AC24E65CB936560A6856AA4C38997F114401AED3254D7DDFACBE03C2028F689D0E67F55239E8FA679CE15F3EBC7A0C6280A34,05 Jan 2013 21:34:32 -0000" />
  11.  
  12. <script language="ecmascript" type="text/ecmascript">
  13.  
  14.     // define token request XML
  15.     var tokenReq = '<?xml version="1.0" encoding="utf-8"?>';
  16.     tokenReq += '<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance&quot; xmlns:xsd="http://www.w3.org/2001/XMLSchema&quot; xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">&#039;;
  17.     tokenReq += '  <soap:Body>';
  18.     tokenReq += '    <GetUpdatedFormDigestInformation xmlns="http://schemas.microsoft.com/sharepoint/soap/&quot; />';
  19.     tokenReq += '  </soap:Body>';
  20.     tokenReq += '</soap:Envelope>';
  21.  
  22.     var siteFullUrl = "http://intranet.contoso.com&quot;;
  23.     var lists;
  24.  
  25.     function startScript() {
  26.       refreshDigest();
  27.     }
  28.  
  29.     function refreshDigest() {
  30.         $.support.cors = true; // enable cross-domain query
  31.         $.ajax({
  32.             type: 'POST',
  33.             data: tokenReq,
  34.             crossDomain: true,  // had no effect, see support.cors above
  35.             contentType: 'text/xml; charset="utf-8"',
  36.             url: siteFullUrl + '/_vti_bin/sites.asmx',
  37.             headers: {
  38.                 'SOAPAction': 'http://schemas.microsoft.com/sharepoint/soap/GetUpdatedFormDigestInformation&#039;,
  39.                 'X-RequestForceAuthentication': 'true'
  40.             },   
  41.             dataType: 'xml',
  42.             complete: function (result) {  
  43.                 $('#__REQUESTDIGEST').val($(result.responseXML).find("DigestValue").text());
  44.                 sendJSCOMReq();
  45.             }
  46.         });
  47.     }
  48.  
  49.     function sendJSCOMReq()
  50.     {
  51.         var _spPageContextInfo = {webServerRelativeUrl: "\u002f", webLanguage: 1033, currentLanguage: 1033, webUIVersion:4,pageListId:"{7bbd4c55-f832-40e2-8e2a-243455c3b2ba}",pageItemId:1,userId:1073741823, alertsEnabled:true, siteServerRelativeUrl: "\u002f", allowSilverlightPrompt:'True'};
  52.  
  53.         var siteRelativeUrl = "/";
  54.         var context = new SP.ClientContext(siteRelativeUrl); // we set a dummy relative path that always exists
  55.         context.$1P_0 = siteFullUrl;
  56.  
  57.         var web = context.get_web();
  58.         lists = web.get_lists();
  59.  
  60.         context.load(lists);
  61.         context.executeQueryAsync(Function.createDelegate(this, this.onQuerySucceeded), Function.createDelegate(this, this.onQueryFailed));        
  62.     }
  63.  
  64.     function onQuerySucceeded(sender, args) {
  65.         var count = lists.get_count();
  66.         var listTitles = "Number of lists: " + count + ":\n";
  67.         for(var i=0;i<count; i++)
  68.         {
  69.             var list = lists.get_item(i);
  70.             listTitles += "  " + list.get_title() + "\n";
  71.         }
  72.         alert(listTitles);
  73.     }
  74.  
  75.     function onQueryFailed(sender, args) {
  76.       alert("Request failed: "+ args.get_message());
  77.     }
  78.  
  79.     // start the custom script execution after the scripts and page are loaded
  80.     SP.SOD.executeOrDelayUntilScriptLoaded(function () {
  81.         $(document).ready(startScript);
  82.     }, "sp.js");
  83.  
  84. </script>

After solving these problems I had a bad feeling. What happens if I authenticate myself as a standard user with low privileges, then stole the digest of a site owner included in a SharePoint page using a network traffic analyzer tool and try to send my request with the token of the other user? My experiments show that this issue is handled by SharePoint, as I received an error stating the token was not valid and I had to get a new one from the server.

In the next post I take another step forward to show you how to achieve the same using the Office 365 Preview version.

October 16, 2011

Creating more advanced conditions for your ECB menus through jQuery and synchronous Client Object Model / WCF Data Services calls

In my last post I showed you how to alter the Edit Control Block (ECB) menu of SharePoint 2010 based on simple conditions. As I described, if you need to use more advanced conditions, for example based on list item field values or other information not available on the client side, you should apply a few tricks or even hacks.

For these advanced solutions it is useful to provide the context to our allowDeletion method through the ctx parameter, so we add this one to the parameter list of the method.

It means that our original condition in method AddListMenuItems is changed from

if (allowDeletion(currentItemID))

to

if (allowDeletion(ctx, currentItemID))

In the simplest case, the information is already there at the page on client side, you simply have to find the way to get it. Checking the HTML source of the page or using the Developer Tools in the Tools menu of Internet Explorer (you can press F12 as a shortcut key) usually helps you to find the right track.

For example, if you need the title of the item, that is included in the LinkTitle field (as we have an ECB menu linked to the title) for each items. When looking up the right item, we should use the ID of the item (passed as the itemID parameter) and the ID of our list view (available in ctx.ctxId).

Note: Although it should be evident, your page must load the jQuery library before you can use jQuery methods. Similarly, the ECMAScript Client Object Model library must be loaded before you reference its objects in methods later in the posts.

So if we would like to allow deletion only for items having title beginning with ‘V’ then we need to apply a method like this:

  1. function allowDeletion(ctx, itemID) {
  2.  
  3.     var title = jQuery('div[Field="LinkTitle"][CTXName="ctx' + ctx.ctxId + '"][id="' + itemID + '"]').text();
  4.  
  5.     return (title.toUpperCase().startsWith("V"));
  6.  
  7. }

Next step is to see how to get the information if it is not available on the client side, but included in the list items on the server side. My first idea was to use the ECMAScript Client Object Model, however it turned out quickly that is not the best choice, as the AddListMenuItems method does not wait for the asynchronous reply supported by the client OM to be returned. Although I created a dirty workaround for that, I will show you that later.

Another alternative is to use REST protocol through the WCF Data Services.

As mentioned, our call must be synchronous, so we have to use jQuery.ajax specifying async: false. The next code snippet shows an example using the same condition applied above, that is title of the item must be started with ‘V’ to enable deletion.

  1. function allowDeletion(ctx, itemID) {
  2.  
  3.     itemUrl = ctx.HttpRoot + "/_vti_bin/listdata.svc/" + ctx.ListTitle +"(" + itemID + ")?$select=Title"    
  4.  
  5.     var title = "";
  6.  
  7.     jQuery.ajax(
  8.     {
  9.         type: 'GET',
  10.         url: itemUrl,
  11.         dataType: 'json',
  12.         success: function (result) {
  13.             if (result.isOk != false) {
  14.                 title = result.d.Title;
  15.             }
  16.         },
  17.         data: {},
  18.         async: false
  19.     });
  20.  
  21.     return (title.toUpperCase().startsWith("V"));
  22.  
  23. }

Note: When working with jQuery and WCF Data Services, it is useful to know about the parsererror issue with field values containing apostrophe (single quote) and how to fix it.

It is important to note that you are not restricted to the actual list only. With a bit of additional complexity you can query related lists as well using $expand and the lookup field / user field IDs in the current list. If you are unsure how to compose the URL for you request, I suggest you to try to create the filter first in C# using the LINQ syntax, then use Fiddler to capture the request or apply this simple trick from Sahil Malik.

If network bandwidth and speed are limited, then waiting for the synchronous result will cause issues via blocking the UI thread of the browser. If that is the case you should consider “pre-caching” the data required for checks on page load through a single request. If the item count of the list is limited, then you can cache all of the data (in our case, the IDs and the related titles of the items is needed), in the case of a larger list, you should get only the items displayed in the current page of the view. To get the IDs of the items on the page you should run a jQuery select similar to the one we used to get the title in the first example, and submit the REST request using a complex condition.

Last, I would like to show you my workaround for calling ECMAScript Client Object Model synchronously. As you might now, it is officially / theoretically not possible. I found that it can be done technologically, although I had to spend a few hours Fiddlering and digging into the internals of the ECMAScript Client OM (SP.Runtime.debug.js), comparing the JavaScript methods to the ones in the managed Client OM classes using Reflector. If you demand more information about it I can give you more details, but now I publish it “as is”.

To build up the request I use the “traditional” ECMAScript Client Object Model (that, of course, should be already loaded before our script), but before sending it I get the built-up request in XML format from the internal methods of the OM, and send the request through jQuery.ajax in synchronous mode, just as like the case of WCF Data Services. The JSON result is “loaded” into JavaScript objects.

Be aware that the method described here don’t use the public, documented interfaces, so it is probably not supported by MS, and suggested to be used only as an experiment. There is no guarantee that it will work for you, especially if our environments are at different SP / cumulative update level.

  1. function allowDeletion(ctx, itemID) {
  2.  
  3.     var title = "";
  4.  
  5.     try {
  6.         var context = SP.ClientContext.get_current();
  7.         var web = context.get_web();
  8.         var selectedListId = SP.ListOperation.Selection.getSelectedList();
  9.         var selectedListItem = web.get_lists().getById(selectedListId).getItemById(itemID);
  10.         context.load(selectedListItem, "Title");
  11.  
  12.         // start hacking
  13.         var pendingRequest = context.get_pendingRequest();
  14.         var webRequest = pendingRequest.get_webRequest();
  15.  
  16.         // get the request XML
  17.         var body = pendingRequest.$24_0().toString();
  18.  
  19.         // get the URL of client.svc
  20.         var url = webRequest.get_url();
  21.  
  22.         // "initialize" request
  23.         SP.ClientRequest.$1T(webRequest);
  24.  
  25.         // we should add digest later to the request as an HTTP header (see below)
  26.         var digest = webRequest.get_headers()['X-RequestDigest'];
  27.  
  28.         jQuery.ajax(
  29.         {
  30.             type: "POST",
  31.             data: body,
  32.             url: ctx.HttpRoot + url,
  33.             success: function (result) {
  34.                 if (result.isOk != false) {
  35.                     title = result[result.length - 1].Title;
  36.                 }
  37.             },
  38.             headers: {
  39.                 "x-requestdigest": digest
  40.             },
  41.             contentType: "application/x-www-form-urlencoded",
  42.             async: false
  43.         });
  44.  
  45.     }
  46.     catch (e) {
  47.         alert("Error: " + e);
  48.     }
  49.  
  50.     return (title.toUpperCase().startsWith("V"));
  51.  
  52. }

BTW, the example above achieves the same, it allows deletion only for items having title beginning with ‘V’. Similarly to the former WCF DS example, the synchronous request through the network blocks the UI thread, and might require pre-caching in case of a slow network.

September 21, 2011

Creating ECB menu items based on specific list properties using the client object model

In my past posts I’ve already described how to hide and modify ECB menu items through JavaScript and the SharePoint client object model.

Now I show how to create new menu items on demand based on the properties of the list.

Since it is not so different from the former examples, I do not provide the full solution for the current sample, only include the most important codes.

Our first Elements file contains the ScriptLink custom actions. The Sequence attribute of the CustomAction elements is important, as the later scripts depend on the former ones.

First we add a reference to jQuery, next we add our custom menus.js file that contain the logic for adding the new menu. Last we add a script block that (after the page is loaded) wait for the ECMAScript client OM script (sp.js) to be loaded, then calls the createEcbMenus method (included in menus.js).

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  3.   <CustomAction
  4.      ScriptSrc="scripts/jQuery/jquery-1.6.1.min.js"
  5.      Location="ScriptLink"
  6.      Sequence="1000" />
  7.   <CustomAction
  8.       Location="ScriptLink"
  9.       ScriptSrc="scripts/menus.js"
  10.       Sequence="1100" />
  11.   <CustomAction
  12.     Location="ScriptLink"
  13.     ScriptBlock="$.noConflict(); jQuery(document).ready(function() { ExecuteOrDelayUntilScriptLoaded(createEcbMenus, 'sp.js'); });"
  14.     Sequence="1200" />
  15. </Elements>

Following code is the content of the menus.js that we deploy to the Layouts\scripts folder.

  1. var inProgress = false;
  2. var list;
  3. var selectedListId;
  4.  
  5. function createEcbMenus() {
  6.     ExecuteOrDelayUntilScriptLoaded(createEcbMenusEx, 'Core.js');
  7. };
  8.  
  9.  
  10. function createEcbMenusEx() {
  11.     if (!inProgress) {
  12.         try {
  13.             inProgress = true;
  14.             var selection = SP.ListOperation.Selection;
  15.             if (selection != null) {
  16.                 this.selectedListId = selection.getSelectedList();
  17.                 if (selectedListId != null) {
  18.                     var context = SP.ClientContext.get_current();
  19.                     web = context.get_web();
  20.                     this.list = web.get_lists().getById(this.selectedListId);
  21.                     // we will use the Title property of the list later on
  22.                     // so add it to the query
  23.                     context.load(this.list, "Title");
  24.                     context.executeQueryAsync(Function.createDelegate(this, this.gotList), Function.createDelegate(this, this.failed));
  25.                 }
  26.             }
  27.         }
  28.         catch (e) {
  29.             alert("Error: " + e);
  30.             inProgress = false;
  31.         }
  32.     }
  33. }
  34.  
  35. function failed(sender, args) {
  36.     alert("Operation failed: " + args.get_message());
  37.     inProgress = false;
  38. }
  39.  
  40. function gotList() {
  41.     if (this.list.get_title() == 'My List') {
  42.         // just to be sure jQuery is loaded
  43.         if (jQuery) {
  44.             var id = "ECBItems_" + this.selectedListId.toLowerCase();
  45.  
  46.             jQuery('div[id*="ECBItems"]').each(
  47.                 function () {
  48.                     if (jQuery(this).attr('id') == id) {
  49.                         jQuery('div[id*="ECBItems"]').each(
  50.                         function () {
  51.                             if (jQuery(this).attr('id') == id) {
  52.                                 jQuery(this).append('<div><div>Custom menu item</div><div></div><div>javascript:doSomethingWithListItem({ItemId})</div><div>0×0</div><div>0×0</div><div>List</div><div>100</div><div>1000</div></div>')
  53.                             }
  54.                         });
  55.                     }
  56.                 });
  57.         }
  58.  
  59.     }
  60.     inProgress = false;
  61. }
  62.  
  63.  
  64. function doSomethingWithListItem(listItemId) {
  65.     alert("List item ID: " + listItemId);
  66. }

The createEcbMenus method we call from the last ScriptLink custom action (see above) simply waits for the loading of the Core.js file, then it calls the createEcbMenusEx method.

The createEcbMenusEx method submits a query using the client OM to get the Title property of the current list, that we use in this sample to decide if we should add the new menu item or not.

The most important part is the gotList callback method that is called when the submitted query succeeded. If the list is named “My List” then we check for the ECBItems DIV, and add the DIV structure for the new menu item to it. See former posts mentioned above for details of the format. In this case the menu title will be Custom menu item, and it calls the doSomethingWithListItem method when clicked.

Theoretically this code should work, but in practice, sometimes it simply does not. It has a very straightforward reason, namely it does not find the ECBItems DIV where it should to add the new item to. It happens when no custom action deployed for the EditControlBlock.

Note: If you need more info about it, check the former posts referred to above for the RenderECBItemsAsHtml method. BTW, that method is called by XsltListViewWebPart.OutputECB method that is called by the BaseXsltListWebPart.RenderWebPart method during the list view rendering. If you would like to know more about the custom action internals, I suggest you to read this post.

As a workaround we should add a dummy custom action element file that is never displayed. In this case I used the RegistrationId=”12345” and RegistrationType="List", so if you don’t have a list based on a template with ID 12345, you should be OK.

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  3.   <CustomAction Description="Dummy Custom Action that should be never displayed"
  4.               Id="MyCompany.DummyAction"
  5.               Location="EditControlBlock"
  6.               ImageUrl="/_layouts/images/dummy.png"
  7.               Sequence="1000"
  8.               Title="Dummy"
  9.               RegistrationType="List"
  10.               RegistrationId="12345" >
  11.     <UrlAction Url="javascript:alert('It is a dummy CA')"/>
  12.   </CustomAction>
  13. </Elements>

Deploying the dummy custom action forces the ECBItems DIV to be rendered, so our script will able to add the new item to the menu.

July 26, 2011

How to add a button to the ribbon to update a field for selected list items using the client object model

I received a comment for one of my former posts asking how it is possible to add a custom button to the ribbon that allows users to set field values for selected list items. In this post I show you a solution for this request.

You can download the complete solution from here.

The solution contains a content type with a numerical field called Numeric. A test list schema is created and the content type is assigned to it. There is also a list instance for the test list with a few items.

The following custom action definition assigns the new ribbon button to our content type. Important to note that the hexadecimal digits in the ID of the content type must be written with all capital letters in the RegistrationId attribute.

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  3.   <CustomAction
  4.     Id="Ribbon.Library.Actions.ResetItems"
  5.     Location="CommandUI.Ribbon"
  6.     RegistrationType="ContentType"
  7.     RegistrationId="0x0100E36DB888927049468D7CBAF1C7B70582"
  8.     Title="Reset Selected Items Action">
  9.     <CommandUIExtension>
  10.       <CommandUIDefinitions>
  11.         <CommandUIDefinition
  12.           Location="Ribbon.ListItem.Manage.Controls._children">
  13.           <Button Id="Ribbon.ListItem.Manage.SetItemData"
  14.             Command="SetItemDataButtonCommand"
  15.             Image16by16="/_layouts/images/JCOMUpdateSelectedItems/exclamation.png"
  16.             LabelText="Reset Selected Items"
  17.             TemplateAlias="o2" />
  18.         </CommandUIDefinition>
  19.       </CommandUIDefinitions>
  20.       <CommandUIHandlers>
  21.         <CommandUIHandler
  22.           Command="SetItemDataButtonCommand"
  23.           CommandAction="javascript:updateSelectedItems();"
  24.           EnabledScript="javascript:isNonFolderSelected();" />
  25.       </CommandUIHandlers>
  26.     </CommandUIExtension>
  27.   </CustomAction>
  28. </Elements>

As you can see, the SetItemDataButtonCommand command has a CommandAction that is bound to the updateSelectedItems JavaScript method, and the EnabledScript attribute is set to the  isNonFolderSelected method.

This latter one ensures that the ribbon button will be disabled until at least one non-folder item is selected in the list. It can be achieved by iterating through the selected items and checking their fsObjType property, as illustrated by the following code:

  1. function isNonFolderSelected() {
  2.     var selectedItems = SP.ListOperation.Selection.getSelectedItems();
  3.     var result = false;
  4.  
  5.     for (var i = 0; i < selectedItems.length; i++) {
  6.         // it is a list item, not a folder
  7.         if (selectedItems[i].fsObjType == 0) {
  8.             result = true;
  9.             break;
  10.         }
  11.     }
  12.  
  13.     return result;
  14. }

If the user clicks on the button, the updateSelectedItems method is invoked. This method gets a reference for the current web and list objects, takes the selected items, and for each non-folder item sets the Numeric field to zero. Finally it calls executeQueryAsync to submit the changes.

  1. var inProgress = false;
  2. var selectedListId;
  3.  
  4. function updateSelectedItems() {
  5.     if (inProgress) {
  6.         alert("Another request is in progress. Try again later!");
  7.     }
  8.     else {
  9.         try {
  10.             var context = SP.ClientContext.get_current();
  11.             var web = context.get_web();
  12.             var selectedItems = SP.ListOperation.Selection.getSelectedItems();
  13.             this.selectedListId = SP.ListOperation.Selection.getSelectedList();
  14.             for (var i = 0; i < selectedItems.length; i++) {
  15.                 // it is a list item, not a folder
  16.                 if (selectedItems[i].fsObjType == 0) {
  17.                     var listItem = web.get_lists().getById(selectedListId).getItemById(selectedItems[i].id);
  18.                     context.load(listItem);
  19.                     listItem.set_item("Numeric", 0);
  20.                     listItem.update();
  21.  
  22.                 }
  23.             }
  24.  
  25.             context.executeQueryAsync(Function.createDelegate(this, itemsUpdated), Function.createDelegate(this, updateFailed));
  26.         }
  27.         catch (e) {
  28.             alert("Error: " + e);
  29.             inProgress = false;
  30.         }
  31.     }
  32. }
  33.  
  34. function updateFailed(sender, args) {
  35.     alert("Operation failed: " + args.get_message());
  36.     inProgress = false;
  37. }
  38.  
  39. function itemsUpdated() {
  40.     inProgress = false;
  41.     var convertedListId = selectedListId.toLowerCase().replace("-", "_").replace("{", "").replace("}", "");
  42.     var controlId = 'ctl00$m$g_' + convertedListId + '$ctl02';
  43.     __doPostBack(controlId, 'cancel');
  44.     //window.location.href = window.location.href;
  45. }

On successful execution of the request the itemsUpdated method is called. It updates the UI asynchronously as described in this post.

To enable asynchronous updates, theoretically you would have to ‘Enable Asynchronous Update’ at AJAX Options of the list web part, although based on my last tests the method works even if the asynchronous update is not allowed.

image

If you don’t need the asynchronously update, you can simply use this line to reload the whole page after Numeric values are set for selected items.

window.location.href = window.location.href;

image

July 25, 2011

Triggering an asynchronous refresh from code

In one of my former posts I’ve demonstrated how to modify the list item properties from an ECB custom action using the client object model.

You might have noticed there that after the modification I reloaded the list content by setting the page location to itself:

window.location.href = window.location.href;

It means that all of the page content is reloaded that may be not optimal given that SharePoint 2010 lists support asynchronous updating.

But how can we achieve this functionality from client side code? To understand that, we should first enable the manual refresh button in the web part properties.

(Note: Checking the ‘Show Manual Refresh Button’ checkbox will check the ‘Enable Asynchronous Update’ checkbox after applying the settings.)

image

Then let’s see what HTML code is injected as the result of this setting (most important part is highlighted with bold):

<a href=”javascript:” onclick=” javascript: __doPostBack(‘ctl00$m$g_e9bf1559_32e7_40ea_88fa_3aced214cc97$ctl02′,’cancel’);return false;”><img src=”/_layouts/images/staticrefresh.gif” id=”ManualRefresh” border=”0″ alt=”Click here to refresh the data view.” /></a>

In the code above the value ‘e9bf1559_32e7_40ea_88fa_3aced214cc97’ is the ID (Guid) of the list having the leading and trailing curly brackets trimmed and hyphens replaced with underscore.

We can reproduce that for the list displayed by the current page using a code like this:

  1. var selectedListId = SP.ListOperation.Selection.getSelectedList();
  2. var convertedListId = selectedListId.toLowerCase().replace(“-”, “_”).replace(“{“, “”).replace(“}”, “”);
  3. var controlId = ‘ctl00$m$g_’ + convertedListId + ‘$ctl02′;
  4. __doPostBack(controlId, ‘cancel’);

Note: The above solution works for my current SharePoint installation as long as at least the ‘Enable Asynchronous Update’ checkbox is checked at AJAX Options. There is no guarantee that the structure of the control ID remains the same in forthcoming cumulative updates / service packs, or if you add other web parts to the page.

The code relies on the client object model when determining the list ID by calling SP.ListOperation.Selection.getSelectedList(), so the sp.js file must be loaded before the script executes. See my former post about how to use ExecuteOrDelayUntilScriptLoaded to enforce that.

In my forthcoming post I will illustrate this technique via a complete example.

July 24, 2011

Hiding ECB custom actions based on specific list properties using the client object model

There might be cases when you need to display an Edit Control Block custom action only when specific circumstances are met. These conditions can be list or user specific or more generic, like the menu is visible only for a given time interval.

Unfortunately the declarative description of a custom action allows only a very limited set of conditions through the RegistrationType, RegistrationId, RequiredAdmin, RequireSiteAdministrator and Rights attributes. If you have more complex requirements, you probably need to find a more advanced solution. The client object model in SharePoint 2010 allows you to create some really powerful customizations.

In this post I show you how to display an ECB custom action menu for lists having a specific property, but you can use the very same technique if you need to show / hide the menu based on the properties of the current user or even more complex SharePoint properties. This example shows a “Publish” ECB menu item for each Custom List as long as versioning is enabled for the list.

In the first step we register our ECB menu item for the Custom List list type.

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  3.   <CustomAction Description="Custom Action Sample for Versioned Lists"
  4.               Id="JCOMCustomAction.PublishData"
  5.               Location="EditControlBlock"
  6.               ImageUrl="/_layouts/images/JCOMCustomActionOnVersionedLists/exclamation.png"
  7.               Sequence="1000"
  8.               Title="Publish"
  9.               RegistrationType="List"
  10.               RegistrationId="100" >
  11.     <UrlAction Url="javascript:alert('Item {ItemId} in list {ListId} is published')"/>
  12.   </CustomAction>
  13. </Elements>

We also register three ScriptLink custom action to inject a reference to the jQuery library, to our custom script file CustomAction.js, and a ScriptBlock that calls our custom checkListProps method after the page and the client object model library (sp.js) is loaded. It’s not enough to wait for the page load. If we don’t wait for the client object model library, and try to call our custom script immediately on page load, all of the references to the client library will be unknown to the script runtime.

We can wait for the page load using the jQuery(document).ready, and for sp.js file to be loaded using the ExecuteOrDelayUntilScriptLoaded method. Important, that one should use only the name of the method to call as the first parameter (like checkListProps and not checkListProps()).

Note, that since I called the jQuery noConflict() method to avoid possible conflicts, I have to explicitly type jQuery instead of the $ shortcut here and later in my custom script as well.

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  3.   <CustomAction
  4.       ScriptSrc="JCOMCustomActionOnVersionedLists/jquery-1.6.1.min.js"
  5.       Location="ScriptLink"
  6.       Sequence="1000"
  7.       />
  8.   
  9.   <CustomAction
  10.       Location="ScriptLink"
  11.       ScriptSrc="JCOMCustomActionOnVersionedLists/CustomAction.js"
  12.       Sequence="1001"
  13.       />
  14.  
  15.   <CustomAction
  16.       Location="ScriptLink"
  17.       ScriptBlock="$.noConflict(); jQuery(document).ready(function() { ExecuteOrDelayUntilScriptLoaded(checkListProps, 'sp.js'); });"
  18.       Sequence="1002"
  19.       />
  20.  
  21. </Elements>

The most interesting part of the solution is our custom script file. The skeleton of the script is similar to my former solution.

  1. var inProgress = false;
  2. var list;
  3. var selectedListId;
  4.  
  5. function checkListProps() {
  6.     if (!inProgress) {
  7.         try {
  8.             inProgress = true;            
  9.             var selection = SP.ListOperation.Selection;
  10.             if (selection != null) {
  11.                 this.selectedListId = selection.getSelectedList();
  12.                 if (selectedListId != null) {
  13.                     var context = SP.ClientContext.get_current();
  14.                     var web = context.get_web();
  15.                     this.list = web.get_lists().getById(this.selectedListId);
  16.                     context.load(this.list);
  17.                     context.executeQueryAsync(Function.createDelegate(this, this.gotList), Function.createDelegate(this, this.failed));
  18.                 }
  19.             }
  20.         }
  21.         catch (e) {
  22.             alert("Error: " + e);
  23.             inProgress = false;
  24.         }
  25.     }
  26. }
  27.  
  28. function failed(sender, args) {
  29.     alert("Operation failed: " + args.get_message());
  30.     inProgress = false;
  31. }
  32.  
  33. jQuery.expr[":"].econtains = function (obj, index, meta, stack) { return (obj.textContent || obj.innerText || jQuery(obj).text() || "").toLowerCase() == meta[3].toLowerCase(); }
  34.  
  35. function gotList() {
  36.     if (!this.list.get_enableVersioning()) {                
  37.         
  38.         // just to be sure jQuery is loaded
  39.         if (jQuery) {
  40.  
  41.             var id = "ECBItems_" + this.selectedListId.toLowerCase();
  42.  
  43.             // this lookup does not work, probably due to braces in the ID
  44.             //jQuery("#" + id)
  45.  
  46.             // workaround
  47.             jQuery('div[id*="ECBItems"]').each(
  48.                 function () {
  49.                     if (jQuery(this).attr('id') == id) {
  50.                     // replace "Publish" to match the title of your ECB custom action
  51.                      jQuery(this).find('div div:first-child:econtains("Publish")').parent().remove(); } });  
  52.         }
  53.  
  54.      }
  55.      inProgress = false;
  56. }

The checkListProps method that we call after the page and the client object model script library is loaded first stores the current list ID in the selectedListId variable then gets a reference for the current list into the list variable using the list ID and the current web, and finally starts an asynchronous query to retrieve the list properties.

The failed method only displays the error message if the query fails.

The gotList method is called on query success. It checks if the versioning is enabled and if not, it removes the part of the ECB block that is responsible for displaying our custom action.

Finding the correct HTML element is a bit tricky. You can find another version in this MSDN forum thread for WSS 3.0, but I refactored this for SharePoint 2010 using jQuery to get somewhat more compact code.

The DIV tag that contains the ECB block has an ID like ECBItems_{62827211-6a39-4d9b-908f-d74ef14b2746}, where the GUID is the ID of the list. Unfortunately I was not able to get a reference for this kind of ID using jQuery, probably because the curly braces are not valid characters in the HTML ID attribute.

My workaround is to lookup HTML DIV elements with ID containing “ECBItems”, then iterate through the results using each(). If the ID is what we are looking for then we find the element whose first sub-DIV contains exactly the text of our custom action, and remove its parent node from the DOM.

If you are sure your page contains a single ECB block you can simply use this line:

$(‘[id*="ECBItems"]‘).find(‘div div:first-child’).filter(function() {  return this.innerText == "Publish" }).parent().remove(); 

We need to check the first sub-DIV, not any arbitrary one, because the first one contains the text of the custom action, and we do not want to remove a possible another custom action with the same text in the DIV for UrlAction. We also have to do an exact match to avoid removing custom actions with similar texts. For example, the standard :contains() jQuery selector, would remove a Publish later menu too in our case. To force exact math I introduced the :econtains() selector.

You can download the sample solution from here.

To test the solution, I’ve created a Custom List instance with a few list items on my test site manually (not included in the sample code). After deploying the solution, the menu does not contain the Publish item.

image

Next, I set the list to support versioning:

image

When I was back to the list, the Publish item appeared in the ECB menu:

image

April 8, 2011

Using ECMAScript Client Object Model from ECB Custom Actions

For MOSS 2007 / WSS 3.0 one could use JavaScript in Edit Control Block (EBC) custom action as described in this article by Jan Tielens. For SharePoint 2010 it is now even easier to achieve similar results with the ECMAScript Client Object Model.

In my test project I’ve created a simple custom content type, having a Title and a number-typed field called Numeric, a list definition and a list instance including sample data and bound the custom actions to the content type.

The deployed list called Test List Instance looks like this:

image

The next step is to create the JavaScript file and bind it to the pages. It can be done using a CustomAction with the Location value ScriptLink, as shown here:

  1. <?xmlversion=1.0encoding=utf-8?>
  2. <Elementsxmlns=http://schemas.microsoft.com/sharepoint/>
  3.   <CustomAction
  4.       Location=ScriptLink
  5.       ScriptSrc=JCOMCustomAction/CustomAction.js
  6.       Sequence=100 />
  7. </Elements>

Unfortunately, in this case you cannot use RegistrationType nor RegistrationId to specify the items your custom action is bound to.

The JavaScript file itself and the images we use for the ECB custom actions are added to the project through mapped folders:

image

In this sample I’ve created two actions, one to query the field values of the current item, and another one to reset the Numeric field value to zero for the current item.

First, let’s see the custom action definition of the “getter” method.

  1. <?xmlversion=1.0encoding=utf-8?>
  2. <Elementsxmlns=http://schemas.microsoft.com/sharepoint/>
  3.   <CustomActionDescription=JavaScript Client Object Model Get Item Data Custom Action
  4.               Id=JCOMCustomAction.GetItemData
  5.               Location=EditControlBlock
  6.               ImageUrl=/_layouts/images/JCOMCustomAction/question.png
  7.               Sequence=1000
  8.     Title=Get Item Data
  9.     RegistrationType=ContentType
  10.     RegistrationId=0x0100e36db888927049468d7cbaf1c7b70582>
  11.     <UrlActionUrl=javascript:getItemById(‘{ListId}’, {ItemId})/>
  12.   </CustomAction>
  13. </Elements>

We call the getItemById JavaScript method with two parameters, {ListId} for the list ID (GUID value passed as string, so we should use apostrophes) and {ItemId} (this one is a plain numeric value) for item ID of the current item. Both of these parameters are tokens and will be replaced by the actual values.

In the corresponding JavaScript block we check first if there is another request in progress, and if there is not, then we query the field values in an asynchronous ECMAScript client object call.

  1. var item;
  2. var inProgress = false;
  3. function getItemById(listId, listItemId) {
  4.     if (inProgress) {
  5.         alert(“Another request is in progress. Try again later!”);
  6.     }
  7.     else {
  8.         try {
  9.             inProgress = true;
  10.             var context = new SP.ClientContext.get_current();
  11.             var web = context.get_web();
  12.             var list = web.get_lists().getById(listId);
  13.             this.item = list.getItemById(listItemId);
  14.             context.load(this.item, “Title”, “Numeric”);
  15.             context.executeQueryAsync(Function.createDelegate(this, this.itemReceived), Function.createDelegate(this, this.failed));
  16.         }
  17.         catch (e) {
  18.             alert(“Error: “ + e);
  19.             inProgress = false;
  20.         }
  21.     }
  22. }
  23. function itemReceived() {
  24.     gotItem(this.item);
  25.     inProgress = false;
  26. }
  27. function failed(sender, args) {
  28.     alert(“Operation failed: “ + args.get_message());
  29.     inProgress = false;
  30. }
  31. function gotItem(item) {
  32.     alert(“Title: “ + item.get_item(“Title”) + “\r\nNumeric: “ + item.get_item(“Numeric”));
  33. }

Selecting the menu item in the ECB and the results shown below.

image

image

The custom action definition and the “setter” method is very similar to the former one.

  1. <?xmlversion=1.0encoding=utf-8?>
  2. <Elementsxmlns=http://schemas.microsoft.com/sharepoint/>
  3.   <CustomActionDescription=JavaScript Client Object Model Set Item Data Custom Action
  4.               Id=JCOMCustomAction.SetItemData
  5.               Location=EditControlBlock
  6.               ImageUrl=/_layouts/images/JCOMCustomAction/exclamation.png
  7.               Sequence=1000
  8.     Title=Set Item Data
  9.     RegistrationType=ContentType
  10.     RegistrationId=0x0100e36db888927049468d7cbaf1c7b70582>
  11.     <UrlActionUrl=javascript:setItemById(‘{ListId}’, {ItemId})/>
  12.   </CustomAction>
  13. </Elements>

After a successful update, we have to refresh the page content to show the change. I achieved that simple by reloading the entire page. Some kind of AJAX-refresh would be definitely better here, but I have not yet found the right way to force that.

  1. function setItemById(listId, listItemId) {
  2.     if (inProgress) {
  3.         alert(“Another request is in progress. Try again later!”);
  4.     }
  5.     else {
  6.         try {
  7.             inProgress = true;
  8.             var context = new SP.ClientContext.get_current();
  9.             var web = context.get_web();
  10.             var list = web.get_lists().getById(listId);
  11.             this.item = list.getItemById(listItemId);
  12.             context.load(this.item);
  13.             item.set_item(“Numeric”, 0);
  14.             item.update();
  15.             context.executeQueryAsync(Function.createDelegate(this, this.itemUpdated), Function.createDelegate(this, this.failed));
  16.         }
  17.         catch (e) {
  18.             alert(“Error: “ + e);
  19.             inProgress = false;
  20.         }
  21.     }
  22. }
  23. function itemUpdated() {
  24.     inProgress = false;
  25.     window.location.href = window.location.href;
  26. }

Selecting the setter menu item in the ECB and the results shown below. Notice the zero value of the Numeric field for the updated item.

image

image

You can download the sample project from this location.

Theme: Shocking Blue Green. Blog at WordPress.com.

Follow

Get every new post delivered to your Inbox.

Join 42 other followers