Second Life of a Hungarian SharePoint Geek

April 8, 2014

Mysterious “File Not Found” Error When Working With the ECMAScript Client Object Model

Recently I was debugging a very simple JavaScript in Internet Explorer 9 with the F12 Developer Tools, when received a “File Not Found” error. In the script I tried to open a site via

webToCheck = context.get_site().openWeb(‘/Subweb’);

and it seemed as the site did not exist. However, the same site could be opened from the Managed Client Object Model.

I launched Fiddler to check what happens in the background and was surprised to see another web site (let’s call it SubwebWrong) in the request. This web site really did not exist, so the response of the server was reasonable.

image

But where did that site name came from? I found a former entry in the Watch window of the Developer Tools, that referred to this site:

webToCheck = context.get_site().openWeb(‘/SubwebWrong’);

So it seems that this command was executed automatically by IE, that caused the script to malfunction. Pretty strange behavior, indeed.

During debugging I received an erratic “Access denied. You do not have permission to perform this action or access this resource.” error as well. It happened typically after working for a longer time on the same page. I assume that the reason is that the form digest on the page was timed out.

March 5, 2013

Deleting files from the IE cache as part of the Visual Studio deployment process

Filed under: CKS.Dev, Internet Explorer, SP 2010, Visual Studio, VSX — Tags: , , , , — Peter Holpar @ 21:44

In the past months I had again a lot to do with client side SharePoint development, that means in practice mainly projects including ribbon extensions and tons of JavaScript files. One of the issues with that type of development is the bad habit of Internet Explorer called caching. For example, when you make modifications to the .js files, and re-deploy your project, it is rather common, that IE executes the former versions of the scripts, that I find pretty annoying.

Some of the workarounds I found for that issue in the past years:

Option Nr.1: Disable caching in IE (Internet options / General / Browsing history / Settings). It is OK if you use your environment only for development (like a Dev-VM), but not optimal if you should use the same browser to fulfill your other daily tasks.

image

Option Nr.2: Type the full URL of the script in the address text box of IE, download a local copy of the script to a temporary folder, and refresh the page with Ctrl+F5. Rather cumbersome method, especially if you have to refresh several scripts .

Option Nr.3: Open the location of the cache (Internet options / General / Browsing history / Settings / View files, marked with blue on the screenshot above), and delete the file(s) manually. The location in the file system is typically C:\Users\UserName\AppData\Local\Microsoft\Windows\Temporary Internet Files. Works quite good, but it takes some time and is still something you can forget.

Wouldn’t it be more comfortable to automate the process, for example, as a deployment step in Visual Studio? Definitely, but how to achieve that? Well, deleting files from the IE cache programmatically is far from being straightforward, but fortunately I found a nice example on MSDN for a sample wrapper class in C#, including a lot of PInvoke calls. Having this solution, the Visual Studio Extensibility part of the exercise was a routine task.

My first idea was to contribute this VS extension to the CKS.Dev project on CodePlex (see some posts on my other contributions to CKS.Dev here), but the project is just being upgraded to SharePoint 2013 / Visual Studio 2012, so I extended my own extension (documented here) instead.

Expected functionality:

I would like to specify through Visual Studio project properties, which files should be deleted from the cache (e.g. from the Temporary Internet Files folder).

First, I introduce a new property called IE cache delete rule that determines the scope of the action and has the following possible values:

Only from current site – only matching files cached from the active SharePoint site (the one you specified as the target of the deployment in VS) are deleted.

All from current sites – all files cached from the active SharePoint site are deleted, other filters are ignored.

No site specific – all cached files that fulfill the filters are deleted, independently from the origin URL.

Filters (there is a logical OR between the filters that means a cached file should fulfill at least one of the conditions to be deleted):

IE cache file list – It is a list of the names of the files to be deleted from the cache.

IE cache file pattern – It is a regular expression pattern. All files matching the pattern will be deleted from the cache, as long as it also matches the scope of the action (see IE cache delete rule property above)

Let’s have a look at the implementation!

My original SPVSProjectProps class was extended with the new properties:

