Monday, March 20, 2017

Problem with outdated value in ModifiedBy search managed property after copying the document

Recently we faced with interesting problem: there is functionality which copies documents from another site collection in Sharepoint Online via SP.RequestExecutor:

   1: var sourceExecutor = new SP.RequestExecutor(sourceSiteUrl);
   2: var targetExecutor = new SP.RequestExecutor(targetSiteUrl);
   3:  
   4: // Get form digest of target site collection
   5: jQuery.ajax({
   6:     url: targetSiteUrl + "/_api/contextinfo",
   7:     type: "POST",
   8:     headers: {
   9:         "Accept": "application/json;odata=verbose"
  10:     },
  11:     success: function (data) {
  12:         try {
  13:             var digest = data.d.GetContextWebInformation.FormDigestValue;
  14:  
  15:             // Build executor action to retrieve the file data.
  16:             var getFileAction = {
  17:                 url: sourceSiteUrl + "/_api/web/GetFileByServerRelativeUrl('" +
  18:                     sourceServerRelativeUrl + "')/$value",
  19:                 method: "GET",
  20:                 binaryStringResponseBody: true,
  21:                 success: function (getFileData) {
  22:                     // Get the binary data.
  23:                     var result = data.body;
  24:                     // Build executor action to copy the file data to the new location.
  25:                     var copyFileAction = {
  26:                         url: targetSiteUrl + "/_api/web/GetFolderByServerRelativeUrl('" +
  27:                             targetFolder.get_serverRelativeUrl() + "')/Files/Add(url='" +
  28:                             fileNameFromInput + "', overwrite=true)",
  29:                         method: "POST",
  30:                         headers: {
  31:                             "Accept": "application/json; odata=verbose",
  32:                             "X-RequestDigest": digest
  33:                         },
  34:                         contentType: "application/json;odata=verbose",
  35:                         binaryStringRequestBody: true,
  36:                         body: getFileData.body,
  37:                         success: function (copyFileData) {
  38:                             ...
  39:                         },
  40:                         error: function (ex) {
  41:                             ...
  42:                         }
  43:                     };
  44:  
  45:                     targetExecutor.executeAsync(copyFileAction);
  46:                 },
  47:                 error: function (ex) {
  48:                     ...
  49:                 }
  50:             };
  51:             sourceExecutor.executeAsync(getFileAction);
  52:         } catch (e) {
  53:             ...
  54:         }
  55:     },
  56:     error: function (ex) {
  57:         ...
  58:     }
  59: });

After file is successfully copied Modified By field shown in list views displays correct value – it shows account of the user which copied the document (i.e. under which account code above was executed). However when we tried to get the value of ModifiedBy managed properly from search index we got value which original file had in the source site collection before it was copied to target site collection (i.e. person which last modified the document in the source site collection). Recrawling of the list didn’t help. Also we tried to explicitly set value of Editor field in document metadata after file has been copied and it didn’t help either.

In order to avoid this issue the following workaround was used: at first as before we get list of the files from search index using javascript object model and then when we get last n results sorted in correct way for displaying in web part we make n requests to content database in order to get correct value of ModifiedBy field (remember that in list views correct account is shown in this field and list views show data from content database):

   1: var documentsItems = [];
   2: var ctx = SP.ClientContext.get_current();
   3: for (var i in documentsFromSearch) {
   4:     var item = documentsFromSearch[i];
   5:     var path = item.Path;
   6:  
   7:     var relativeUrl = _spPageContextInfo.webServerRelativeUrl +
   8:         path.replace(_spPageContextInfo.siteAbsoluteUrl, "");
   9:  
  10:     documentsItems.push({
  11:         RelativeURL: relativeUrl,
  12:         File: null,
  13:         // item["ModifiedBy"] from search contains incorrect  value
  14:         ModifiedBy: item["ModifiedBy"]
  15:     });
  16: }
  17:  
  18: for (var i in documentsItems) {
  19:     var doc = documentsItems[i];
  20:     doc.File = ctx.get_web().getFileByServerRelativeUrl(doc.RelativeURL);
  21:     ctx.load(doc.File, "ListItemAllFields");
  22: }
  23:  
  24: ctx.executeQueryAsync(
  25:     function () {
  26:         for (var i in documentsItems) {
  27:             var doc = documentsItems[i];
  28:             if (typeof (doc.File) == "undefined" || doc.File == null) {
  29:                 continue;
  30:             }
  31:             var item = doc.File.get_listItemAllFields();
  32:             if (typeof (item) == "undefined" || item == null) {
  33:                 continue;
  34:             }
  35:             doc.ModifiedBy = item.get_item("Editor").$5E_1;
  36:         }
  37:     },
  38:     function (sender, args) {
  39:         ...
  40:     });

