Second Life of a Hungarian SharePoint Geek

May 30, 2013

Configurable Column Widths in SharePoint Views

Filed under: jQuery, REST, SP 2010, Views — Tags: , , , — Peter Holpar @ 18:32

In my recent post I wrote about a solution that enables users to resize the column width of SharePoint views. In this post I provide a sample for configuring the width of the columns through a helper list.

I’ve created a custom list called ColumnWidths and added a string (ColumnName) and a numeric field (ColumnWidth) to it for the name and the desired column width of the field. Note, that you should use the same name for the field, as it is referred to in the view / HTML source, for example, a title with context menu is called LinkTitle. The Title field of the configuration list item contains the site relative URL of the view that we would like to customize.

image 

The script is based on jQuery and the same LINQ for JavaScript (ver.3.0.3-Beta4) library that I used in these samples as well. The script was injected to the target view page (for example into the /Lists/Test/AllItems.aspx) through the Content Editor Web Part.

In this case I used REST to query the configuration list, a similar result could be achieved through the ECMAScript Client Object Model.

Code Snippet
  1. <script src="/_layouts/jQuery/jquery-1.8.3.min.js"></script>
  2. <script src="/_layouts/jQuery/linq.js"></script>
  3. <script src="/_layouts/jQuery/linq.jquery.js"></script>
  4.  
  5. <script language="ecmascript" type="text/ecmascript">
  6.  
  7. $(document).ready(startScript);
  8.  
  9. function startScript() {
  10.  
  11.     var pageUrl = document.URL;
  12.     var pageUrlLenght = pageUrl.length;
  13.     var siteUrl = ctx.HttpRoot;
  14.     var siteUrlLength = siteUrl.length;
  15.     var siteRelativeUrl = pageUrl.substring(siteUrlLength, pageUrlLenght);
  16.     var url = siteRelativeUrl.substring(0, siteRelativeUrl.lastIndexOf("?"));
  17.  
  18.     $.ajax({
  19.         type: 'GET',
  20.         contentType: 'application/json;odata=verbose',
  21.         url: siteUrl + "/_vti_bin/listdata.svc/ColumnWidths()?$filter=Title eq '/Lists/Test/AllItems.aspx'&$select=ColumnName,ColumnWidth",
  22.         headers: {                    
  23.             "Accept": "application/json; odata=verbose"
  24.             },
  25.         dataType: "json",
  26.         complete: function (result) {
  27.             var response = JSON.parse(result.responseText);
  28.             if (response.error) {
  29.                 alert("Error: " + response.error.code + "\n" + response.error.message.value);
  30.             }
  31.             else {
  32.                 var columnWidths = response.d.results;
  33.                 Enumerable.from(columnWidths).forEach(function(x) {
  34.                     $('div.ms-vh-div[name="' + x.ColumnName + '"]').closest('th').width(x.ColumnWidth);
  35.                 });
  36.             }
  37.         },
  38.         error: function(xmlHttpRequest, textStatus, errorThrown) {
  39.                     alert(errorThrown);
  40.         }
  41.     });
  42.  
  43. }
  44.  
  45. </script>

Here is the original formatting of our Test list view. This is the same output as the view is displayed before the script is executed:

image

And here is the view after the page is loaded completely and the script is executed:

image

Through this method the width of the columns can be relative easily configured without using SharePoint Designer.

May 11, 2013

Accessing Office 365 SharePoint sites using REST from a local HTML / JavaScript Host

Filed under: JavaScript, jQuery, Office 365, REST, SP 2013 — Tags: , , , , — Peter Holpar @ 22:12

A few month ago I wrote about accessing Office 365 sites using the JavaScript. In that sample I used the SharePoint ECMAScript client object model. Last month a commenter, Gilles asked if we could use REST the same way to access the information on the O365 site. The answer is a definitive yes, I already provided similar solutions in my Favorites in the Cloud posts (here and here). In those posts I used however WinJS.xhr and not the ajax method of jQuery, so it might worth to see another sample that utilizes jQuery.

Originally – to keep the samples in this post short – I planned to include only the code that is relevant to the solution and / or differs from the client OM solution, however later I made a lot of small enhancements in the original code as well, so probably it is simpler to publish the full code “as is” even with possible duplicates / overlapping with the former version.

The sample in this post simply creates a document library in the O365 site, but it illustrates the process of authentication and can serve as a base for more sophisticated applications as well.

The format of the token (tokenReq) and the authentication requests (authReq) is the same as the JSCOM sample, and the process itself is also very similar:

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 contextinfo REST endpoint (see MSDN for details) or from the Sites web service store the value into a JavaScript variable (digest).

4. Execute the REST request.

