Saturday, July 26, 2014

Provision managed metadata term sets and fields for Sharepoint Online using client object model

Starting from Sharepoint 2010 we used to work with managed metadata. But Sharepoint Online introduced new challenge: we have to provision managed metadata fields and term sets via client object model now. I searched for existing solutions first and they didn’t satisfy me. The most often solution I found was to hardcode term store id and provision managed metadata fields declaratively (with optional sugar like automatic replace of this id during publishing of wsp package). I don’t like this approach. What I needed is the same way which we use for regular on-premise Sharepoint installations:

  1. create term sets from xml file;
  2. provision managed metadata fields;
  3. bind fields to term sets.

I wrote PowerShell script which automates these tasks for Sharepoint Online using client object model. Let’s start from creating term sets in local site collection’s term store:

   1: function Create-Term($ctx, $termSet, $label, $lcid)
   2: {
   3:     $term = $termSet.CreateTerm($label, $lcid, [System.Guid]::NewGuid())
   4:     $ctx.ExecuteQuery()
   5: }
   6:  
   7: function Create-TermSet($ctx, $group, $termSetXml, $lcid)
   8: {
   9:     Write-Host "Creating term set" $termSetXml.Name -foregroundcolor Green
  10:     $termSet = $group.CreateTermSet($termSetXml.Name, $termSetXml.Id, $lcid)
  11:     $ctx.ExecuteQuery()
  12:  
  13:     $termSetXml.Term | ForEach-Object { Create-Term $ctx $termSet $_.Name $lcid }
  14: }
  15:  
  16: function Get-TermStore($ctx)
  17: {
  18:     Write-Host "Loading taxonomy session" -foregroundcolor Green
  19:     $session =
  20: [Microsoft.SharePoint.Client.Taxonomy.TaxonomySession]::GetTaxonomySession($ctx)
  21:     $session.UpdateCache();
  22:     $ctx.Load($session)
  23:     $ctx.ExecuteQuery()
  24:  
  25:     Write-Host "Loading term stores" -foregroundcolor Green
  26:     $termStores = $session.TermStores
  27:     $ctx.Load($termStores)
  28:     $ctx.ExecuteQuery()
  29:     $termStore = $termStores[0]
  30:     $ctx.Load($termStore)
  31:     Write-Host "Term store with the following id is loaded:"
  32: $termStore.Id -foregroundcolor Green
  33:     return $termStore
  34: }
  35:  
  36: function Provision-TermSets($ctx, $xmlFilePath)
  37: {
  38:     Write-Host "Load term sets from xml" -foregroundcolor Green
  39:     [xml]$xmlContent = (Get-Content $xmlFilePath)
  40:     if (-not $xmlContent)
  41:     {
  42:         Write-Host "Xml was not loaded successfully. Term sets won't be created"
  43: -foregroundcolor Red
  44:         return
  45:     }
  46:  
  47:     $termStore = Get-TermStore $ctx
  48:  
  49:     Write-Host "Creating group" $xmlContent.Id -foregroundcolor Green
  50:     $groups = $termStore.Groups
  51:     $ctx.Load($groups)
  52:     $ctx.ExecuteQuery()
  53:  
  54:     $group = $groups | Where-Object {$_.Name -eq $xmlContent.Group.Name}
  55:     if ($group)
  56:     {
  57:         Write-Host "Group" $xmlContent.Group.Name
  58: "already exists. If you want to recreate it, delete existing group first"
  59: -foregroundcolor Yellow
  60:         return
  61:     }
  62:  
  63:     $group = $termStore.CreateGroup($xmlContent.Group.Name, $xmlContent.Group.Id)
  64:     $ctx.ExecuteQuery()
  65:     $xmlContent.Group.TermSet |
  66: ForEach-Object { Create-TermSet $ctx $group $_ $termStore.DefaultLanguage }
  67: }

The entry point is Provision-TermSets() method (line 36). Before to call it we need to define term sets in the xml. I used the following structure:

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <Group Name="TestGroup" Id="...">
   3:   <TermSet Name="TermSet1" Id="...">
   4:     <Term Name="Term11" />
   5:     <Term Name="Term12" />
   6:     <Term Name="Term13" />
   7:   </TermSet>
   8:   <TermSet Name="TermSet2" Id="...">
   9:     <Term Name="Term21" />
  10:     <Term Name="Term22" />
  11:     <Term Name="Term23" />
  12:   </TermSet>
  13:   <TermSet Name="TermSet3" Id="...">
  14:     <Term Name="Term31" />
  15:     <Term Name="Term32" />
  16:     <Term Name="Term33" />
  17:   </TermSet>
  18: </Group>

