Sunday, July 10, 2011

Provision hierarchical managed metadata via feature receiver in Sharepoint

Provisioning of the managed metadata during creation of site collection is quite often requirement in Sharepoint development. It is common case when with site collection or web site creation you need to provision some initial content in order to simplify work of the content producers (e.g. content pages, lookup lists with data, etc). Metadata also can be provisioned during site collection creation via feature activation. E.g. customers may provide you default metadata in xml file and you need to create taxonomy based on this file. In this post I will show how to do it.

First of all we need to create a feature itself. In order to do this we need to determine the scope of our feature. As we want to provision managed metadata with site collection creation our feature will have Site scope. It is convenient because we can add it into onet.xml of custom site definitions (into <SiteFeatures> section of appropriate template). But you also need to note that content producers may change the metadata after it was provisioned. The simplest solution which can be used in order to prevent overriding of the user changes is to check that metadata is not provisioned yet (e.g. check that managed metadata group doesn’t exist). More complicated solution is to support merge of the term sets and terms. In my example simple all or nothing approach will be used.

Now let’s return to our feature. Here its feature.xml file:

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <Feature
   3:     Id="2FCC2C0B-A5D9-480B-B15B-189B5755185B"
   4:     Title="Managed metadata example"
   5:     Description=""
   6:     Version="1.0.0.0"
   7:     Scope="Site"
   8:     Hidden="FALSE"
   9:     ReceiverAssembly="ManagedMetadataProgrammaticalExample, Version=1.0.0.0, Culture=neutral, PublicKeyToken=d71c4ad7a0705ced"
  10:     ReceiverClass="ManagedMetadataProgrammaticalExample.TermSetsFeatureReceiver"
  11:     xmlns="http://schemas.microsoft.com/sharepoint/">
  12:     <ElementManifests>
  13:         <ElementFile Location="DefaultMetadata.xml"/>
  14:     </ElementManifests>
  15: </Feature>

It has feature receiver which will make actual work, i.e. it will parse xml file and create taxonomy in your Managed metadata service application. Also it has reference to the DefaultMetadata.xml file which contains default taxonomy. E.g. default metadata for the internet shop may look like this (for simplicity I use only couple of terms):

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <TermSets>
   3:   <TermSet Name="Categories">
   4:     <Term Name="Books">
   5:       <Term Name="IT">
   6:         <Term Name="Sharepoint" />
   7:         <Term Name="ASP.Net MVC" />
   8:         <Term Name=".Net" />
   9:       </Term>
  10:       <Term Name="Math">
  11:         <Term Name="Algebra" />
  12:         <Term Name="Geometry" />
  13:         <Term Name="Math analysis" />
  14:       </Term>
  15:     </Term>
  16:     <Term Name="Films">
  17:       <Term Name="The Big Bang Theory" />
  18:       <Term Name="Haus MD" />
  19:       <Term Name="Numb3rs" />
  20:     </Term>
  21:   </TermSet>
  22: </TermSets>

Although this example xml file contains one term set – code shown below will also work for multiple term sets case. Provisioning of this metadata will be performed in 2 steps:

  1. Parse xml and store data in object representation
  2. Create term sets and terms

We will need 2 helper DTO classes:

   1: public class TermSetDTO
   2: {
   3:     public string Name { get; set; }
   4:     public List<TermDTO> Terms { get; set; }
   5: }
   6:  
   7: public class TermDTO
   8: {
   9:     public string Name { get; set; }
  10:     public List<TermDTO> Terms { get; set; }
  11: }

As you can see these are simple representation of OTB TermSet and Term entities.

