Monday, December 15, 2014

Localize content type names and field titles in Sharepoint sandbox solutions

If you work with Sharepoint starting with pre-2013 version most probably you know how to localize strings (content type names, field types, feature names, web part titles, etc.) in farm solutions. In order to do it we need to use provisioning resources with special syntax: $Resources:{resource file name without resx extension},{string identifier}, e.g. “$Resources:MyResource,Foo”. It assumes that specified resx file is provisioned with farm solution to 14 or 15/Resources folder (in VS solution you may create mapped folder for that). But when we work with farm solutions situation is different: files are not saved to the local file system. Because of that we can’t use the same approach for localizing strings for various artifacts which are provisioned with sandbox solution. In this post I will show one approach which may be used for localizing content types names and fields titles which is most probably is most often needed requirement.

First of all I need to say that tried many ways before found really working approach. Also need to say that this approach uses codebehind which is as you probably know is deprecated in sandbox solutions by MS. If it is important for you to not use codebehind in sandbox solutions, you may rewrite the code e.g. via javascript client object model which and use the same idea for running it first time for provisioned site (see below). Also example below is shown for scenario when in the same site collection there may be several different languages. When you have single language per site collection approach will be simpler: instead of fixing content types inheritors in all bound lists it will be enough to fix only site content type and propagate changes to all lists which use this content type.

Ok, lets check the approach itself. Site columns (fields) and content types are provisioned with hardcoded strings (I use English names, but you may use any language for default string values depending on your language):

   1: <Field ID="..."
   2:     Name="MyField"
   3:     SourceID="http://schemas.microsoft.com/sharepoint/v3"
   4:     StaticName="MyField"
   5:     Group="Custom"
   6:     Type="Text"
   7:     DisplayName="My Field"/>

content type:

   1: <ContentType ID="..."
   2:              Name="My Content type"
   3:              Group="Custom"
   4:              Inherits="TRUE"
   5:              Version="0">
   6:   <FieldRefs>
   7:     <FieldRef ID="..." Name="MyField" />
   8:     ...
   9:   </FieldRefs>
  10: </ContentType>

After we will provision these elements to the Sharepoint site they will have names/titles which are hardcoded in xml shown above. Also in all lists where this content type is bound to the same string values will be used. The next step is to run code which will localize these strings depending on the specified language.

As I already mentioned above we will consider the scenario when there may be sub sites (SPWeb) on different languages in the same site collection. In this case we will need to get reference on the list content type which inherits site content type and update strings there. If we would update site content type itself and would propagate changes to all lists which use this content type, all sub sites would have titles on the same language which was last used which is of course not what we want.

Translated strings should be stored in resx files which are embedded to your sandbox solution’s assembly. After you will add them to VS solution you will also need to add additional assemblies to your package manifest (one assembly per each translation):

image

In this example we added Finnish and Russian resources.

After that we are ready to make actual localization. Here is the sandbox code which will localize list content type and its fields:

   1: public static void LocalizeCTFields(SPWeb web)
   2: {
   3:     var list = web.Lists.Cast<SPList>().FirstOrDefault(l =>
   4:         l.RootFolder.Url == "MyList");
   5:     if (list == null)
   6:     {
   7:         return;
   8:     }
   9:  
  10:     var ci = new CultureInfo((int)web.Language);
  11:     web.AllowUnsafeUpdates = true;
  12:     var fieldsList = list.Fields.Cast<SPField>().ToList();
  13:     var ct = list.ContentTypes.Cast<SPContentType>().FirstOrDefault(c =>
  14:         c.Id.IsChildOf(new SPContentTypeId(MY_CONTENT_TYPE_ID)));
  15:     if (ct != null)
  16:     {
  17:         ct.Name = Properties.Resources.ResourceManager.
  18:             GetString("MyContentType_Title", ci);
  19:         ct.Update();
  20:     }
  21:  
  22:     localizeField(fieldsList, new Guid(FIELD1_ID),
  23:         Properties.Resources.ResourceManager.GetString("Field1_Title", ci));
  24:     localizeField(fieldsList, new Guid(FIELD2_ID),
  25:         Properties.Resources.ResourceManager.GetString("Field2_Title", ci));
  26:     ...
  27: }
  28:  
  29: private static void localizeField(List<SPField> fields, Guid fieldId,
  30:     string title)
  31: {
  32:     if (fields.IsNullOrEmpty() || string.IsNullOrEmpty(title))
  33:     {
  34:         return;
  35:     }
  36:     var field = fields.FirstOrDefault(f => f.Id == fieldId);
  37:     if (field == null)
  38:     {
  39:         return;
  40:     }
  41:     field.Title = title;
  42:     field.Update(true);
  43: }