Code Snippet
  1. <script type="text/ecmascript" src="http://code.jquery.com/jquery-1.8.3.min.js"></script>
  2.  
  3. <script language="ecmascript" type="text/ecmascript">
  4.  
  5.     var tokenReq = '<?xml version="1.0" encoding="utf-8"?>';
  6.     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;;
  7.     tokenReq += '  <soap:Body>';
  8.     tokenReq += '    <GetUpdatedFormDigestInformation xmlns="http://schemas.microsoft.com/sharepoint/soap/&quot; />';
  9.     tokenReq += '  </soap:Body>';
  10.     tokenReq += '</soap:Envelope>';
  11.  
  12.     // you should set these values according your actual request
  13.     var usr = 'username@yourdomain.onmicrosoft.com';
  14.     var pwd = 'password';
  15.     var siteFullUrl = "https://yourdomain-my.sharepoint.com&quot;;
  16.    
  17.     var loginUrl = siteFullUrl + "/_forms/default.aspx?wa=wsignin1.0";
  18.     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;
  19.     authReq +=      '  <s:Header>'
  20.     authReq +=      '    <a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Action>&#039;
  21.     authReq +=      '    <a:ReplyTo>'
  22.     authReq +=      '      <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>&#039;
  23.     authReq +=      '    </a:ReplyTo>'
  24.     authReq +=      '    <a:To s:mustUnderstand="1">https://login.microsoftonline.com/extSTS.srf</a:To>&#039;
  25.     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;
  26.     authReq +=      '      <o:UsernameToken>'
  27.     authReq +=      '        <o:Username>' + usr + '</o:Username>'
  28.     authReq +=      '        <o:Password>' + pwd + '</o:Password>'
  29.     authReq +=      '      </o:UsernameToken>'
  30.     authReq +=      '    </o:Security>'
  31.     authReq +=      '  </s:Header>'
  32.     authReq +=      '  <s:Body>'
  33.     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;
  34.     authReq +=      '      <a:EndpointReference>'
  35.     authReq +=      '        <a:Address>' + loginUrl + '</a:Address>'
  36.     authReq +=      '      </a:EndpointReference>'
  37.     authReq +=      '      </wsp:AppliesTo>'
  38.     authReq +=      '      <t:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</t:KeyType>&#039;
  39.     authReq +=      '      <t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType>&#039;
  40.     authReq +=      '      <t:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</t:TokenType>'
  41.     authReq +=      '    </t:RequestSecurityToken>'
  42.     authReq +=      '  </s:Body>'
  43.     authReq +=      '</s:Envelope>';    
  44.  
  45.     function startScript() {
  46.       getToken();
  47.     }
  48.  
  49.     var token;
  50.     // Step 1: we get the token from the STS
  51.     function getToken()
  52.     {
  53.         $.support.cors = true; // enable cross-domain query
  54.         $.ajax({
  55.             type: 'POST',
  56.             data: authReq,
  57.             crossDomain: true, // had no effect, see support.cors above
  58.             contentType: 'application/soap+xml; charset=utf-8',
  59.             url: 'https://login.microsoftonline.com/extSTS.srf&#039;,
  60.             dataType: 'xml',
  61.             success: function (data, textStatus, result) {
  62.                 // extract the token from the response data
  63.                 // var token = $(result.responseXML).find("wsse\\:BinarySecurityToken").text(); // we should work with responseText, because responseXML is undefined, due to Content-Type: application/soap+xml; charset=utf-8
  64.                 token = $(result.responseText).find("wsse\\:BinarySecurityToken").text();
  65.                 getFedAuthCookies();
  66.             },
  67.             error: function (result, textStatus, errorThrown) {
  68.                 reportError(result, textStatus, errorThrown);
  69.             }
  70.         });
  71.     }
  72.  
  73.     // Step 2: "login" using the token provided by STS in step 1
  74.     function getFedAuthCookies()
  75.     {
  76.         $.support.cors = true; // enable cross-domain query
  77.         $.ajax({
  78.             type: 'POST',
  79.             data: token,
  80.             crossDomain: true, // had no effect, see support.cors above
  81.             contentType: 'application/x-www-form-urlencoded',
  82.             url: loginUrl,         
  83.             // dataType: 'html', // default is OK: Intelligent Guess (xml, json, script, or html)
  84.             success: function (data, textStatus, result) {
  85.                 // we should update the digest
  86.                 //refreshDigestViaWS(); // or alternatively:
  87.                 refreshDigestViaREST();
  88.             },
  89.             error: function (result, textStatus, errorThrown) {
  90.                 reportError(result, textStatus, errorThrown);
  91.             }
  92.         });
  93.     }
  94.  
  95.     var digest;
  96.  
  97.     // Step 3a: get the digest from the Sites web service and refresh the one stored locally
  98.     function refreshDigestViaWS()
  99.     {
  100.         $.support.cors = true; // enable cross-domain query
  101.         $.ajax({
  102.             type: 'POST',
  103.             data: tokenReq,
  104.             crossDomain: true, // had no effect, see support.cors above
  105.             contentType: 'text/xml; charset="utf-8"',
  106.             url: siteFullUrl + '/_vti_bin/sites.asmx',
  107.             headers: {
  108.                 'SOAPAction': 'http://schemas.microsoft.com/sharepoint/soap/GetUpdatedFormDigestInformation&#039;,
  109.                 'X-RequestForceAuthentication': 'true'
  110.             },
  111.             dataType: 'xml',
  112.             success: function (data, textStatus, result) {
  113.                 digest = $(result.responseXML).find("DigestValue").text();
  114.                 sendRESTReq();
  115.             },
  116.             error: function (result, textStatus, errorThrown) {
  117.                         var response = JSON.parse(result.responseText);
  118.                         if ((response.error != undefined) && (response.error.message != undefined)) {
  119.                             alert(response.error.message.value);
  120.                         }
  121.             }
  122.         });
  123.     }
  124.  
  125.     // Step 3b: get the digest from the contextinfo and refresh the one stored locally
  126.     function refreshDigestViaREST()
  127.     {
  128.         $.support.cors = true; // enable cross-domain query
  129.         $.ajax({
  130.             type: 'POST',
  131.             data: tokenReq,
  132.             crossDomain: true, // had no effect, see support.cors above
  133.             contentType: 'text/xml; charset="utf-8"',
  134.             url: siteFullUrl + '/_api/contextinfo',
  135.             dataType: 'xml',
  136.             success: function (data, textStatus, result) {  
  137.                 digest = $(result.responseText).find("d\\:FormDigestValue").text();
  138.                 sendRESTReq();
  139.             },
  140.             error: function (result, textStatus, errorThrown) {
  141.                         var response = JSON.parse(result.responseText);
  142.                         if ((response.error != undefined) && (response.error.message != undefined)) {
  143.                             alert(response.error.message.value);
  144.                         }
  145.             }
  146.         });
  147.     }
  148.  
  149.     // Step 4: send the REST request
  150.     function sendRESTReq() {
  151.         $.support.cors = true; // enable cross-domain query
  152.         $.ajax({
  153.             type: 'POST',   
  154.             data: JSON.stringify({
  155.                                     __metadata: { type: 'SP.List' },
  156.                                     Title: 'RESTDocLib',
  157.                                     BaseTemplate: 101
  158.                                 }),
  159.             // equivalent:       
  160.             // data: "{'__metadata': { 'type': 'SP.List' }, 'Title': 'RESTDocLib','BaseTemplate': 101}" ,
  161.             url: siteFullUrl + "/_api/web/lists",
  162.             crossDomain: true, // had no effect, see support.cors above
  163.             contentType: 'application/json;odata=verbose',
  164.              headers: {
  165.                  'X-RequestDigest': digest,
  166.                  "Accept": "application/json; odata=verbose"
  167.              },
  168.             success: function (data, textStatus, result) {  
  169.                 alert("Created");
  170.             },
  171.             error: function (result, textStatus, errorThrown) {
  172.                         var response = JSON.parse(result.responseText);
  173.                         if ((response.error != undefined) && (response.error.message != undefined)) {
  174.                             alert(response.error.message.value);
  175.                         }
  176.             }
  177.         });
  178.     }
  179.  
  180.     function reportError(result, textStatus, errorThrown) {
  181.         var response = JSON.parse(result.responseText);
  182.         if ((response.error != undefined) && (response.error.message != undefined)) {
  183.             alert(response.error.message.value);
  184.         }
  185.     }
  186.  
  187.     $(document).ready(startScript);
  188.  
  189. </script>