One thing we should notice here is that we explicitly define term set ids with names. It will help us when we will bind managed metadata fields to them (see below). Having term sets structure in xml file we can now create them using the following command:

   1: $context = New-Object Microsoft.SharePoint.Client.ClientContext($siteURL)
   2: $context.AuthenticationMode =
   3: [Microsoft.SharePoint.Client.ClientAuthenticationMode]::Default
   4: $securePassword = ConvertTo-SecureString $password -AsPlainText -Force
   5: $credentials =
   6: New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($username,
   7: $securePassword)
   8: $context.Credentials = $credentials
   9:  
  10: Provision-TermSets $context $xmlPath

On lines 1-8 we prepare client context with user’s credentials and then call Provision-TermSets() function defined above with created context and path to xml file. After that we will have our term sets created in the term store.

Second step is to create managed metadata fields. We will do it declaratively, which is supported in sandbox solutions, so it will be the same as it would be for on-premise version. In our example we have 3 term sets, so lets provision 3 managed metadata fields:

   1: <?xml version="1.0" encoding="utf-8"?>
   2: <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
   3:   <Field Type="Note"
   4:     DisplayName="Field1TaxHTField"
   5:     MaxLength="255"
   6:     Group="Test"
   7:     ID="..."
   8:     StaticName="Field1TaxHTField"
   9:     Name="Field1TaxHTField"
  10:     Hidden="TRUE"
  11:     ShowInViewForms="FALSE"
  12:     Description="" />
  13:   <Field ID="..."
  14:     SourceID="http://schemas.microsoft.com/sharepoint/v3"
  15:     Type="TaxonomyFieldType"
  16:     DisplayName="Field1"
  17:     ShowField="Term1033"
  18:     Required="FALSE"
  19:     EnforceUniqueValues="FALSE"
  20:     Group="Test"
  21:     StaticName="Field1"
  22:     Name="Field1"
  23:     Hidden="FALSE"
  24:     Mult="FALSE">
  25:     <Default></Default>
  26:     <Customization>
  27:       <ArrayOfProperty>
  28:         <Property>
  29:           <Name>IsPathRendered</Name>
  30:           <Value xmlns:q7="http://www.w3.org/2001/XMLSchema"
  31: p4:type="q7:boolean" xmlns:p4="http://www.w3.org/2001/XMLSchema-instance">
  32:             true
  33:           </Value>
  34:         </Property>
  35:         <Property>
  36:           <Name>TextField</Name>
  37:           <Value xmlns:q6="http://www.w3.org/2001/XMLSchema"
  38: p4:type="q6:string" xmlns:p4="http://www.w3.org/2001/XMLSchema-instance">
  39:             {... - use id of note field defined above}
  40:           </Value>
  41:         </Property>
  42:       </ArrayOfProperty>
  43:     </Customization>
  44:   </Field>
  45:   <Field Type="Note"
  46:     DisplayName="Field2TaxHTField"
  47:     MaxLength="255"
  48:     Group="Test"
  49:     ID="..."
  50:     StaticName="Field2TaxHTField"
  51:     Name="Field2TaxHTField"
  52:     Hidden="TRUE"
  53:     ShowInViewForms="FALSE"
  54:     Description="" />
  55:   <Field ID="..."
  56:     SourceID="http://schemas.microsoft.com/sharepoint/v3"
  57:     Type="TaxonomyFieldType"
  58:     DisplayName="Field2"
  59:     ShowField="Term1033"
  60:     Required="FALSE"
  61:     EnforceUniqueValues="FALSE"
  62:     Group="Test"
  63:     StaticName="Field2"
  64:     Name="Field2"
  65:     Hidden="FALSE"
  66:     Mult="TRUE">
  67:     <Default></Default>
  68:     <Customization>
  69:       <ArrayOfProperty>
  70:         <Property>
  71:           <Name>IsPathRendered</Name>
  72:           <Value xmlns:q7="http://www.w3.org/2001/XMLSchema"
  73: p4:type="q7:boolean" xmlns:p4="http://www.w3.org/2001/XMLSchema-instance">
  74:             true
  75:           </Value>
  76:         </Property>
  77:         <Property>
  78:           <Name>TextField</Name>
  79:           <Value xmlns:q6="http://www.w3.org/2001/XMLSchema"
  80: p4:type="q6:string" xmlns:p4="http://www.w3.org/2001/XMLSchema-instance">
  81:             {...- use id of note field defined above}
  82:           </Value>
  83:         </Property>
  84:       </ArrayOfProperty>
  85:     </Customization>
  86:   </Field>
  87:   <Field Type="Note"
  88:     DisplayName="Field3TaxHTField"
  89:     MaxLength="255"
  90:     Group="Test"
  91:     ID="..."
  92:     StaticName="Field3TaxHTField"
  93:     Name="Field3TaxHTField"
  94:     Hidden="TRUE"
  95:     ShowInViewForms="FALSE"
  96:     Description="" />
  97:   <Field ID="..."
  98:     SourceID="http://schemas.microsoft.com/sharepoint/v3"
  99:     Type="TaxonomyFieldType"
 100:     DisplayName="Field3"
 101:     ShowField="Term1033"
 102:     Required="FALSE"
 103:     EnforceUniqueValues="FALSE"
 104:     Group="Test"
 105:     StaticName="Field3"
 106:     Name="Field3"
 107:     Hidden="FALSE"
 108:     Mult="TRUE">
 109:     <Default></Default>
 110:     <Customization>
 111:       <ArrayOfProperty>
 112:         <Property>
 113:           <Name>IsPathRendered</Name>
 114:           <Value xmlns:q7="http://www.w3.org/2001/XMLSchema"
 115: p4:type="q7:boolean" xmlns:p4="http://www.w3.org/2001/XMLSchema-instance">
 116:             false
 117:           </Value>
 118:         </Property>
 119:         <Property>
 120:           <Name>TextField</Name>
 121:           <Value xmlns:q6="http://www.w3.org/2001/XMLSchema"
 122: p4:type="q6:string" xmlns:p4="http://www.w3.org/2001/XMLSchema-instance">
 123:             {... - use id of note field defined above}
 124:           </Value>
 125:         </Property>
 126:       </ArrayOfProperty>
 127:     </Customization>
 128:   </Field>
 129: </Elements>