Code Snippet
  1. // IECacheFilePattern property related members
  2. private const string IECacheFilePatternPropertyId = "IECacheFilePattern";
  3. private const string IECacheFilePatternPropertyDefaultValue = "";
  4.  
  5. [DisplayName("IE cache file pattern")]
  6. [DescriptionAttribute("This property specifies a regular expression pattern to determine which files should be deleted from the IE cache when the Clear IE cache deployment step is activated")]
  7. [DefaultValue(IECacheFilePatternPropertyDefaultValue)]
  8. // we want our custom property to show up in the SharePoint section in the project properties property grid
  9. [Category("SharePoint")]
  10. public string IECacheFilePattern
  11. {
  12.     get
  13.     {
  14.         string propertyValue;
  15.         int hr = projectStorage.GetPropertyValue(IECacheFilePatternPropertyId, string.Empty,
  16.             (uint)_PersistStorageType.PST_PROJECT_FILE, out propertyValue);
  17.  
  18.         // Try to get the current value from the project file; if it does not yet exist, return a default value.
  19.         if (!ErrorHandler.Succeeded(hr) || String.IsNullOrEmpty(propertyValue))
  20.         {
  21.             propertyValue = IECacheFilePatternPropertyDefaultValue;
  22.         }
  23.  
  24.         return propertyValue;
  25.     }
  26.  
  27.     set
  28.     {
  29.         projectStorage.SetPropertyValue(IECacheFilePatternPropertyId, string.Empty,
  30.             (uint)_PersistStorageType.PST_PROJECT_FILE, value);
  31.     }
  32. }
  33.  
  34. // IECacheFileList property related members
  35. private const string IECacheFileListPropertyId = "IECacheFileList";
  36. private const string IECacheFileListPropertyDefaultValue = "";
  37.  
  38. [DisplayName("IE cache file list")]
  39. [Description("This property specifies which files should be deleted from the IE cache when the Clear IE cache deployment step is activated")]
  40. [DefaultValue(RelatedTimerJobsPropertyDefaultValue)]
  41. // we want our custom property to show up in the SharePoint section in the project properties property grid
  42. [Category("SharePoint")]
  43. // use custom property editor to avoid "Constructor on type 'System.String' not found." error in design mode
  44. [Editor("System.Windows.Forms.Design.StringArrayEditor, System.Design", typeof(UITypeEditor))]
  45. [TypeConverter(typeof(CsvArrayConverter))]
  46. public String[] IECacheFileList
  47. {
  48.     get
  49.     {
  50.         String propertyValue;
  51.         int hr = projectStorage.GetPropertyValue(IECacheFileListPropertyId, string.Empty,
  52.             (uint)_PersistStorageType.PST_PROJECT_FILE, out propertyValue);
  53.  
  54.         // Try to get the current value from the project file; if it does not yet exist, return a default value.
  55.         if (!ErrorHandler.Succeeded(hr) || String.IsNullOrEmpty(propertyValue))
  56.         {
  57.             propertyValue = IECacheFileListPropertyDefaultValue;
  58.         }
  59.  
  60.         // remove accidental whitespaces
  61.         String[] fileNames = propertyValue.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
  62.         fileNames = Array.ConvertAll<String, String>(fileNames, fileName => fileName.Trim());
  63.         return fileNames;
  64.     }
  65.  
  66.     set
  67.     {
  68.         String propertyValue =
  69.             (value == null) ?
  70.             String.Empty :
  71.             // remove accidental whitespaces
  72.             String.Join("|", Array.ConvertAll<String, String>(value, fileName => fileName.Trim()));
  73.         projectStorage.SetPropertyValue(IECacheFileListPropertyId, string.Empty,
  74.             (uint)_PersistStorageType.PST_PROJECT_FILE, propertyValue);
  75.     }
  76. }
  77.  
  78. // IECacheDeleteRule property related members
  79. private const string IECacheDeleteRulePropertyId = "IECacheDeleteRule";
  80. private const IECacheDeleteRules IECacheDeleteRulePropertyDefaultValue = IECacheDeleteRules.OnlyCurrentSite;
  81.  
  82. [DisplayName("IE cache delete rule")]
  83. [Description("This property specifies the relation between the current SharePoint site and files to be deleted from IE cache")]
  84. [DefaultValue(IECacheDeleteRulePropertyDefaultValue)]
  85. // we want our custom property to show up in the SharePoint section in the project properties property grid
  86. [Category("SharePoint")]
  87. [TypeConverter(typeof(IECacheDeleteRuleConverter))]
  88. public IECacheDeleteRules IECacheDeleteRule
  89. {
  90.     get
  91.     {
  92.         // set default value
  93.         IECacheDeleteRules propertyValue = IECacheDeleteRulePropertyDefaultValue;
  94.         string propertyValueString;
  95.         int hr = projectStorage.GetPropertyValue(IECacheDeleteRulePropertyId, string.Empty,
  96.             (uint)_PersistStorageType.PST_PROJECT_FILE, out propertyValueString);
  97.  
  98.         // Try to get the current value from the project file; if it does not yet exist, return a default value.
  99.         if (ErrorHandler.Succeeded(hr) && !String.IsNullOrEmpty(propertyValueString))
  100.         {
  101.             Enum.TryParse<IECacheDeleteRules>(propertyValueString, out propertyValue);
  102.         }
  103.  
  104.         return propertyValue;
  105.     }
  106.  
  107.     set
  108.     {
  109.         projectStorage.SetPropertyValue(IECacheDeleteRulePropertyId, string.Empty,
  110.             (uint)_PersistStorageType.PST_PROJECT_FILE, value.ToString());
  111.     }
  112. }
  113.  
  114. public enum IECacheDeleteRules
  115. {
  116.     [Description("Only from current site")]
  117.     OnlyCurrentSite,
  118.     [Description("All from current site")]
  119.     AllFromCurrentSite,
  120.     [Description("No site specific")]
  121.     NoSiteSpecific
  122. }
  123.  
  124. // based on EnumConverter example from
  125. // http://www.c-sharpcorner.com/uploadfile/witnes/using-propertygrid-in-net/
  126. class IECacheDeleteRuleConverter : EnumConverter
  127. {
  128.     private Type enumType;
  129.  
  130.     public IECacheDeleteRuleConverter(Type type) : base(type)
  131.     {
  132.         enumType = type;
  133.     }
  134.  
  135.     public override bool CanConvertTo(ITypeDescriptorContext context, Type destType)
  136.     {
  137.         return destType == typeof(string);
  138.     }
  139.  
  140.     public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destType)
  141.     {
  142.         FieldInfo fi = enumType.GetField(Enum.GetName(enumType, value));
  143.         DescriptionAttribute dna = (DescriptionAttribute)Attribute.GetCustomAttribute(fi, typeof(DescriptionAttribute));
  144.         if (dna != null)
  145.             return dna.Description;
  146.         else
  147.             return value.ToString();
  148.     }
  149.  
  150.     public override bool CanConvertFrom(ITypeDescriptorContext context, Type srcType)
  151.     {
  152.         return srcType == typeof(string);
  153.     }
  154.  
  155.     public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
  156.     {
  157.         foreach (FieldInfo fi in enumType.GetFields())
  158.         {
  159.             DescriptionAttribute dna =
  160.             (DescriptionAttribute)Attribute.GetCustomAttribute(fi, typeof(DescriptionAttribute));
  161.             if ((dna != null) && ((string)value == dna.Description))
  162.                 return Enum.Parse(enumType, fi.Name);
  163.         }
  164.         return Enum.Parse(enumType, (string)value);
  165.     }
  166. }

