Sunday, September 28, 2014

Programmatically attach and detach event receivers to specific Sharepoint list safely

In Sharepoint development we often need to attach/detach event receivers to specific list (by specific list I mean single list on the web site where feature is activated, not to all lists created with particular list template). So I decided to make this post for future references also for myself. As title says it will show how to attach event receivers to particular event type (e.g. ItemAdded or ItemUpdated) safely, i.e. if feature will be activated several time with force flag, the same event receiver won’t be attached again and as result it won’t be triggered several times which may cause problems on your environment.

We will need feature with feature receiver: attaching of list event receiver will be made in FeatureActivated() method and detaching in FeatureDeactivating():

   1: public class TestFeatureReceiver : SPFeatureReceiver
   2: {
   3:     public override void FeatureActivated(SPFeatureReceiverProperties properties)
   4:     {
   5:         var web = properties.Feature.Parent as SPWeb;
   6:         this.addEventReceiverToSitesList(web);
   7:     }
   8:  
   9:     private void addEventReceiverToSitesList(SPWeb web)
  10:     {
  11:         var list = web.Lists.Cast<SPList>().FirstOrDefault(l => l.Title == "test")
  12:         if (list == null)
  13:         {
  14:             return;
  15:         }
  16:  
  17:         bool listWasChanged = false;
  18:         this.ensureEventReceiverAdded(web, list.EventReceivers,
  19:             SPEventReceiverType.ItemAdded, ref listWasChanged);
  20:         this.ensureEventReceiverAdded(web, list.EventReceivers,
  21:             SPEventReceiverType.ItemUpdated, ref listWasChanged);
  22:         if (listWasChanged)
  23:         {
  24:             list.Update();
  25:         }
  26:     }
  27:  
  28:     private void ensureEventReceiverAdded(SPWeb web,
  29:         SPEventReceiverDefinitionCollection receivers, SPEventReceiverType type,
  30:         ref bool wasChanged)
  31:     {
  32:         if (receivers == null)
  33:         {
  34:             return;
  35:         }
  36:  
  37:         if (receivers.Cast<SPEventReceiverDefinition>().Any(e => e.Type == type &&
  38:             e.Assembly == typeof(TestEventReceiver).Assembly.FullName &&
  39:             e.Class == typeof(TestEventReceiver).FullName))
  40:         {
  41:             // event receiver is already attached for specified event type
  42:         }
  43:         else
  44:         {
  45:             receivers.Add(type, typeof(TestEventReceiver).Assembly.FullName,
  46:                 typeof(TestEventReceiver).FullName);
  47:             wasChanged = true;
  48:         }
  49:     }
  50:  
  51:     public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
  52:     {
  53:             var web = properties.Feature.Parent as SPWeb;
  54:             this.removeEventReceiverFromSitesList(web);
  55:     }
  56:  
  57:     private void removeEventReceiverFromSitesList(SPWeb web)
  58:     {
  59:             var list = web.Lists.Cast<SPList>().FirstOrDefault();
  60:             if (list == null)
  61:             {
  62:                 return;
  63:             }
  64:  
  65:             bool listWasChanged = false;
  66:             this.ensureEventReceiverRemoved(list.EventReceivers,
  67:                 SPEventReceiverType.ItemAdded, ref listWasChanged);
  68:             this.ensureEventReceiverRemoved(list.EventReceivers,
  69:                 SPEventReceiverType.ItemUpdated, ref listWasChanged);
  70:             if (listWasChanged)
  71:             {
  72:                 list.Update();
  73:             }
  74:     }
  75:  
  76:     private void ensureEventReceiverRemoved(
  77:         SPEventReceiverDefinitionCollection receivers, SPEventReceiverType type,
  78:         ref bool wasChanged)
  79:     {
  80:         if (receivers == null)
  81:         {
  82:             return;
  83:         }
  84:  
  85:         var itemAddedReceiver = receivers.Cast<SPEventReceiverDefinition>().
  86:             FirstOrDefault(e => e.Type == type &&
  87:                 e.Assembly == typeof(TestEventReceiver).Assembly.FullName &&
  88:                 e.Class == typeof(TestEventReceiver).FullName);
  89:         if (itemAddedReceiver != null)
  90:         {
  91:             itemAddedReceiver.Delete();
  92:             wasChanged = true;
  93:         }
  94:     }
  95: }

Like shown in example above before to add or remove event receiver for specified event type it checks first that event receiver is attached to the list or not. I.e. it will be safe to activate this feature multiple times with force parameter on the same web site and then deactivate it. Also using this technique it will be possible to safely attach event receivers to another event types after feature was already activated: it won’t attach the same receiver to existing events and will only attach it for new ones.

Thursday, September 25, 2014

Create managed metadata term sets via Sharepoint client object model in Sharepoint Online

During provisioning to Sharepoint Online we usually try to automate as much actions as possible. One of such actions is creation of managed metadata hierarchy (Group > Term sets > Terms). In on-premise installation with full access to the basic object model this is straightforward process nowdays, but in Sharepoint Online we need to use client object model instead. Before to start you should ensure that account under which code will run is added to Term store administrators group (in order to check it on the root site of your site collection go to Site settings > Term store management). Also we will need xml file with initial managed metadata hierarchy in the following format:

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <Group Name="MyGroup" Id="{C16C65EA-446B-45F3-B232-FA4212C8E621}">
   3:   <TermSet Name="TermSet1" Id="{D4ABDBD7-9E60-4493-9A09-F90AB06B812E}">
   4:     <Term Name="Item11" />
   5:     <Term Name="Item12" />
   6:     <Term Name="Item13" />
   7:   </TermSet>
   8:   <TermSet Name="TermSet2" Id="{30BE6983-F40C-4D96-8413-10BDB920D31B}">
   9:     <Term Name="Item21" />
  10:     <Term Name="Item22" />
  11:     <Term Name="Item23" />
  12:   </TermSet>
  13:   <TermSet Name="TermSet3" Id="{AD589539-8875-4E8E-BAAE-C0223E3B0124}">
  14:     <Term Name="Item31">
  15:       <Term Name="SubItem1" />
  16:       <Term Name="SubItem2" />
  17:       <Term Name="SubItem3" />
  18:     </Term>
  19:     <Term Name="Item32" />
  20:     <Term Name="Item33" />
  21:   </TermSet>
  22: </Group>

As shown in the example above ids for groups and term sets are explicitly specified. They will be needed for binding managed metadata fields – I will write about it in another article. Also in this example only 2 levels of nested terms are shown, but you may use any nesting level, just ensure that xml if formatted correctly.

PowerShell script which creates taxonomy hierarchy from the xml file in the local site collection term store is shown below:

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

It is quite self-descriptive: at first it creates group, then for each term set xml tag creates appropriate term set and then terms with sub terms. Function Provision-TermSets is called this way:

   1: $ctx = New-Object Microsoft.SharePoint.Client.ClientContext("http://example.com")
   2: $ctx.AuthenticationMode =
   3:     [Microsoft.SharePoint.Client.ClientAuthenticationMode]::Default
   4: $securePassword = ConvertTo-SecureString "password" -AsPlainText -Force
   5: $credentials =
   6:     New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials(
   7:         "username", $securePassword)
   8: $ctx.Credentials = $credentials
   9:  
  10: Provision-TermSets $ctx "c:/temp/termsets.xml"

After executing go to Site settings > Term store management and check that your managed metadata hierarchy is successfully created.