As I wrote above this part is the same as for on-premise Sharepoint, so I won’t comment it.

The last step is to bind created managed metadata fields to the term sets. It can be done via the following PowerShell script:

   1: function Bind-Managed-Metadata-Field($ctx, $termStoreId, $fieldId, $termSetId)
   2: {
   3:     $rootWeb = $ctx.Web
   4:     $fields = $rootWeb.Fields
   5:     $ctx.Load($fields)
   6:     $ctx.ExecuteQuery()
   7:  
   8:     try
   9:     {
  10:         $field = $fields.GetById($fieldId)
  11:     }
  12:     catch
  13:     {
  14:         Write-Host "Field" $fieldId "not found in site columns collection."
  15: "It won't be bound to the term set" -foregroundcolor red
  16:         return
  17:     }
  18:  
  19:     $taxField = [Microsoft.SharePoint.Client.ClientContext].GetMethod("CastTo").
  20: MakeGenericMethod([Microsoft.SharePoint.Client.Taxonomy.TaxonomyField]).
  21: Invoke($ctx, $field)
  22:     $taxField.SspId = $termStoreId
  23:     $taxField.TermSetId = $termSetId
  24:     $taxField.TargetTemplate = ""
  25:     $taxField.AnchorId = [System.Guid]::Empty
  26:     $taxField.UpdateAndPushChanges($true)
  27:     $ctx.ExecuteQuery()
  28:     Write-Host "Field" $fieldId "was successfully bound to termset"
  29: $termSetId -foregroundcolor green
  30: }
  31:  
  32: function Bind-Managed-Metadata-Fields($ctx, $xmlFilePath)
  33: {
  34:     Write-Host "Binding managed metadata fields to term sets"
  35: -foregroundcolor green
  36:     [xml]$xmlContent = (Get-Content $xmlFilePath)
  37:     if (-not $xmlContent)
  38:     {
  39:         Write-Host "Xml was not loaded successfully. "
  40: "Fields won't be bound to term sets" -foregroundcolor Red
  41:         return
  42:     }
  43:     $termStore = Get-TermStore $ctx
  44:     $groups = $termStore.Groups
  45:     $ctx.Load($groups)
  46:     $ctx.ExecuteQuery()
  47:     $group = $groups | Where-Object {$_.Name -eq $xmlContent.Group.Name}
  48:     if (-not $group)
  49:     {
  50:         Write-Host "Group" $xmlContent.Group.Name "not found. "
  51: "Fields won't be bound to term sets" -foregroundcolor Red
  52:         return
  53:     }
  54:  
  55:     Bind-Managed-Metadata-Field $ctx $termStore.Id "{field1 id}" "{term set1 id}"
  56:     Bind-Managed-Metadata-Field $ctx $termStore.Id "{field2 id}" "{term set2 id}"
  57:     Bind-Managed-Metadata-Field $ctx $termStore.Id "{field2 id}" "{term set3 id}"
  58: }