In this code we at first create array of objects from results returned from search index (lines 3-16) and then for each object we get it’s File object and from it’s list item read Editor value (lines 18-40). Note that for getting editor’s display name here we used undocumented property $5E_1 of SP.FieldUserValue – more proper way is to get user’s id from it’s get_lookupId() method and load it in separate request from web’s user collection, so please be aware of that. Of course this approach is slower than original because it makes n requests to content database, but if data is loaded asynchronously and number of displayed files is not big, performance will remain acceptable and web part will display correct value in ModifiedBy column. Hope that this information will help someone.

Tuesday, March 7, 2017

Provision and automatic update of embedded resources to App_LocalResources folder under Sharepoint Template/Layouts or Template/ControlTemplates sub folders

In one of my previous posts I showed how to include embedded provision resources (located in {SharepointRoot}\Resources) to wsp package so they will be provisioned and updated automatically (see Provision and automatic update of embedded resources to Resources folder in Sharepoint hive). However on practice we also often add UI resources specific to concrete application layouts page (page located under Template/Layouts subfolder) or user control (under Template/ControlTemplates). E.g. if we have a page /Template/Layouts/Test/foo.aspx then UI resource will be located in /Template/Layouts/Test/App_LocalResources/foo.aspx.resx. Note that it has the same name as parent page plus .resx extension. In this case we can use the following declarative syntax in foo.aspx layout:

   1: <asp:Literal Text="<%$Resources:Test%>" runat="server" />

and ASP.Net will automatically get resource string with key Test from foo.aspx.resx file.