It gets the reference to the SPLIst (lines 3-8), then find needed content type in the list (lines 13-15) and updates content type name (lins 15-20) and all fields in the list which correspond to appropriate content type (lines 22-26) with strings from resource file for the language of the current site (line 10). Note that we get reference on fields collection from SPList.Fields, not from SPContentType.Fields or FieldLinks.

The remaining question is where to put this code and in what moment it should be executed. I tried to use WebProvisioned handler, but it didn’t work: there were no any errors, just changes were not applied to the content types and fields, and they remain with default English texts. The only working approach I found is to put hidden web part with initialization code to the site’s front page. Once site is created user is redirected to the front page of this site. In this moment we may execute necessary post initialization logic. We need to ensure that it will be executed once. In the farm env it will be tricky to ensure that code is running in single thread once, so I will omit these details here:

   1: public class InitializationWebPart : WebPart
   2: {
   3:     protected override void OnLoad(EventArgs e)
   4:     {
   5:         var web = SPContext.Current.Web;
   6:         web.AllowUnsafeUpdates = true;
   7:  
   8:         // site should be initialized once
   9:         if (this.isInitialized(web))
  10:         {
  11:             return;
  12:         }
  13:  
  14:         try
  15:         {
  16:             LocalizeCTFields(web);
  17:         }
  18:         finally
  19:         {
  20:             this.setInitialized(web);
  21:         }
  22:     }
  23:  
  24:     private bool isInitialized(SPWeb web)
  25:     {
  26:         if (!web.AllProperties.ContainsKey("IsInitialized"))
  27:         {
  28:             return false;
  29:         }
  30:         return bool.Parse(web.AllProperties["IsInitialized"] as string);
  31:     }
  32:  
  33:     private void setInitialized(SPWeb web)
  34:     {
  35:         web.SetProperty("IsInitialized", true);
  36:         web.Update();
  37:     }
  38: }

As shown in this example when web part runs the code once, it will update property bag setting and won’t run twice.

This post shows that although in sandbox solution simple operations like localization of content types and fields are more complicated they are still possible and you may use it in your projects.

Wednesday, December 10, 2014

Overview of Sharepoint migration tools which support O365

Recently we evaluated tools suitable for migrating the content from old Sharepoint 2007 to Sharepoint Online (O365). In this post I will share summary table which may help you to orient in some migration tools available on market. Note that on the moment when you will read this article prices may be outdated. Here is the table:

Tool Price SP2007 to O365 supported Requires server installation Comments

Quest (Dell) Content Migrator for SharePoint

$834,00 per 1 seat for 12 months Yes No For content migrations (documents, list items), not for whole sites

Quest (Dell) Migration Suite for SharePoint

Quote should be requested.
Example: minimum 250 Gb, and price per gigabyte is 50 €
Yes No Better for large and complex migrations. Quest tool is an OEM of the MetaVis product. Licensed per Gb
MetaVis Migrator for SharePoint Quote be requested.
Example (prices for 12 months):

1-50 Users            $2,995.00
51-300 Users        $5,495.00
301-2500 Users     $6,495.00
2501-7500 Users   $7,995.00
7500-15000 Users $12,995.00
15000+ Users       $24,995.00
Yes No The same as Quest migration suite, but with different licensing model (per seat and company volume)
Sharegate Migration tool $1,995 per seat for 12 months Yes No Better for small and simple migrations. However also supports PowerShell automation which makes it possible to use it for large migrations as well
Metalogix Content-Matrix

25 Gb - free
50 Gb - 1995$
100 Gb - 3995$
>100 Gb – quote should be requested