We have two new methods to the static ExtensionHelper class. The ShouldDeleteIECacheFile method encapsulates the logic of cache file deletion as function of our new project properties. The ClearIECacheFile method calls the core cache file deletion (see ClearIEFiles method of the DeleteIECache class below) with this logic injected as a parameter. We do some logging in both of these methods to inform users in the Output window of VS about the progress and result of the deployment. We should keep the list of deleted files in the local filesDeleted variable, otherwise each files would be listed twice due to the logic implemented in the ClearIEFiles method.

Code Snippet
  1. internal static void ClearIECacheFile(ISharePointProject project)
  2. {
  3.     try
  4.     {
  5.         LogToOutputWindows(project, "Clearing files from IE cache");
  6.         SPVSProjectProps propertiesObject;
  7.         if (project.Annotations.TryGetValue<SPVSProjectProps>(out propertiesObject))
  8.         {
  9.             String ieCacheFilePattern = propertiesObject.IECacheFilePattern;
  10.             IEnumerable<String> ieCacheFileList = propertiesObject.IECacheFileList;
  11.             SPVSProjectProps.IECacheDeleteRules ieCacheDeleteRule = propertiesObject.IECacheDeleteRule;
  12.             List<String> filesDeleted = new List<String>();
  13.  
  14.             Func<Uri, bool> shouldDelete = new Func<Uri, bool>(u => ShouldDeleteIECacheFile(u,
  15.                                                                                         ieCacheFilePattern,
  16.                                                                                         ieCacheFileList,
  17.                                                                                         ieCacheDeleteRule,
  18.                                                                                         filesDeleted,
  19.                                                                                         project));
  20.             DeleteIECache.ClearIEFiles(shouldDelete);
  21.  
  22.             LogToOutputWindows(project, String.Format("Number of files deleted from IE cache: {0}", filesDeleted.Count));
  23.         }
  24.     }
  25.     catch (Exception ex)
  26.     {
  27.         LogToOutputWindows(project, String.Format("Clearing files from IE cache failed. Exception: {0}", ex.Message));
  28.     }
  29. }
  30.  
  31. internal static bool ShouldDeleteIECacheFile(Uri uri, String filePattern, IEnumerable<String> fileList,
  32.     SPVSProjectProps.IECacheDeleteRules ieCacheDeleteRule, List<String> filesDeleted, ISharePointProject project)
  33. {
  34.     bool result = false;
  35.  
  36.     Uri siteUrl = project.SiteUrl;
  37.     Regex filePatternRegex = string.IsNullOrEmpty(filePattern) ? null : new Regex(filePattern);
  38.     List<string> fileListEx = (fileList == null) ? new List<string>() : fileList.ToList();
  39.     string fileName = uri.Segments[uri.Segments.Length – 1];
  40.  
  41.     bool isFromCurrentSite = uri.AbsoluteUri.IndexOf(siteUrl.AbsoluteUri, StringComparison.InvariantCultureIgnoreCase) == 0;
  42.     if (ieCacheDeleteRule == SPVSProjectProps.IECacheDeleteRules.AllFromCurrentSite)
  43.     {
  44.         result = isFromCurrentSite;
  45.     }
  46.     else
  47.     {
  48.         result = ((fileListEx.Any(f => (f.ToUpper() == fileName.ToUpper()))) ||
  49.                 (filePatternRegex != null) && (filePatternRegex.IsMatch(fileName)));
  50.         if (ieCacheDeleteRule == SPVSProjectProps.IECacheDeleteRules.OnlyCurrentSite)
  51.         {
  52.             result = result && isFromCurrentSite;
  53.         }
  54.     }
  55.  
  56.     if ((result) && (!filesDeleted.Contains(uri.AbsoluteUri)))
  57.     {
  58.         filesDeleted.Add(uri.AbsoluteUri);
  59.         LogToOutputWindows(project, String.Format("Deleting file from IE cache: '{0}'", uri));
  60.     }
  61.  
  62.     return result;
  63. }