Method Bind-Managed-Metadata-Field() which is shown on lines 1-30 makes the actual binding. Its code is quite obvious, the only interesting thing is how to call clientContext.CastTo<TaxonomyField>() generic method in PowerShell. It is shown on lines 19-21. As we know ids of the term sets (see above) we may just specify these ids when call Bind-Managed-Metadata-Field() for our fields (lines 55-57). After that managed metadata fields will be bound to the term sets and you will be able to define values for these fields when create or update content.

As you can see for Sharepoint Online regular tasks are implemented in different way, but if you work with on-premise Sharepoint installations, there should not be a lot of problems to move to client object model. Hope that this information will help you in your work.

Sunday, July 13, 2014

Use custom search results page in Search box web part with contextual scopes in Sharepoint

As you probably know OTB Search box web part allows user to specify search keywords and perform search across configured search scopes. There are 2 types of scopes:

  • contextual – means that they depend on the current context, i.e. on where user is currently located on the site. Examples of contextual scopes are “This site” (search will be done in the current site and its sub sites) and “This list” (search will be done only in the current list or doclib);
  • custom – scopes configured in Central administration. Examples are “All sites” (search will be done across all Sharepoint sites in specified content source) and “People” (search will be done only across people).

Both contextual and custom scopes are shown in the scopes dropdown list near the text box in Search box web part. It is possible to control what scopes are shown there. E.g. in “Dropdown mode” property we may specify one of the following values:

  • Do not show scopes drop down
  • Show scopes drop down
  • Show, and default to ‘s’ URL parameter
  • Show and default to contextual scope
  • Show, do not include contextual scopes
  • Show, do not include contextual scopes, and default to ‘s’ URL parameter

They are quite self-explanative. The problem however is that using this property we may only hide all scopes or hide only contextual scopes. If we want to leave only contextual scopes and hide all custom scopes we need to set another web part property “Scope display group” to empty value. Scope display groups themselves are configured in Site settings > Search scopes and include custom scopes configured in Central administration.

By default search box web part redirects user to OTB OSSSearchResults.aspx application layout page which uses default application.master master page and as result has OTB look and feel. If we want to use custom look and feel we need either to use search center site collection or create new publishing page on the site and add Search core results web part on it. After that we need to specify URL of this page in the “Target search results page URL” property of Search box web part. The problem is that this property is used only for custom scopes, but not for contextual scopes. For the last ones it is ignored and user is anyway redirected to the OTB OSSSearchResults.aspx page.

In order to fix this issue we may use the following solution: create custom web part which inherits OTB SearchBoxEx control and sets its private variable “m_strOssSearchResultsUrl” via reflection (exactly this variable contains URL of the page used for search in contextual scopes):

   1: public class SearchBoxEx : Microsoft.SharePoint.Portal.WebControls.SearchBoxEx
   2: {
   3:     protected override void OnPreRender(EventArgs e)
   4:     {
   5:         if (!string.IsNullOrEmpty(this.SearchResultPageURL))
   6:         {
   7:             ReflectionHelper.SetPrivateField(this, this.GetType().BaseType,
   8:                 "m_strOssSearchResultsUrl", this.SearchResultPageURL);
   9:         }
  10:         base.OnPreRender(e);
  11:     }
  12: }