Note 1: The lookup of the token (in the getToken method) had to be changed. In the former version we used:

$(result.responseText).find("BinarySecurityToken").text();

in the new version we have to use:

$(result.responseText).find("wsse\\:BinarySecurityToken").text();

Note 2: I used JSON’s stringify and parse methods to (de)serialize JavaScript objects to / from text. Important experience, that this methods do not work in the Quirks mode of Internet Explorer.

April 17, 2013

How to use LINQ expressions in your JavaScript code when accessing the REST API (and other useful JavaScript libraries)

Filed under: JavaScript, jQuery, LINQ, Project Server, REST, SP 2010, SP 2013 — Tags: , , , , , , — Peter Holpar @ 04:24

Working with both C# and JavaScript to process and display information stored on Project Server, I had to realize the power of LINQ in C#, and the lack of this language feature in JavaScript. Fortunately, after a quick search I found the LINQ for JavaScript library, that enables a very similar syntax. Below I show a few examples to give you a highlight of its features.

The other handy library I use these days frequently is Datejs, that enables effective handling of the Date objects in JavaScript.

Although the code samples in this post are related to Project Server, you can apply the same technique to SharePoint as well. For the sake of simplicity I post only code snippets, and not the full code of the application. Hopefully it will be enough to demonstrate the power of these libraries.

LINQ for JavaScript

Note, that I’m using version 3.0.3-Beta4 in the samples below, and not the stable 2.2.0.2 version. There are considerable syntax differences between these versions, like lower case vs. upper case function names, so be sure the check the reference.htm coming with the actual version of your choice.

See my former post about using and limitations of the $expand query option.

In the first step I send a few REST request to the server and store the results in JavaScript object trees.

The sample below sends a REST request to the Project Server to query information, like Name, Id and ResourceCalendarExceptions properties of the enterprise resources as well as the Name of their BaseCalendar property. We need the response as a JSON stream, so we set the Accept header to ‘application/json; odata=verbose‘. If the call was successful, the result is stored in the resources variable.

var siteFullUrl = ‘http://yourProjectSite&#8217;;

var resources;
var calendars;
var assignments;