I kept the DeleteIECache class and its ClearIEFiles method as it was published on MSDN, except this method has a Func<Uri, bool> shouldDelete parameter that we use to decide if a specific file should be deleted from the IE cache:

returnValue = shouldDelete(uri) ? DeleteUrlCacheEntry(internetCacheEntry.lpszSourceUrlName) : false;

The code for the deployment step is pretty straightforward, it simply calls the ClearIECacheFile method of our ExtensionHelper class. Since we don’t have to call any SharePoint-specific code on the server side (that means no x64 process), there is no need for SharePoint commands in this case. That makes our life easier.

Code Snippet
  1. [DeploymentStep("PHolpar.ClearIECache")]
  2. [Export(typeof(IDeploymentStep))]
  3. internal class ClearIECacheDeployStep : IDeploymentStep
  4. {
  5.  
  6.     public bool CanExecute(IDeploymentContext context)
  7.     {
  8.         return true;
  9.     }
  10.  
  11.     public void Execute(IDeploymentContext context)
  12.     {
  13.         ISharePointProject project = context.Project;
  14.         ExtensionHelper.ClearIECacheFile(project);
  15.     }
  16.  
  17.     public void Initialize(IDeploymentStepInfo stepInfo)
  18.     {
  19.         stepInfo.Name = "Clear IE cache";
  20.         stepInfo.Description = "This step deletes the specified files from the Internet Explorer cache of the current user.";
  21.         stepInfo.StatusBarMessage = "IE cache is being cleared…";
  22.     }
  23. }

To test the new extension, I’ve created a deployment configuration called Clear IE cache.

image

This DC has only a single deployment step, our new Clear IE cache DS.

image

Using the following project properties (reg. exp. used: ^.*\.(js|JS)$) we can delete all .js files that were cached from the active SharePoint site:

image

We can specify the files to be deleted explicitly in the IE cache file list property:

image

In this case these files would be deleted from the cache independently from the URL they were downloaded from:

image

The next screenshot displays a sample deployment for the previous configuration:

image

Using the All from current sites value of IE cache delete rule (not illustrated here) the deployment process clears all files cached from the active SharePoint site, filtering properties (IE cache file list and IE cache file pattern) are ignored.

You can download the updated version (v 1.1) of my VS extension from the original location.

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.

Blog at WordPress.com.