Here we used ReflectionHelper class:

   1: public static class ReflectionHelper
   2: {
   3:     public static object CallMethod(object obj, string name,
   4:         params object[] argv)
   5:     {
   6:         BindingFlags bf = 0 | BindingFlags.Instance | BindingFlags.Static |
   7:             BindingFlags.Public | BindingFlags.NonPublic;
   8:         MethodInfo mi = obj.GetType().FindMembers(MemberTypes.Method, bf,
   9:             Type.FilterName, name)[0] as MethodInfo;
  10:         return mi.Invoke(obj, argv);
  11:     }
  12:  
  13:     public static void SetPrivateField(object obj, string name,
  14:         object val)
  15:     {
  16:         SetPrivateField(obj, obj.GetType(), name, val);
  17:     }
  18:  
  19:     public static void SetPrivateField(object obj, Type type,
  20:         string name, object val)
  21:     {
  22:         BindingFlags bf = 0 | BindingFlags.Instance | BindingFlags.Static |
  23:             BindingFlags.Public | BindingFlags.NonPublic;
  24:         FieldInfo mi = type.FindMembers(MemberTypes.Field, bf, Type.FilterName,
  25:             name)[0] as FieldInfo;
  26:         mi.SetValue(obj, val);
  27:     }
  28:  
  29:     public static object GetPrivateField(object obj, string name)
  30:     {
  31:         BindingFlags bf = 0 | BindingFlags.Instance | BindingFlags.Static |
  32:             BindingFlags.Public | BindingFlags.NonPublic;
  33:         FieldInfo mi = obj.GetType().FindMembers(MemberTypes.Field, bf,
  34:             Type.FilterName, name)[0] as FieldInfo;
  35:         return mi.GetValue(obj);
  36:     }
  37:  
  38:     public static object CallStaticMethod(Type t, string name,
  39:         params object[] argv)
  40:     {
  41:         BindingFlags bf = 0 | BindingFlags.Instance | BindingFlags.Static |
  42:             BindingFlags.Public | BindingFlags.NonPublic;
  43:         MethodInfo mi = t.FindMembers(MemberTypes.Method, bf, Type.FilterName,
  44:             name)[0] as MethodInfo;
  45:         return mi.Invoke(null, argv);
  46:     }
  47:  
  48:     public static object GetStaticData(Type t, string name)
  49:     {
  50:         BindingFlags bf = 0 | BindingFlags.Instance | BindingFlags.Static |
  51:             BindingFlags.Public | BindingFlags.NonPublic;
  52:         FieldInfo fi = t.FindMembers(MemberTypes.Field, bf, Type.FilterName,
  53:             name)[0] as FieldInfo;
  54:         return fi.GetValue(null);
  55:     }
  56: }

After that it will use custom search page URL also for contextual scopes.

Friday, July 11, 2014

Create Sharepoint sites with web templates from sandbox solution using PowerShell

In order to create sub sites in Sharepoint site collection we may use New-SPWeb cmdlet. It has Template parameter in which we may specify what template should be used for the new site. This parameter has SPWebTemplatePipeBind type, i.e. instead of passing string to it we need to get instance of appropriate web template first. If you remember OTB web template names differ from custom web templates provisioned via features: for OTB they look like “template name#id”, e.g. “STS#0” (also it looks like this for custom site templates provisioned in old style via copying onet.xml directly to SiteTemplates sub folder in Sharepoint hive, not via features), while for custom web templates name look like “feature id#template name”, e.g. “{e7fc7957-cfdc-47d7-8afa-3cb8dc3f199e}#Test”.

One of the way to get web template instance in PowerShell it is to use another cmdlet Get-SPWebTemplate. Unfortunately it doesn’t work for custom web templates provisioned with sandbox solution. In this case it will always show “The web template does not exist” error.

Another try may be to get instance from list of all web templates try to create site with it:

   1: $template = $site.GetWebTemplates($lang) | Where-Object {$_.Name -eq $templateName}
   2: $web = New-SPWeb -Url $url -name $title -Template $template -Language $lang

However although web template instance will be correctly retrieved in this example, call to New-SPWeb will show the following warning:

WARNING: Template is not found and is not applied.

Site will be created, but when you will try to navigate to the created web, OTB template picker will be shown and will ask you to choose web template.

In order to avoid this problem we need to create sub site without template, and then call SPWeb.ApplyWebTemplate() method on the created instance:

   1: $template = $site.GetWebTemplates($lang) | Where-Object {$_.Name -eq $templateName}
   2: $web = New-SPWeb -Url $url -name $title -Language $lang
   3: $web.ApplyWebTemplate($template.Name)

After that sub site should be created successfully from custom web template which is provisioned with sandbox solution. Instead of SPSite.GetWebTemplates() method which returns list of all available web templates you may use SPWeb.GetAvailableWebTemplates() which returns list of allowed web templates for specific site. However sometimes from PowerShell you need to create sites using those web templates which are not allowed for regular users for creating sites from UI. In this case SPSite.GetWebTemplates() method should be used.

Wednesday, July 2, 2014

Fix “Internet Explorer cannot display the webpage” when create new web application in Sharepoint 2013

When create new web application via Central administration in Sharepoint 2013 you may face with the following error: after some time browser shows the error message “Internet Explorer cannot display the webpage”. IIS site and content database are often created successfully, but Ii f you will check virtual directory for the failed web applicatin in c:\inetpub\wwwroot\VirtualDirectories\wss\…, it will be empty. It happens because Sharepoint needs more time for creating new web application than timeout limit set for the application pool of Central administration. In order to fix the problem go to IIS manager > Application pools > SharePoint Central Administration v4 and click Advanced properties. In the opened window under Process model find the following settings:

  • Ping Maximum Response Time
  • Shutdown Time Limit (seconds)
  • Startup Time Limit (seconds)

and change default value 90 to the greater value for all of them:

image

After that try to create new web application again. This time it should be created successfully.