Tuesday, April 13, 2010

SP 2010 - Adding Metadata Columns to Content Type Programatically

I have a feature that provisions a few Site Columns and a Content Type that will use those Site Columns. This was easy enough to achieve with some XML packaged into a Feature. However, my Content Type also needed to have a Managed Metadata column, which doesn't work with XML.

The reasons you can't do a Managed Metadata Site Column through XML are the same reasons Lookup columns don't work - the GUIDs that refer to the underlying objects change between environments, so you can't hardcode them into the XML. If you wanted to write the XML by hand to do this and already knew the GUIDs, it would of course work, but that's more effort than it's worth for such a limited solution.

My first thought when trying this was that I knew when I had exported my Content Type that I created through the GUI I found out that my Managed Metadata Site Column as provisioned ended up being several different SPFields under the covers. So in my first pass, I tried to create all of these fields. That ended up breaking my site because I was unable to remove all the SPFields/SPContentTypes after the fact!

Well, it turns out the answer isn't so hard after you try all the wrong ways first. I ended up having my Feature that provisions the base Site Columns and Content Type via XML. I then created a Feature Receiver that creates the Managed Metadata Site Column and then adds it to the Content Type. Here is the code:

public enum Operation
{
 Add,
 Remove
}

[Guid("367cb65f-cd86-440a-8f39-1bfa2a9ab1f6")]
public class MyContentTypeEventReceiver : SPFeatureReceiver
{
 /*
  * On feature activation, we are going to provision a site column that points to the Managed MetaData Service used by this site.
  * We have to do this because managed metadata columns work like lookups do under the covers and are keyed to the store they were
  * created with.
  * 
  * After creating the site column, we will then update our content type to include the new site column.
  */
 public override void FeatureActivated(SPFeatureReceiverProperties properties)
 {
  ProvisionMetadataSiteColumn("Managed Metadata Service", "MyGroup", "MyTermSet", "MyField", "MyFieldGroup", true, true);
  UpdateContentType("MyContentType", "MyField", Operation.Add);
 }

 public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
 {
  UpdateContentType("MyContentType", "MyField", Operation.Remove);
  RemoveSiteColumn("MyField");
 }

 #region Helper Methods

 private bool ProvisionMetadataSiteColumn(string termStoreName, string termGroupName, string termSetName, string fieldName, string fieldGroupName, bool isRequired, bool allowMultipleValues)
 {
  bool added = false;
  using(SPSite site = SPContext.Current.Site)
  {
   if(GetSiteColumn(site.RootWeb, fieldName) == null)
   {
    //this can time out which would cause it to be returned with no term stores; might be a fluke or very situational
    TaxonomySession session = new TaxonomySession(site);

    TermStore termStore = GetTermStore(session, termStoreName);
    if (termStore != null)
    {
     Group group = GetTermGroup(termStore, termGroupName);
     if (group != null)
     {
      TermSet termSet = GetTermSet(group, termSetName);
      if (termSet != null)
      {
       string fieldType = (allowMultipleValues ? "TaxonomyFieldTypeMulti" : "TaxonomyFieldType");

       TaxonomyField field = (TaxonomyField)site.RootWeb.Fields.CreateNewField(fieldType, fieldName);
       field.SspId = termStore.Id;
       field.TermSetId = termSet.Id;
       field.AllowMultipleValues = allowMultipleValues;
       field.Group = fieldGroupName;
       field.Required = isRequired;

       site.RootWeb.Fields.Add(field);
       site.RootWeb.Update();

       added = true;
      }
     }
    }
   }
  }

  return added;
 }

 private void UpdateContentType(string contentTypeName, string fieldName, Operation operation)
 {
  using (SPSite site = SPContext.Current.Site)
  {
   SPContentType contentType = GetContentType(site.RootWeb, contentTypeName);
   if (contentType != null)
   {
    SPField field = GetSiteColumn(site.RootWeb, fieldName);
    if (field != null)
    {
     bool hasFieldLink = HasFieldLink(contentType, field.Id);

     if (operation == Operation.Add && !hasFieldLink)
     {
      SPFieldLink link = new SPFieldLink(field);
      contentType.FieldLinks.Add(link);
      contentType.Update();
     }
     else if (operation == Operation.Remove && hasFieldLink)
     {
      contentType.FieldLinks.Delete(field.Id);
      contentType.Update();
     }
    }
   }
  }
 }

 private bool RemoveSiteColumn(string fieldName)
 {
  bool deleted = false;

  using(SPSite site = SPContext.Current.Site)
  {
   SPField field = GetSiteColumn(site.RootWeb, fieldName);
   if (field != null)
   {
    try
    {
     field.Delete();
     deleted = true;
    }
    catch { }
   }
  }

  return deleted;
 }

 #endregion

 #region Helper Methods to allow NULL checks with SharePoint

 private SPField GetSiteColumn(SPWeb web, Guid fieldId)
 {
  SPField field = null;

  try
  {
   field = web.Fields[fieldId];
  }
  catch { }

  return field;
 }

 private SPField GetSiteColumn(SPWeb web, string fieldName)
 {
  SPField field = null;

  try
  {
   field = web.Fields[fieldName];
  }
  catch { }

  return field;
 }

 private SPContentType GetContentType(SPWeb web, string contentTypeName)
 {
  SPContentType contentType = null;

  try
  {
   contentType = web.ContentTypes[contentTypeName];
  }
  catch { }

  return contentType;
 }

 private bool HasFieldLink(SPContentType contentType, Guid fieldId)
 {
  bool found = false;
  foreach (SPFieldLink fl in contentType.FieldLinks)
  {
   if (fl.Id == fieldId)
   {
    found = true;
    break;
   }
  }

  return found;
 }

 private TermStore GetTermStore(TaxonomySession session, string termStoreName)
 {
  TermStore termStore = null;

  try
  {
   termStore = session.TermStores[termStoreName];
  }
  catch { }

  return termStore;
 }

 private Group GetTermGroup(TermStore termStore, string termGroupName)
 {
  Group group = null;

  try
  {
   group = termStore.Groups[termGroupName];
  }
  catch { }

  return group;
 }

 private TermSet GetTermSet(Group group, string termSetName)
 {
  TermSet termSet = null;

  try
  {
   termSet = group.TermSets[termSetName];
  }
  catch { }

  return termSet;
 }

 #endregion
}

3 comments:

  1. Before ContentUpdate(), you might wanna use UpdateIncludingSealedAndReadOnly.
    Else the derived contenttypes don't inherit the column.

    ReplyDelete
  2. Hey Mike, well done ... Many thanks!

    ReplyDelete
  3. Great Post! But how do I make a field associate with a particular Term in a Termset?

    ReplyDelete