This approach simplifies localization of UI components in Sharepoint. The problem is that if we add our resource file as Content to Visual Studio project it will be added to result manifest.xml of wsp package during publishing. However if we will change it’s type to Embedded resource it won’t be added (which is needed if we want to use resource strings also from C# code), i.e. in this case we will need to update it manually (i.e. behavior is the same as for root resources described in the article mentioned above).

In order to fix this issue, i.e. in order to force Visual Studio to add embedded resx file from App_LocalResources folder to wsp package we will use the same approach described in Provision and automatic update of embedded resources to Resources folder in Sharepoint hive. But syntax of SharePointProjectItem.spdata file will be different. In case of local UI resources it will look like this:

   1: <?xml version="1.0" encoding="utf-8"?>
   2: <ProjectItem Type="Microsoft.VisualStudio.SharePoint.GenericElement"
   3: SupportedTrustLevels="All"
   4: SupportedDeploymentScopes="Web, Site, WebApplication, Farm, Package"
   5: xmlns="http://schemas.microsoft.com/VisualStudio/2010/SharePointTools/SharePointProjectItemModel">
   6:   <Files>
   7:     <ProjectItemFile Source="..\..\..\Layouts\Test\App_LocalResources\foo.aspx.resx"
   8:         Target="Layouts\Test\App_LocalResources" Type="TemplateFile" />
   9:   </Files>
  10: </ProjectItem>

After that if you will publish wsp package reference on embedded resource will be added there:

   1: <TemplateFiles>
   2:     ...
   3:     <TemplateFile Location="Layouts\Test\App_LocalResources\foo.aspx.resx" />
   4: </TemplateFiles>

I.e. we will have all embedded resources provisioned automatically.

Friday, February 24, 2017

Problem with delayed propagation of Azure AD groups to Sharepoint Online

As you probably know in Sharepoint Online it is possible to use Azure AD groups which belong to your tenant for configuring permissions. The problem is that Azure AD security groups are not become available in Sharepoint Online immediately after creation, there is some delay between the moment when group was created and the moment when it can be resolved in Sharepoint people picker. If you have automation process which creates Sharepoint Online site, Azure groups and then grants them permissions on created site, you need to handle this delay.

One of the solutions is to make several attempts to try to resolve Azure AD group in Sharepoint. If group is not available yet, wait some time and repeat attempt. And you need to decide maximum number of attempts and delay between them. Here is possible solution:

   1: private static void AddAzureGroupToSharepointGroup(ClientContext ctx,
   2:     Microsoft.SharePoint.Client.Group spGroup, string azureGroupName)
   3: {
   4:     if (spGroup == null)
   5:     {
   6:         Console.WriteLine("Sharepoint group is null (Azure group name '{0}')",
   7:             azureGroupName);
   8:         return;
   9:     }
  10:     if (string.IsNullOrEmpty(azureGroupName))
  11:     {
  12:         Console.WriteLine("Azure group name '{0}' is empty", azureGroupName);
  13:         return;
  14:     }
  15:  
  16:     Console.WriteLine("Add Azure group '{0}' to Sharepoint group '{1}'",
  17:         azureGroupName, spGroup.LoginName);
  18:  
  19:     int num = 0, numOfAttempts = 30;
  20:     do
  21:     {
  22:         try
  23:         {
  24:             var user = ctx.Web.EnsureUser(azureGroupName);
  25:             ctx.Load(user);
  26:             ctx.ExecuteQueryRetry();
  27:             break;
  28:         }
  29:         catch
  30:         {
  31:             Console.WriteLine("Group '{0}' is not available yet (attempt #{1})",
  32:                 azureGroupName, num + 1);
  33:         }
  34:         Thread.Sleep(60000);
  35:         num++;
  36:     } while (num < numOfAttempts);
  37:  
  38:     ctx.Web.AddUserToGroup(spGroup.LoginName, azureGroupName);
  39: }

This methods add specified Azure AD group to Sharepoint group. In order to make it work Azure group should be available in Sharepoint Online (should be resolvable in people picker). It makes max 30 attempts and waits 1 minute between each attempts (lines 19-36). If group is not available yet, then call to Web.EnsureUser() will throw exception (line 24). We catch this exception and increment attempts counter (lines 29-35). If group was resolved it means that it is propagated to Sharepoint Online (Azure AD groups are represented in Sharepoint Online as User object – in the same way as for regular AD groups) and we can add it to the Sharepoint group (line 38). Method Web.AddUserToGroup() is extension method which is implemented in OfficeDevPnP.Core.

Tuesday, February 14, 2017

Set initial value to Sharepoint people picker via javascript

Suppose that in our Sharepoint Online site we want to use Azure groups for configuring permissions on securable objects (sub sites, document libraries, etc.) and that all Azure groups’ names start with common prefix “azure_” which is set as part of naming convention used in the company. In this case users will always need to type this prefix in people picker each time they want to grant permissions to some Azure group. Is it possible to help them and set common prefix as initial value of people picker? Yes, it is possible with using the following trick:

   1: var peoplePickerUtility = {
   2:  
   3:     isPeoplePickerOpened: false,
   4:     isPeoplePickerChanged: false,
   5:  
   6:     addPrefixToPeoplePicker: function() {
   7:         if (window.location.href.toLowerCase().indexOf("/user.aspx") < 0) {
   8:             return;
   9:         }
  10:  
  11:         setTimeout(function(){
  12:             try {
  13:                 if (typeof(SPClientPeoplePicker) != "undefined" &&
  14: $("#" + SPClientPeoplePicker.SPClientPeoplePickerDict.peoplePicker_TopSpan.EditorElementId).is(":visible")) {
  15:                     peoplePickerUtility.isPeoplePickerOpened = true;                    
  16:                 }
  17:                 else {
  18:                     peoplePickerUtility.isPeoplePickerOpened = false;
  19:                     peoplePickerUtility.isPeoplePickerChanged = false;
  20:                 }
  21:                 
  22:                 if (peoplePickerUtility.isPeoplePickerOpened &&
  23:                         !peoplePickerUtility.isPeoplePickerChanged) {
  24: $("#" + SPClientPeoplePicker.SPClientPeoplePickerDict.peoplePicker_TopSpan.EditorElementId).val("azure_");
  25:                     peoplePickerUtility.isPeoplePickerChanged = true;
  26:                 }
  27:                 
  28:                 peoplePickerUtility.addPrefixToPeoplePicker();
  29:             }
  30:             catch(e) {
  31:                 console.log("Error in peoplePickerUtility.addPrefixToPeoplePicker: " + e.message);
  32:             }
  33:         }, 1000);
  34:     }
  35: };
  36:  
  37: $(function() {
  38:     peoplePickerUtility.addPrefixToPeoplePicker();
  39: });

The idea is that we run function the loop each 1 sec (line 11) which checks whether people picker is opened via SPClientPeoplePicker.SPClientPeoplePickerDict.peoplePicker_TopSpan object. If it is visible it sets its initial value (“azure_”) and sets flag which says that it is already changed (line 25), so it won’t change it twice once it is opened and override value added by user. If people picker will be closed and opened again, it will set initial value again. This code works only on granting permissions page (user.aspx) in order to not affect whole site. Of course this is not perfect solution, but it works. After that people picker will have “azure_” text as initial value:

Hope that this information will help someone.

Friday, February 3, 2017

Improve Yammer embed feed performance on Sharepoint Online

Yammer embed feed is often added to the front page of the company intranet in Sharepoint. From one side it adds value to customers as users may see corporate feed together with other important content on the same page. From other side it adds drawback: increase of the total page load time (by total time I mean time needed for both server and client side components. Yammer adds more time to the client part). We measured that Yammers adds ~3-4 seconds to the scripting part of the page load time. Is there a way to improve situation?

Let’s check how Yammer embed feed is added on the page. In most cases it is added via Content editor web part and the following code:

   1: <!DOCTYPE HTML>                                          
   2: <html>
   3:     <head></head>
   4:     <body>
   5:         <script type="text/javascript"
   6: src="https://c64.assets-yammer.com/assets/platform_embed.js"></script>
   1:                                                   
   2:         <div id="embedded-feed" style="height:1000px;width:400px;"></div>
   3:         <script>
   4:             yam.connect.embedFeed({"config": { "header": false },
   5:                 "container": "#embedded-feed",
   6:                 network: 'example.com' });
   7:             $(window).load(function () {
   8:                 $('.frontpage #embedded-feed').width('100%');
   9:             });
  10:         
</script>
   7:     </body>
   8: </html>

As you can see script is loaded from Yammer CDN https://c64.assets-yammer.com/assets/platform_embed.js. So the first thing I tried was to move this script to the Sharepoint doclib, but it didn’t give visible results (internally this script loads a lot of other scripts and css files from CDN and also adds iframe dynamically).

After that I tried to use another approach which gave some results. First of all create new page (e.g. yammer.aspx) in Pages doclib and detach it from Page layout via Sharepoint Designer. After that remove all code from there and add only Yammer code shown above.

Then we need to modify the page where we show embed Yammer feed: instead of Content editor web part which was used for Yammer initially we will use Page viewer web part and configure it to use newly created page yammer.aspx as a source (that’s why we needed to detach this page from page layout – it should not contain any UI elements like headers, navigation, etc. It should only contain Yammer widget). I.e. after that page will look the same, but technically there is difference how Yammer is shown there. Now Yammer is shown in sub page via iframe in separate http request and its scripts not affect load of basic page. At least for us after that our testing showed that scripting time was reduced on ~2 seconds. Hope that information will be helpful for you.