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.

No comments:

Post a Comment