$.ajax({
                type: ‘GET’, 
                contentType: ‘application/json;odata=verbose’,
                url: siteFullUrl + "/sites/pwa/_api/ProjectServer/EnterpriseResources?$select=Name,Id,ResourceCalendarExceptions,BaseCalendar/Name&$expand=ResourceCalendarExceptions,BaseCalendar/Name",
                headers: {
                    ‘X-RequestDigest’: $(‘#__REQUESTDIGEST’).val(),
                    ‘Accept’: ‘application/json; odata=verbose’
                },
                dataType: ‘json’,
                complete: function (result) {
                    var response = JSON.parse(result.responseText);
                    if (response.error) {
                        alert("Error: " + response.error.code + "\n" + response.error.message.value);
                    }
                   else {
                       resources = response.d.results;
                   }
});

We can request the enterprise calendar data (name and exceptions) using a similar call of jQuery AJAX method, but in this case the URL of the REST query looks like this one below:

"/sites/pwa/_api/ProjectServer/Calendars?$select=Name,BaseCalendarExceptions&$expand=BaseCalendarExceptions"

and we store the response in the calendars variable.

calendars = response.d.results;

To to get the assignments for March I submitted a REST request with the following URL:

"/sites/pwa/_api/ProjectData/Assignments?$select=ResourceId,ProjectName,AssignmentStartDate,AssignmentFinishDate,AssignmentBookingId&$filter=AssignmentStartDate+le+datetime’2013-03-31′ and AssignmentFinishDate+ge+datetime’2013-03-01’"

and stored the results in the assignments variable:

assignments = response.d.results;

Having all of the responses on the client side, we can process them using a code like this one:

// default enterprise base calendar
var wdExcs = calendars[0].BaseCalendarExceptions.results;

$.each(resources, function (index) {
        // get the calendar exceptions of the current resource
        var wdExcsPriv = this.ResourceCalendarExceptions.results;
        var resId = this.Id;

        // get all assignments of the current resource
        var assignmentsPriv = Enumerable.from(assignments).where(function(x) { return (x.ResourceId == resId); }).toArray();

        // get the first day of the current month, that was 1st of March at the time of test
        var day = Date.today();
        var day = day.moveToFirstDayOfMonth();
        var dayOfWeek = day.getDay();

        var assignmentsForThisDay = Enumerable.from(assignmentsPriv).where(function(x) { return (new Date(parseInt(x.AssignmentStartDate.substr(6))) <= day) && (new Date(parseInt(x.AssignmentFinishDate.substr(6))) >= day) }).toArray();

        // is it a weekend, a base calendar exception (like state holidays) or a resource exception (private holiday)?
        var isHoliday = ((dayOfWeek == 0) || (dayOfWeek == 6)
          || (Enumerable.from(wdExcs).any(function(x) { return (Date.parse(x.Start) <= day) && (Date.parse(x.Finish) >= day) }))
          || (Enumerable.from(wdExcsPriv).any(function(x) { return (Date.parse(x.Start) <= day) && (Date.parse(x.Finish) >= day) })));

        // is there at least a single committed assignment or only proposed ones?
        var isBooked = (Enumerable.from(assignmentsForThisDay).any(function(x) { return (x.AssignmentBookingId==0); }));

        // aggregate (join) the assignment names
        var projects = (Enumerable.from(assignmentsForThisDay).select("$.ProjectName")).join();

});

Datejs

You can see Datejs in action in the previous code snippet, like the call of the moveToFirstDayOfMonth function above.

It has a lot of other useful features, for example, to get the number of days in the current month:

var month = today.getMonth();
var year = today.getFullYear();
var dayCount = Date.getDaysInMonth(year, month);

The getDayDiff function (requires time.js) returns the number of days between two dates:

function getDayDiff(time1, time2) {
  var timeSpan = new TimeSpan(time1 – time2);
  var dayCount = timeSpan.getDays() ;
  return dayCount;
}

The next snippet sets the variable firstDay to the first day of the current month, and lastDay to the last day of the following month:

var firstDay = Date.today();
firstDay.moveToFirstDayOfMonth();

var lastDay = Date.today().add(1).month();
lastDay.moveToLastDayOfMonth();

I’ve been working with these libraries for a few weeks now, and I can say they made my coding more efficient, so I think they worth a try if you have to work with complex REST responses and Date objects from JavaScript, that is very likely if you would like to implement some exciting SharePoint applications based on the new app model.

March 28, 2013

Limitations of the $expand REST query option when working with Project Server OData services

Filed under: Project Online, PS 2013, REST — Tags: , , — Peter Holpar @ 13:43

Recently I prepared a REST request for the ProjectData service of Project Server. I assembled the following simple query using LINQPad :

from a in Assignments
select new
{
  a.Resource.ResourceCanLevel,
  a.ProjectName,
  a.ResourceName
}

LINQPad sent the following query to the server:

http://yourProjectSite/_api/ProjectData/Assignments()?$expand=Resource&$select=Resource/ResourceCanLevel,ProjectName,ResourceName

The server responded with:

Unsupported query option encountered: $expand

I tried to submit a similar query, but specified exactly the property I needed (used Resource/ResourceCanLevel in place of former value Resource) in the $expand option:

http://yourProjectSite/_api/ProjectData/Assignments?$select=Resource/ResourceCanLevel,ResourceName,ProjectName&$expand=Resource/ResourceCanLevel

The response did not change.

Then – as a last desperate try – I’ve tried to simply omit the $expand option, like this:

http://yourProjectSite/_api/ProjectData/Assignments?$select=Resource/ResourceCanLevel,ResourceName,ProjectName

Of course, I’ve received this message:

Only properties specified in $expand can be traversed in $select query options

The reason and explanation for this behavior I found as a note on MSDN:

The ProjectData service does not implement the $links query option or the $expand query option. Excel 2013 internally uses the Association elements and the AssociationSet elements in the entity data model to help create associations between entities, for pivot tables and other constructs.”

Bad enough… The good news: the ProjectServer service (_api/ProjectServer) has no such limitation.

March 8, 2013

Accessing Office 365 REST services using LINQPad

LINQPad is a great tool, even for a SharePoint developer when working with the RESTful web services. However, it does not provide an authentication mechanism against Office 365, a major issue when there is no on-premise SharePoint or Project Server at your hand to develop and test your queries (as suggested by Andrew Lavinsky in this post), as illustrated by the figures below.

We add a new connection of type WCF Data Service 5.1 (OData 3) to LINQPad:

image

Specify the URI of the ListData.svc at our  O365 tenant, and Default (XML) as Formatter.

image

Then we receive the following error (you can try to specify username and password in the previous step, but it makes no difference):

Error: The remote server returned an error: (403) Forbidden.

image

I’ve found a workaround for this issue on the web, but for me it was so complex at the first sight (even though I later understood how it should work), so I decided to find another way, using my other favorite tool Fiddler.

Our “solution”: we will “cache” the authentication cookies from an Internet Explorer session, then inject the same cookies to the LINQPad sessions.

Start Fiddler, choose Roles / Customize Rules…, and edit the CustomRules.js file (don’t forget to create a backup!).

Before the OnBeforeRequest function add this code:

static var authCookies = "";
static var o365Site = "yourO365Site.sharepoint.com"; // modify this value!

At the beginning of the OnBeforeRequest function add this code:

if (oSession.HostnameIs(o365Site)) {
  var cookie = oSession.oRequest["Cookie"];
  if ((cookie == "") && (authCookies != "")) {
    //oSession.oRequest["Accept"] = "text/html, application/xhtml+xml, */*";
    oSession.oRequest["Cookie"] = authCookies;
  }
}

At the beginning of the OnBeforeResponse function add this code:

if (oSession.HostnameIs(o365Site)) {
  var cookie = oSession.oRequest["Cookie"];
  if (cookie != "") {
    authCookies = cookie;
  }
}

Done! Save the changes of CustomRules.js. Then (having Fiddler running and capturing network traffic!) start IE, navigate to your O365 site, and authenticate yourself when requested. Cookies are cached in Fiddler at this point.

Note: In my development environment I always enable Fiddler to decrypt HTTPS traffic. I have not tested this solution with decryption disabled, and have doubts, if it should work. If you test it, please, leave us a comment with the results.

image

In the next step (the same Fiddler instance is still running and capturing network traffic!), try to reconnect LINQPad to the same O365 site. Cookies are replayed by Fiddler, authentication in LINQPad should work this time.

image

To test the functionality, I submitted a simple query:

image

So far the good news. After “solving” the authentication issue, let’s see a further problem, and that is bound to the (missing) $metadata support of Microsoft’s OData implementation in SP 2013.

As you might have noticed, in the example above I used the “old-style”, SP 2010 compatible version of the REST API (_vti_bin/ListData.svc), and not the “new-school” format, including _api (like _api/web/), and that is no just accidentally.

Since LINQPad needs the $metadata to build up the object structure, it simply does not work without that:

In LINQPad:

Error: The remote server returned an error: (404) Not Found.

image

In Fiddler (HTTP status 404):

Cannot find resource for the request $metadata.

One of the workarounds may be (again with Fiddler) to use an existing beta installation of SP 2013, capture the response for the $metadata request to a file, then in the development environment send it as a response for the $metadata request from LINQPad automatically, but it is rather hacky, even for me. In my opinion it simply does not worth monkeying so much with that, we should rather learn and use the syntax of the OData requests.

Developers (including myself) who need to work against Project Online are luckier. Although the OData service of PS seems to be available only through the new _api interface, the $metadata support is still there in this case.

image

And a sample query:

image

Have fun using LINQPad against your O365 site and Project Online!

UPDATE: The same trick can be applied, when we would like to add a service reference to our Visual Studio project, referring to an O365 / Project Online site (just like described here for an on-premise PS). Although VS 2012 displays an authentication dialog for my – in this case German – O365 site (it is not the case with VS 2010),

image

it seems to have no effect (at least, not always?):

image

Note, that based on the error details it is likely not just a simple authentication issue, as VS would like to append /_vti_bin/ListData.svc to the service URL.

In this case you can use Fiddler again to replay the cookies and authenticate on behalf of VS:

image

UPDATE 2 (3/22/2013): I was wrong when I wrote that developers working against Project Online were much luckier. I’ve just realized, that although $metadata is really available for the ProjectData service, it is not supported for the ProjectServer service, that you should use to access entities like enterprise resources, calendars or custom fields (as illustrated by the next screenshot, requesting https://yourProjects.sharepoint.com/sites/pwa/_api/ProjectServer).

image

In LINQPad:

Error: The remote server returned an error: (404) Not Found.

image

In Fiddler (HTTP status 404):

Cannot find resource for the request $metadata.

February 24, 2013

Favorites in the Cloud: Implementing a Windows 8 Search application with An Office 365 backend

In the previous part of this post I illustrated, how we can share links from IE 10 on O365. For general background info on the subject I suggest you to read that part first. In this part I provide the code for an app that helps to look up the favorites and to open them in IE.

Just like in the former part, I used a sample project from the Windows 8 SDK as a prototype of the development, and concentrate on the SharePoint-specific code only. In this case we use the Search contract sample\JavaScript solution as the framework of the development.

The solution

After opening the Search contract sample solution in Visual Studio 2012, we have to make a few minor modifications in the code, and a major one to achieve the goals.

First, open the sample-utils.js file, look up the ScenarioOutput control, and append the following code snippet at the end of its declaration, after _addStatusOutput (don’t forget the comma separator!):

,
_addResultsOutput: function (element) {
    var resultsDiv = document.createElement("div");
    resultsDiv.id = "results";
    element.insertBefore(resultsDiv, element.childNodes[0]);
}

This div serves as the content placeholder when displaying the results.

In the default.js file we should first find this code block:

// Scenario 1 : Support receiving a search query while running as the main application.
Windows.ApplicationModel.Search.SearchPane.getForCurrentView().onquerysubmitted = function (eventObject) {
    WinJS.log && WinJS.log("User submitted the search query: " + eventObject.queryText, "sample", "status");
};

and replace it with this one (changes are highlighted with yellow):

var query;
// Scenario 1 : Support receiving a search query while running as the main application.
Windows.ApplicationModel.Search.SearchPane.getForCurrentView().onquerysubmitted = function (eventObject) {
    WinJS.log && WinJS.log("User submitted the search query: " + eventObject.queryText, "sample", "status");
    query = eventObject.queryText;
    querySPLinks();

};

Next, add this larger code snippet at the end (but before the closing braces!) of the default.js, and update the credential and the site URL. In a real-world app you should of course prompt for the credentials and optionally store them in a secure location.

Code Snippet
  1. // update these values to match your site and credentials
  2. var usr = 'username@yoursite.onmicrosoft.com';
  3. var pwd = 'password';
  4. var siteFullUrl = "https://yoursite.sharepoint.com&quot;;
  5. var linkListName = "SharedLinks";
  6.  
  7. var tokenReq = '<?xml version="1.0" encoding="utf-8"?>';
  8. 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;;
  9. tokenReq += '  <soap:Body>';
  10. tokenReq += '    <GetUpdatedFormDigestInformation xmlns="http://schemas.microsoft.com/sharepoint/soap/&quot; />';
  11. tokenReq += '  </soap:Body>';
  12. tokenReq += '</soap:Envelope>';
  13.  
  14. var loginUrl = siteFullUrl + "/_forms/default.aspx?wa=wsignin1.0";
  15. 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;
  16. authReq += '  <s:Header>'
  17. authReq += '    <a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Action>&#039;
  18. authReq += '    <a:ReplyTo>'
  19. authReq += '      <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>&#039;
  20. authReq += '    </a:ReplyTo>'
  21. authReq += '    <a:To s:mustUnderstand="1">https://login.microsoftonline.com/extSTS.srf</a:To>&#039;
  22. 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;
  23. authReq += '      <o:UsernameToken>'
  24. authReq += '        <o:Username>' + usr + '</o:Username>'
  25. authReq += '        <o:Password>' + pwd + '</o:Password>'
  26. authReq += '      </o:UsernameToken>'
  27. authReq += '    </o:Security>'
  28. authReq += '  </s:Header>'
  29. authReq += '  <s:Body>'
  30. 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;
  31. authReq += '      <a:EndpointReference>'
  32. authReq += '        <a:Address>' + loginUrl + '</a:Address>'
  33. authReq += '      </a:EndpointReference>'
  34. authReq += '      </wsp:AppliesTo>'
  35. authReq += '      <t:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</t:KeyType>&#039;
  36. authReq += '      <t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType>&#039;
  37. authReq += '      <t:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</t:TokenType>'
  38. authReq += '    </t:RequestSecurityToken>'
  39. authReq += '  </s:Body>'
  40. authReq += '</s:Envelope>';
  41.  
  42. function querySPLinks() {
  43.     // clear former results before submitting the new query
  44.     var resultsDiv = document.getElementById("results");
  45.     while (resultsDiv.childNodes.length > 0) {
  46.         var child = resultsDiv.childNodes[0];
  47.         resultsDiv.removeChild(child);
  48.     }
  49.     getToken();
  50. }
  51.  
  52. // Step 1: we get the token from the STS
  53. var token;
  54. function getToken() {
  55.     WinJS.xhr({
  56.         url: "https://login.microsoftonline.com/extSTS.srf&quot;,
  57.         type: 'POST',
  58.         data: authReq,
  59.         headers: { 'Content-type': 'application/soap+xml; charset=utf-8' }
  60.     }).done(
  61.     function fulfilled(result) {
  62.         // extract the token from the response data
  63.         token = result.responseXML.querySelector("BinarySecurityToken").textContent;
  64.         getFedAuthCookies();
  65.     },
  66.     function errHandler(err) {
  67.         var e = err;
  68.     });
  69. }
  70.  
  71. // Step 2: "login" using the token provided by STS in step 1
  72. function getFedAuthCookies() {
  73.     WinJS.xhr({
  74.         url: loginUrl,
  75.         type: 'POST',
  76.         data: token,
  77.         headers: { 'Content-type': 'application/x-www-form-urlencoded' }
  78.     }).done(
  79.     function fulfilled(result) {
  80.         refreshDigest();
  81.     },
  82.     function errHandler(err) {
  83.         var e = err;
  84.     });
  85. }
  86.  
  87. // Step 3: get the digest from the Sites web service and refresh the one stored locally
  88. var digest;
  89. function refreshDigest() {
  90.     WinJS.xhr({
  91.         url: siteFullUrl + '/_vti_bin/sites.asmx',
  92.         type: 'POST',
  93.         headers: {
  94.             'SOAPAction': 'http://schemas.microsoft.com/sharepoint/soap/GetUpdatedFormDigestInformation&#039;,
  95.             'X-RequestForceAuthentication': 'true',
  96.             'Content-type': 'text/xml; charset=utf-8'
  97.         },
  98.         data: tokenReq
  99.     }).done(
  100.    function fulfilled(result) {
  101.        digest = result.responseXML.querySelector("DigestValue").textContent;
  102.        sendRESTReq();
  103.    },
  104.    function errHandler(err) {
  105.        resportError(err);
  106.    });
  107. }
  108.  
  109. // Step 4: execute the REST request
  110. function sendRESTReq() {
  111.     WinJS.xhr({
  112.         url: siteFullUrl + "/_api/web/lists/GetByTitle('"+ linkListName + "')/items?$select=URL",
  113.         type: 'GET',
  114.         headers: {
  115.             'X-RequestDigest': digest,
  116.             "Accept": "application/json; odata=verbose",
  117.             'Content-type': 'application/json;odata=verbose'
  118.         }
  119.     }).done(
  120.    function fulfilled(result) {
  121.        var response = JSON.parse(result.responseText);
  122.        var links = response.d.results;
  123.        var resultsDiv = document.getElementById("results");
  124.        for (var i = 0; i < links.length; i++) {
  125.            var link = links[i];
  126.            var desc = link.URL.Description;
  127.            var url = link.URL.Url;
  128.            // we make the comparision on the client side,
  129.            // and build the UI for the links dynamically
  130.            if (desc.toLowerCase().indexOf(query.toLowerCase()) !== -1) {
  131.                var aLink = document.createElement("a");
  132.                var br = document.createElement("br");
  133.                aLink.id = "link" + i;
  134.                aLink.innerText = desc;
  135.                aLink.href = url;
  136.                resultsDiv.appendChild(aLink);
  137.                resultsDiv.appendChild(br);
  138.            }
  139.        }
  140.    },
  141.    function errHandler(err) {
  142.        var e = JSON.parse(err.responseText);
  143.        reportErrorMsg("Error: " + e.error.code + "\n" + e.error.message.value);
  144.    });
  145. }
  146.  
  147. function reportError(msg) {
  148.     if (e.message != undefined) {
  149.         reportErrorMsg(e.message);
  150.     }
  151.     else if (e.statusText != undefined) {
  152.         reportErrorMsg(e.statusText);
  153.     }
  154.     else {
  155.         reportErrorMsg("Error");
  156.     }
  157. }
  158.  
  159. function reportErrorMsg(msg) {
  160.     WinJS.log(msg, "sample", "error");
  161. }

How does it work?

The authentication mechanism against O365 is the same that I applied in my former post, and we use REST and WinJS.xhr as in the former part of this post.

Instead of selecting the matching items only using CAML, we load all items from the list to the client side, and filter the items in the client app. To limit the bandwidth usage we limit the scope of requested data to the URL property, that includes both the URL and the title of the link.

_api/web/lists/GetByTitle(‘SharedLinks’)/items?$select=URL

I found that if one would like to submit a REST request that includes a field of type URL as a filter, like the query below, a HTTP 400 status is returned by the server. However, when using IE alone to submit the query, we got no info on the reason.

_api/web/lists/GetByTitle(‘SharedLinks’)/items?$select=URL&$filter=startswith(URL,’code’)

First, when monitoring with Fiddler, could we recognize, that an XML document is returned as the body of the response with an error message:

The field ‘URL’ of type ‘URL’ cannot be used in the query filter expression.

Sad, but true.

BTW, using the following two images I illustrate, how the format of the fields of type URL has been changed in the response from the SP2010-style REST (_vti_bin/ListData.svc) to the SP2013-style (_vti_bin/client.svc or simply _api).

_vti_bin/ListData.svc/SharedLinks

image

_api/web/lists/GetByTitle(‘SharedLinks’)/items

image

Testing the search app

After deploying the app, in Window 8 you can activate the Search charm using the Windows + Q shortcut. You should select the Search contract JS sample from the available list, then provide the query term, for example, “project”, and submit the query.

image

The results will be displayed in the Output section of the app. Users can open the links in IE 10 simply by a click.

image

This app and the former one illustrate, that it is rather easy to integrate the cloud based SharePoint with your custom Windows 8 apps using the new, enhanced REST API. Having this type of extensions one can already organize the favorites in IE.

Favorites in the Cloud: Implementing a Windows 8 Share Target application with An Office 365 backend

I don’t know if it is just me, but a considerable part of my “knowledge” on SharePoint (and on other topics as well) is stored as links to interesting articles and nice blog posts. Whenever I need to refresh my memory on a specific topic, I know which texts I should read to get into the context as quick as possible. My friends receive typically a batch of links to such must-reads when they ask my help on a theme.

Organizing the links in a way that enables finding the right ones easily is an art and science of its own. The built-in tools for organizing and finding links (e.g. favorites) in Internet Explorer are rather limited, for example, you can store a link only in a single location (folder), no tagging, no rating, etc. A custom IE add-in can definitely make one’s life easier and the work more effective there.

My pain – The (very) limited favorites in IE 10 (Metro)

When you work with the touch-optimized (a.k.a Metro) version of IE in Windows 8, you have even less out-of-the-box options to handle your links.

You can pin the current page to he favorites.

image

When you activate the URL text box, the favorites are displayed as tiles. The built-in favorites UI seems to be not optimized for hundreds of links. There is no way to organize the links into folders or search them, at least I found no option to manage, but to remove the selected one.

image

You are not allowed to install add-ins in this version of IE, so how to tweak these limitations?

The solution

The simplest way I found to extend the default features is implementing a Share Target application to store your links in a backend system, and a Search application to look up the links. This backend system could be the file system, a database, or even O365.

Overview of the idea

In Windows 8 IE acts as a Share Source application. We should create two Windows 8 apps. First app is a Share Target that uses the Windows 8 Share charm to enable users to store the actual visited page as a link in SharePoint online. The second app lets the users to search the saved links using the Search charm by participating in the Search contract.

In this post I show you a proof-of-concept of a JavaScript application that acts as a Share Target for links and stores them in a Links list on a O365 site, the Search app will be the theme of a next post. As general in the case of POC apps, I concentrate on the main issue, that is interacting with O365 from a JS W8 app, and other – also important – issues are ignored for the sake of simplicity. For example, we store user name and password hardcoded in the app. In a real-world app it is a “worst practice”, you should prompt the user for the credentials and optionally store them in a secure location. Error handling in this app is also very lightly implemented.

As an introduction to the theme of developing Share Target applications you can read this article on MSDN.

If you don’t have it yet, you should download the Windows 8 SDK sample applications from here. I use one of the sample apps (Sharing content target app sample\JavaScript) as the boilerplate of the development of my Share Target app.

But before launching Visual Studio, I prepared the storage place for my links. On my O365 Developer Site I created a new Links list,

image

and named it SharedLinks:

image

That’s all about preparation, let’s start Visual Studio 2012 on W8, and open the JS version of the Sharing content target app sample solution!

In target.html, look up the code for button reportCompleted:

<button id="reportCompleted">Report Completed</button>

and insert this snippet before that text:

<div>
&nbsp;<button id="shareWithO365">Share on O365</button>
</div>
<br />

In target.js, first extend the inititalize function with this line of code to register the event handler method for our new button:

document.getElementById("shareWithO365").addEventListener("click", shareWithO365, false);

then append the following code at the end of the file (but before the closing braces!), and update the credential and the site URL:

Code Snippet
  1. var tokenReq = '<?xml version="1.0" encoding="utf-8"?>';
  2. 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;;
  3. tokenReq += '  <soap:Body>';
  4. tokenReq += '    <GetUpdatedFormDigestInformation xmlns="http://schemas.microsoft.com/sharepoint/soap/&quot; />';
  5. tokenReq += '  </soap:Body>';
  6. tokenReq += '</soap:Envelope>';
  7.  
  8. // update these values to match your site and credentials
  9. var usr = 'username@yoursite.onmicrosoft.com';
  10. var pwd = 'password';
  11. var siteFullUrl = "https://yoursite.sharepoint.com&quot;;
  12. var linkListName = "SharedLinks";
  13.  
  14. var loginUrl = siteFullUrl + "/_forms/default.aspx?wa=wsignin1.0";
  15. 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;
  16. authReq += '  <s:Header>'
  17. authReq += '    <a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Action>&#039;
  18. authReq += '    <a:ReplyTo>'
  19. authReq += '      <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>&#039;
  20. authReq += '    </a:ReplyTo>'
  21. authReq += '    <a:To s:mustUnderstand="1">https://login.microsoftonline.com/extSTS.srf</a:To>&#039;
  22. 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;
  23. authReq += '      <o:UsernameToken>'
  24. authReq += '        <o:Username>' + usr + '</o:Username>'
  25. authReq += '        <o:Password>' + pwd + '</o:Password>'
  26. authReq += '      </o:UsernameToken>'
  27. authReq += '    </o:Security>'
  28. authReq += '  </s:Header>'
  29. authReq += '  <s:Body>'
  30. 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;
  31. authReq += '      <a:EndpointReference>'
  32. authReq += '        <a:Address>' + loginUrl + '</a:Address>'
  33. authReq += '      </a:EndpointReference>'
  34. authReq += '      </wsp:AppliesTo>'
  35. authReq += '      <t:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</t:KeyType>&#039;
  36. authReq += '      <t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType>&#039;
  37. authReq += '      <t:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</t:TokenType>'
  38. authReq += '    </t:RequestSecurityToken>'
  39. authReq += '  </s:Body>'
  40. authReq += '</s:Envelope>';
  41.  
  42. function shareWithO365() {
  43.     // start a long-running share operation
  44.     reportStarted();
  45.     getToken();
  46. }
  47.  
  48. // Step 1: we get the token from the STS
  49. var token;
  50. function getToken() {
  51.     WinJS.xhr({
  52.         url: "https://login.microsoftonline.com/extSTS.srf&quot;,
  53.         type: 'POST',
  54.         data: authReq,
  55.         headers: { 'Content-type': 'application/soap+xml; charset=utf-8' }
  56.     }).done(
  57.     function fulfilled(result) {
  58.         // extract the token from the response data
  59.         token = result.responseXML.querySelector("BinarySecurityToken").textContent;
  60.         getFedAuthCookies();
  61.     },
  62.     function errHandler(err) {
  63.         reportErrorEx(err);
  64.     });
  65. }
  66.  
  67. // Step 2: "login" using the token provided by STS in step 1
  68. function getFedAuthCookies() {
  69.     WinJS.xhr({
  70.         url: loginUrl,
  71.         type: 'POST',
  72.         data: token,
  73.         headers: { 'Content-type': 'application/x-www-form-urlencoded' }
  74.     }).done(
  75.     function fulfilled(result) {
  76.         refreshDigest();
  77.     },
  78.     function errHandler(err) {
  79.         reportErrorEx(err);
  80.     });
  81. }
  82.  
  83. // Step 3: get the digest from the Sites web service and refresh the one stored locally
  84. var digest;
  85. function refreshDigest() {
  86.     WinJS.xhr({
  87.         url: siteFullUrl + '/_vti_bin/sites.asmx',
  88.         type: 'POST',
  89.         headers: {
  90.             'SOAPAction': 'http://schemas.microsoft.com/sharepoint/soap/GetUpdatedFormDigestInformation&#039;,
  91.             'X-RequestForceAuthentication': 'true',
  92.             'Content-type': 'text/xml; charset=utf-8'
  93.         },
  94.         data: tokenReq
  95.     }).done(
  96.    function fulfilled(result) {
  97.         digest = result.responseXML.querySelector("DigestValue").textContent;
  98.         sendRESTReq();
  99.    },
  100.    function errHandler(err) {
  101.        reportErrorEx(err);
  102.    });
  103. }
  104.  
  105. // Step 4: execute the REST request
  106. function sendRESTReq() {
  107.     var title = document.getElementById("title").innerText;
  108.     var url = document.getElementById("description").innerText;
  109.  
  110.     WinJS.xhr({
  111.         url: siteFullUrl + "/_api/web/lists/GetByTitle('" + linkListName + "')/items",
  112.         type: 'POST',
  113.         headers: {
  114.             'X-RequestDigest': digest,
  115.             "Accept": "application/json; odata=verbose",
  116.             'Content-type': 'application/json;odata=verbose'
  117.         },
  118.         data: '{"__metadata":{"type":"SP.Data.' + linkListName + 'ListItem"},"URL": {"Description": "' + title + '", "Url": "' + url + '"}}'
  119.     }).done(
  120.    function fulfilled(result) {
  121.        // long-running share operation completed
  122.        reportCompleted();
  123.    },
  124.    function errHandler(err) {
  125.        var respText = JSON.parse(err.responseText);
  126.        reportErrorMsg("Error: " + respText.error.code + "\n" + respText.error.message.value);
  127.    });
  128. }
  129.  
  130. function reportErrorEx(e) {
  131.     if (e.message != undefined) {
  132.         reportErrorMsg(e.message);
  133.     }
  134.     else if (e.statusText != undefined) {
  135.         reportErrorMsg(e.statusText);
  136.     }
  137.     else {
  138.         reportErrorMsg("Error");
  139.     }
  140. }
  141. function reportErrorMsg(msg) {
  142.     document.getElementById("extendedShareErrorMessage").value = msg;
  143.     // long-running share operation failed
  144.     reportError();
  145. }

How does it work?

The authentication mechanism against O365 is pretty the same that I applied in my former post, however I chose REST instead of the ECMAScript OM this time (see reasons below), and instead of jQuery and its ajax method we should go with WinJS.xhr in the W8 app.

You can read more about REST in SP 2013 here, and WinJS.xhr is documented here.

Note: When working with the SharePoint REST API, you should officially get the digest token through the contextinfo operator (short described here), as illustrated by the code bellow. However, I found no difference between this approach, and using the Sites web service as earlier.

Code Snippet
  1. function getRESTDigest() {
  2.     WinJS.xhr({
  3.         url: siteFullUrl + '/_api/contextinfo', // or '/lists/SharedLinks/_api/contextinfo'
  4.         type: 'POST'
  5.     }).done(
  6.    function fulfilled(result) {
  7.        digest = result.responseXML.querySelector("FormDigestValue").textContent;
  8.        sendRESTReq();
  9.    },
  10.    function errHandler(err) {
  11.        reportErrorEx(err);
  12.    });
  13. }

Testing the share app

When the users would like to share a link from IE, they could press Windows + H to open the Share charm, and then click on the Share Target JS sample to activate our sharing app.

image

When the user clicks on the Share on O365 button, we start a long-running share operation, and authenticates the app against O365, then save the link using the REST API.

image

If there is no error, we report completed for the share operation (see fulfilled function in sendRESTReq), and you should see the new shared link in the SharedLinks list on O365 short after clicking on the Share on O365 button.

image

However, if there is an error, we report the failure (see reportErrorEx and reportErrorMsg functions), and you should see a notification popup.

image

In this case, at the bottom of the Share charm appears a similar warning. By opening it, you can see the exact details of the failure.

image

As you can see, this time I specified a non-existing list name to emulate an error condition.

image

Why JavaScript/HTML?

To tell the truth, the main reason is rather selfish: I was to learn so much new things as possible, and after the first experiments I found the C# solution simply less exciting / trendy (although it was far from trivial as well), while JavaScript/HTML promised a lot to discover. The secondary reason was that I hoped a more platform-(or device)-independent result (at least, in the context of W8, WP8, W8RT), however I have to say, that after reading more on the compatibility issues between these devices I am not sure I achieved that goal. But at least, I tried…

Should you find this solution trivial and look for even more challenges, you can read my comments below regarding the ECMAScript OM.

Why not the JavaScript / ECMAScript Object Model?

To tell the truth, it was my first idea to use the ECMAScript OM solution from my former post to implement the Share Target because of the simplicity and the broad API support of the OM, and although it was not trivial, I was able to create an application that works. There are however reasons, not to choose this way if you would not like to get a lot of troubles and support issues.

Main problems
  • Windows 8 apps are not allowed to reference external scripts, all script files have to be part of the solution. You can install the SharePoint Foundation 2010 Client Object Model Redistributable or SharePoint Server 2013 Client Components SDK Preview, and find most of the scripts at the SharePoint Client Components\Scripts folder in your Program Files directory. Based on the info in the redist.txt, you are allowed to include this files in your application. However, there are other important .js files (like SP.Core.js) that are not included in the package, but required by the ECMAScript OM runtime. These files can be downloaded from O365 and can be attached to the project, although I found no explicit statement that you are granted to use the files such way.
  • ECMAScript OM meant to be used only in the context of a page downloaded from the SharePoint server, and not in the context of an external HTML application. Although we can resolve the technical difficulties involved in the external usage (see my samples for O365 and for on-premise), that is definitely not a supported scenario.

So far so good, we was able to achieve our goal using REST, but what happens, if you need to access resources that are not yet supported by this API (for example, the taxonomy service)? It seems you are out of luck in this case. You can either implement an unsupported solution hacking with the ECMAScript OM (not recommended!), create your wrapper services (and deploy them, for example to Azure; pretty overcomplicated for a simply task in my opinion), or simple forget it / wait for the REST support.

How to retrieve our favorites then?

In this post we saw how to store the links on O365 from our app. In the next part I provide an example of looking-up our favorites and opening them in IE.

« Newer Posts

Blog at WordPress.com.