Now let’s start from the code of the feature receiver:

   1: public class TermSetsFeatureReceiver : SPFeatureReceiver
   2: {
   3:     public override void FeatureActivated(SPFeatureReceiverProperties properties)
   4:     {
   5:         var site = properties.Feature.Parent as SPSite;
   6:         if (site == null)
   7:         {
   8:             return;
   9:         }
  10:  
  11:         SPSecurity.RunWithElevatedPrivileges(
  12:             () =>
  13:                 {
  14:                     using (var elevatedSite = new SPSite(site.ID, site.Zone))
  15:                     {
  16:                         string defaultMetadataFilePath = Path.Combine(properties.Feature.Definition.RootDirectory, "DefaultMetadata.xml");
  17:                         var termSetsService = new TermSetsService();
  18:                         termSetsService.CreateTermSetsFromXml(elevatedSite, defaultMetadataFilePath);
  19:                     }
  20:                 });
  21:     }
  22: }

Very important moment: the code is executed under elevated privileges. It means that you will need to add account of the application pool under which this code is executed (if you create site collection via UI in Central Administration – it will be account of the CA application pool) to the Term store administrators in Managed metadata service application.

The code of the feature receiver is quite simple – it passes path to the DefaultMetadata.xml file to the TermSetsService instance which makes all actual work. It is another useful pattern – decouple functionality from Sharepoint artifacts:

   1: public class TermSetsService
   2: {
   3:     private const string TERM_SET_TAG = "TermSet";
   4:     private const string TERM_TAG = "Term";
   5:     private const string NAME_ATTR = "Name";
   6:     private const string GROUP_NAME = "Shop";
   7:     private const int ENGLISH_LOCALE_ID = 1033;
   8:  
   9:     public void CreateTermSetsFromXml(SPSite site, string fileName)
  10:     {
  11:         TermStore termStore = null;
  12:         try
  13:         {
  14:             var taxonomySession = new TaxonomySession(site);
  15:             termStore = taxonomySession.DefaultKeywordsTermStore;
  16:  
  17:             if (termStore == null)
  18:             {
  19:                 return;
  20:             }
  21:  
  22:             if (!termStore.Groups.IsNullOrEmpty() && termStore.Groups.Any(g => g.Name == GROUP_NAME))
  23:             {
  24:                 return;
  25:             }
  26:  
  27:             var taxonomyGroup = termStore.CreateGroup(GROUP_NAME);
  28:             if (taxonomyGroup == null)
  29:             {
  30:                 return;
  31:             }
  32:  
  33:             // load terms sets from xml file
  34:             var termSets = this.load(fileName);
  35:             if (termSets.IsNullOrEmpty())
  36:             {
  37:                 return;
  38:             }
  39:  
  40:             createTermSets(taxonomyGroup, termSets);
  41:  
  42:             termStore.CommitAll();
  43:         }
  44:         catch (Exception x)
  45:         {
  46:             if (termStore != null)
  47:             {
  48:                 termStore.RollbackAll();
  49:             }
  50:             throw;
  51:         }
  52:     }
  53:  
  54:     private List<TermSetDTO> load(string fileName)
  55:     {
  56:         try
  57:         {
  58:             var result = new List<TermSetDTO>();
  59:  
  60:             XDocument doc = XDocument.Load(fileName);
  61:             foreach (var termSetElement in doc.Descendants(TERM_SET_TAG))
  62:             {
  63:                 var nameAttr = termSetElement.Attributes().FirstOrDefault(a => a.Name == NAME_ATTR);
  64:                 if (nameAttr == null)
  65:                 {
  66:                     continue;
  67:                 }
  68:  
  69:                 string termSetName = nameAttr.Value;
  70:  
  71:                 var termSet = new TermSetDTO { Name = termSetName };
  72:                 termSet.Terms = this.getTerms(termSetElement);
  73:                 result.Add(termSet);
  74:             }
  75:  
  76:             return result;
  77:         }
  78:         catch (Exception x)
  79:         {
  80:             return new List<TermSetDTO>();
  81:         }
  82:     }
  83:  
  84:     private List<TermDTO> getTerms(XElement e)
  85:     {
  86:         try
  87:         {
  88:             if (e == null)
  89:             {
  90:                 return null;
  91:             }
  92:  
  93:             var terms = new List<TermDTO>();
  94:             foreach (var termElement in e.Elements(TERM_TAG))
  95:             {
  96:                 var nameAttr = termElement.Attributes().FirstOrDefault(a => a.Name == NAME_ATTR);
  97:                 if (nameAttr == null)
  98:                 {
  99:                     continue;
 100:                 }
 101:  
 102:                 var term = new TermDTO {Name = nameAttr.Value};
 103:                 term.Terms = this.getTerms(termElement);
 104:                 terms.Add(term);
 105:             }
 106:             return terms;
 107:         }
 108:         catch (Exception x)
 109:         {
 110:             return new List<TermDTO>();
 111:         }
 112:     }
 113:  
 114:     private void createTermSets(Group taxonomyGroup, List<TermSetDTO> termSets)
 115:     {
 116:         if (taxonomyGroup == null)
 117:         {
 118:             return;
 119:         }
 120:  
 121:         if (termSets == null)
 122:         {
 123:             return;
 124:         }
 125:  
 126:         // create managed metadata term set for each term set dto
 127:         foreach (var ts in termSets)
 128:         {
 129:             if (taxonomyGroup.TermSets.Any(t => t.Name == ts.Name))
 130:             {
 131:                 continue;
 132:             }
 133:  
 134:             var termSet = taxonomyGroup.CreateTermSet(ts.Name);
 135:             if (termSet == null)
 136:             {
 137:                 continue;
 138:             }
 139:  
 140:             this.createTerms(termSet, ts.Terms);
 141:         }
 142:     }
 143:  
 144:     private void createTerms(TermSet termSet, List<TermDTO> terms)
 145:     {
 146:         if (termSet == null)
 147:         {
 148:             return;
 149:         }
 150:  
 151:         if (terms.IsNullOrEmpty())
 152:         {
 153:             return;
 154:         }
 155:  
 156:         foreach (var term in terms)
 157:         {
 158:             if (termSet.Terms.Any(t => t.Name == term.Name))
 159:             {
 160:                 continue;
 161:             }
 162:             var parentTerm = termSet.CreateTerm(term.Name, ENGLISH_LOCALE_ID);
 163:             this.createTerms(parentTerm, term.Terms);
 164:         }
 165:     }
 166:  
 167:     private void createTerms(Term parentTerm, List<TermDTO> terms)
 168:     {
 169:         if (parentTerm == null)
 170:         {
 171:             return;
 172:         }
 173:  
 174:         if (terms.IsNullOrEmpty())
 175:         {
 176:             return;
 177:         }
 178:  
 179:         foreach (var term in terms)
 180:         {
 181:             if (parentTerm.Terms.Any(t => t.Name == term.Name))
 182:             {
 183:                 continue;
 184:             }
 185:             var subTerm = parentTerm.CreateTerm(term.Name, ENGLISH_LOCALE_ID);
 186:             this.createTerms(subTerm, term.Terms);
 187:         }
 188:     }
 189: }

The code is quite obvious: at first it recursively parses xml file and loads result to the DTO objects defined above (see load() and getTerms() methods). Then it creates taxonomy in Term store using object model (see createTermSets() and createTerms() methods) – also recursively (you need to add reference to the Micosoft.SharePoint.Taxonomy.dll assembly in order to compile this code).

Also in order to compile it – you will need to add extension method IsNullOrEmpty() for IEnumerable<T> described by Phil Haack in his blog post.

This code uses English local (lcid = 1033) for default term sets. But you can extend it in order to provide translations for your terms. Also it can be extended in order to add support of the synonyms provisioning.

When you will deploy wsp and activate feature in your site collection – it will create hierarchical taxonomy in the Term store:

image

That’s how you can provision taxonomy with site collection creation. Hope it will help you in your work.

No comments:

Post a Comment