Yes No Formelly Axceller

These are not all tools available, because we needed migration specifically from SP2007 to O365, but not all tools support these platforms. However anyway it may be helpful for initial overview.

Monday, November 24, 2014

Save wsp packages which are currently installed in the Sharepoint solution store via PowerShell

During installation of updates to the Sharepoint environment, especially on production environment, it is good to have rollback plan. One of the step of this rollback plan is to restore wsp packages which were used before installation. Here is the PowerShell script which will enumerate and save all wsps packages (farm solutions) currently installed to the solutions store to the local file system:

   1: (Get-SPFarm).Solutions | ForEach-Object { $var = (Get-Location).Path +
   2:     "\" + $_.Name; $_.SolutionFile.SaveAs($var) }

(you need to use it as one line script. I added line break for better readability). Having this script you may backup farm solutions wsps before to perform update and rollback them if needed.

Saturday, November 22, 2014

Extend Sharepoint 2013 search queries with custom query variables populated dynamically in runtime

Sharepoint 2013 allows to extend search queries via several ways. The commonly used way is to do it via Search result sources:

image

In the query builder we may add more conditions to those which user enters in the search box. They are applied to query when user performs search and ResultScriptWebPart or ContentBySearchWebPart are configured to use appropriate search result source. It can be done via web part properties for specific web part or for all web parts if search result source is set as default. If you need to create custom result source and set it default programmatically you may check the following article: Create custom search result source programmatically in Sharepoint 2013. Note that there is one problem related with custom default result sources which I wrote about here: Problem with missing catalog connections when use customized search result source in Sharepoint 2013.

This is useful approach when we need to add more conditions to the search query. Using search query variables we may retrieve values from different site or site collection properties (e.g. id or url), current user’s properties, managed metadata term used for current navigation node and others. Full list of available standard query variables can be found here: Query variables in SharePoint Server 2013. However anyway with standard query variables the set of values which we may use in the queries is quite limited. But what if we need to add condition to the query with value which should be calculated dynamically in runtime, e.g. if we need to add value from cookie or value calculated by complex business logic?

One of the way is to use query string variable {QueryString.<ParameterName>}, where <ParameterName> should be replaced with the actual query string parameter name ,e.g. {QueryString.Country}. On practice however it is not always convenient or possible. E.g. by default search result web part (ResultScriptWebPart) uses client navigation (can be controlled via UpdateAjaxNavigate property), i.e. adds search phrase entered by user in search box on the search results page to the current url after hash “#” symbol. E.g. if user enters something in the search box on site’s front page and clicks search, user is redirected to the search result page with search phrase specified in “k” query string parameter:

http://example.com/search?k=foo

But if after that user adds “bar” phrase to the search box on the search results page, url will look like this:

http://example.com/search?k=foo#k=bar

and ResultScriptWebPart will use the last phrase defined after hash symbol. The problem however that part after hash tag is not considered by the standard as part of url and thus is not sent to the server (see the following forum thread for more details: How to get Url Hash (#) from server side). For our example it means that query string variable sent to the server will be empty (note that there is also one problem with empty query string variables described here: One problem with “Value of a parameter from URL” query variable in Sharepoint 2013 search queries) and search won’t work as we expect.

In order to add dynamically calculated values to the search query we may also try to use http request properties {Request.<PropertyName>}, but as people mentioned in the forums, it really works only with Url property (although I didn’t check it by myself), so you can use e.g. request’s property bag for that.

So what to do when we need to add dynamic value to the search query? In this case the following approach can be used:

1. add necessary condition to the search query builder with custom variable name:

MyManagedProperty={Country}

Here we added condition that managed property MyManagedProperty should be equal to {Country} variable. {Country} – is custom variable which will be added to the query processing pipeline by custom code (see below).

2. implement custom web part and put it on the page with ResultScriptWebPart or ContentBySearchWebPart and implement the following code there:

   1: // retrieve value from cookies or from any other place
   2: string country = ...;
   3:  
   4: var scriptApp = ScriptApplicationManager.GetCurrent(this.Page);
   5: var dataProvider = scriptApp.QueryGroups["Default"].DataProvider;
   6: dataProvider.BeforeSerializeToClient += (sender, args) =>
   7:     {
   8:         var dp = sender as DataProviderScriptWebPart;
   9:         if (dp == null)
  10:         {
  11:             return;
  12:         }
  13:  
  14:         dp.Properties["Country"] = country;
  15:     };

After that you search query will return only those content which has MyManagedProperty set to particular country, which is retrieved from the cookies or calculated using any other business logic. All credits should go to Mikael Svenson from which article I’ve got the idea of using this approach. Hope that this information will help you in your work.

Thursday, November 13, 2014

Problem with updating managed properties in display templates used in Sharepoint search

Display templates are files with .html extension which are used for customizing appearance of the content in Sharepoint search results. When you upload such files to Site settings > Master page and page layouts > Display templates folder, Sharepoint automatically creates js version of display template, which is actually used by search engine. When you publish or delete basic html file, associated js file is published or deleted automatically.

The structure of most display templates looks like this:

   1: <html xmlns:mso="urn:schemas-microsoft-com:office:office"
   2: xmlns:msdt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882"> 
   3: <head>
   4: <title>Foo</title>
   5:  
   6: <!--[if gte mso 9]><xml>
   7: <mso:CustomDocumentProperties>
   8: <mso:TemplateHidden msdt:dt="string">0</mso:TemplateHidden>
   9: <mso:MasterPageDescription msdt:dt="string">Foo</mso:MasterPageDescription>
  10: <mso:ContentTypeId msdt:dt="string">...</mso:ContentTypeId>
  11: <mso:TargetControlType msdt:dt="string">;#SearchResults;#</mso:TargetControlType>
  12: <mso:HtmlDesignAssociated msdt:dt="string">1</mso:HtmlDesignAssociated>
  13: <mso:ManagedPropertyMapping msdt:dt="string">'Title':'Title','Path':'Path',
  14: 'Description':'Description','EditorOWSUSER':'EditorOWSUSER',
  15: 'LastModifiedTime':'LastModifiedTime','CollapsingStatus':'CollapsingStatus',
  16: 'DocId':'DocId','HitHighlightedSummary':'HitHighlightedSummary',
  17: 'HitHighlightedProperties':'HitHighlightedProperties',
  18: 'FileExtension':'FileExtension','ViewsLifeTime':'ViewsLifeTime',
  19: 'ParentLink':'ParentLink','FileType':'FileType','IsContainer':'IsContainer',
  20: 'SecondaryFileExtension':'SecondaryFileExtension','DisplayAuthor':'DisplayAuthor',
  21: 'SPSiteURL':'SPSiteURL',...</mso:ManagedPropertyMapping>
  22: </mso:CustomDocumentProperties>
  23: </xml><![endif]-->
  24: </head>
  25: <body>
  26:     <div id="Item_Foo">
  27:         ...
  28:     </div>
  29: </body>
  30: </html>

In the top area of the file metadata section is defined. Very important section is <mso:ManagedPropertyMapping>…</<mso:ManagedPropertyMapping>. It contains list of managed properties which can be used in the current display template. The actual look and feel template is defined inside topmost div tag (in example above it’s the div with id = “Item_Foo”).

During the lifetime of your application it may be needed to update existing display templates which are used in living site. If you only need to update look and feel part of template it won’t cause a lot of problems: just re-upload html file to the the location inside Site settings > Master page and page layouts > Display templates folder and publish it (depending on your settings you may also need to approve the file). But if you added new managed properties to <mso:ManagedPropertyMapping>…</<mso:ManagedPropertyMapping> section you may face with the problem that values of these properties will be always empty when you will try to use them inside display template.

This problem is caused by Sharepoint cache: it somehow caches mapped managed properties so deeply, that simple re-uploading and re-publishing doesn’t help (I tried also restart Sharepoint search and search host services and restart the server, but it didn’t help). In order to clear them from cache I found the following solution: delete original html file from Site settings > Master page and page layouts > Display templates, upload it from scratch and publish. If this display template was used in some search result types, you may also need to re-create this result type. After that try to search the content, for which updated display template should be used (Ctrl-F5 may be needed in browser as well): new managed properties should appear now.