Monday, October 3, 2011

How To: SharePoint 2010 List Definition with Managed Metadata fields

I recently had a request to create some custom list definitions for a site definition, and within these list definitions were some fields that had to link up to the Taxonomy of the company.

This is not as simple as just adding the fields to the Schema, you also need to create an event receiver that will attach to the FeatureActivated event handler of your feature in order to connect the fields up to the taxonomy. This sounds like quite a complicated solution but even though it is a lot of work, it is in fact really simple. Here's how...


1. Create the list definition project

In Visual Studio, choose to create a new project and select the List Definition project type under the SharePoint 2010 templates:




Choose Deploy as Farm Solution and click Finish

2. Update the name of your definition

After the project has been created you will see that Visual Studio has not inserted the correct names for your list definition. You can update the following to ensure your list has the correct names. (Use your list name wherever I used "MyListDefinition")


Change the actual list definition name from "ListDefinition1" to your list name in the solution explorer



Open the Elements.xml file and change the DisplayName and Description properties. Note: Don't change the value of the Name property as the comment suggest because this may cause an issue with folder names not being correct.

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="
http://schemas.microsoft.com/sharepoint/">
    <!-- Do not change the value of the Name attribute below. If it does not match the folder name of the List Definition project item, an error will occur when the project is run. -->
    <ListTemplate
        Name="MyListDefinition"
        Type="10000"
        BaseType="0"
        OnQuickLaunch="TRUE"
        SecurityBits="11"
        Sequence="320"
        DisplayName="My List Definition"
        Description="This creates a MyListDefinition list"
        Image="/_layouts/images/itann.png"/>
</Elements>


Open the Schema.xml file and change the Title, Url fields

<?xml version="1.0" encoding="utf-8"?>
<List xmlns:ows="Microsoft SharePoint"
      Title="My List Definition"
      FolderCreation="FALSE"
      Direction="$Resources:Direction;"
      Url="Lists/My List Definition"
      BaseType="0"
      xmlns="
http://schemas.microsoft.com/sharepoint/">

3. Create a Content Type

Your managed metadata fields have to be inside a Content Type because it uses pre-defined system fields.

  1. Right click your project and click on Add > New Item...
  2. Select the Content Type template under the SharePoint 2010 templates, give it a name and click ok.
  3. Select "Item" as your base content type and click Finish. I usually pick "Item" as the base because it only contains the Title field which we will make a hidden field.
 
 
Open the Elements.xml file of your content type and update the Name, Group and Description properties.

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="
http://schemas.microsoft.com/sharepoint/">
  <!-- Parent ContentType: Item (0x01) -->
  <ContentType ID="0x01002056f65066514e2e86ac6442eedd8ca8"
               Name="MyContentType"
               Group="MyContentTypes"
               Description="My Content Type"
               Inherits="TRUE"
               Version="0">
    <FieldRefs>
    </FieldRefs>
  </ContentType>
</Elements>


Inside the <FieldRefs> section of your content type, add the fields you are going to use in the list. These are just references to the fields we are still going to create, but it does not matter which ones you create first.

<FieldRefs>
  <FieldRef ID="{67051A64-C702-4FD6-A7BC-9EECFCCCC033}" Name="MyTextField"/>
  <FieldRef ID="{21E0A786-8F94-4A7D-98C3-A8B06B8C592A}" Name="MyUserField"/>
  <FieldRef ID="{B72AF39A-1F54-4041-93C2-F045CA8EDF04}" Name="MyTaxonomyField"/>
  <FieldRef ID="{6AAEB05A-2B38-482E-BC3A-FE2705E99709}" Name="MyTaxonomyFieldTaxHTField0"/>
 
  <!-- DO NOT CHANGE THESE NAMES OR IDS-->
  <FieldRef ID="{f3b0adf9-c1a2-4b02-920d-943fba4b3611}" Name="TaxCatchAll"/>
  <FieldRef ID="{8f6b6dd8-9357-4019-8172-966fcd502ed2}" Name="TaxCatchAllLabel"/>
</FieldRefs>


As you can see, for each ManagedMetadata field you need to add another <FieldRef> which will be used as a "Note" field for itself. This is used to store the actual data of the field as the Taxonomy one only makes a reference to the taxonomy.

You also need to add the bottom two fields called "TaxCatchAll" and "TaxCatchAllLabel". These are put in so that SharePoint search can find items of this content type. Note that since these two are system fields, you must NOT change the GUID's. You can use the standard Visual Studio GUID creator to generate the ID's for the rest of the fields found in Tools > Create GUID > Registry Format.

4. Add the Content Type and Fields to the List Schema

Delete everything in the <ContentTypes> section and add a reference to yours by copying the ID that was generated in the Elements.xml file of your content type.

<ContentTypes>
  <ContentTypeRef ID="0x01002056f65066514e2e86ac6442eedd8ca8" />
</ContentTypes>


Now, delete everything in the <Fields> section and add the references to the fields in the Elements.xml of your content type. We will start by adding the Title field and making it hidden. Then we will add field definitions for the rest:

<Fields>
  <!-- DO NOT CHANGE THE ID OF THE TITLE FIELD -->
  <Field ID="{fa564e0f-0c70-4ab9-b863-0177e6ddd247}"
   Type="Text"
   Name="Title"
   ShowInNewForm="FALSE"
   ShowInEditForm="FALSE"
   ShowInDisplayForm="FALSE"
   ShowInFileDlg="FALSE"
   DisplayName="$Resources:core,Title;"
   Sealed="TRUE"
   SourceID="
http://schemas.microsoft.com/sharepoint/v3"
   StaticName="Title">
  </Field>
 
  <Field ID="{67051A64-C702-4FD6-A7BC-9EECFCCCC033}" Type="Text" Name="MyTextField" DisplayName="My Text Field" />
  <Field ID="{21E0A786-8F94-4A7D-98C3-A8B06B8C592A}" Type="User" Name="MyUserField" DisplayName="My User Field" ShowField="NameWithPicture" />

  <!-- USE THE ID OF THE "NOTE" FIELD IN YOUR ELEMENTS.XML -->
  <Field ID="{6AAEB05A-2B38-482E-BC3A-FE2705E99709}"
  Type="Note"
  DisplayName="MyTaxonomyField_0"
  StaticName="MyTaxonomyFieldTaxHTField0"
  Name="MyTaxonomyFieldTaxHTField0"
  ShowInViewForms="FALSE"
  Required="FALSE"
  Hidden="TRUE"
  CanToggleHidden="TRUE"
  RowOrdinal="0" />
 
 <!-- USE THE ID OF THE "TAXONOMY" FIELD IN YOUR ELEMENTS.XML -->
  <Field ID="{B72AF39A-1F54-4041-93C2-F045CA8EDF04}"
   Type="TaxonomyFieldType"
   DisplayName="My Taxonomy Field"
   ShowField="Term1033"
   Required="TRUE"
   DisplaceOnUpgrade="TRUE"
   EnforceUniqueValues="FALSE"
   Group="_Custom"
   StaticName="MyTaxonomyField"
   Name="MyTaxonomyField">
 <Customization>
   <ArrayOfProperty>
  <Property>
    <Name>TextField</Name>
    <Value xmlns:q6="
http://www.w3.org/2001/XMLSchema"
     p4:type="q6:string"
     xmlns:p4="
http://www.w3.org/2001/XMLSchema-instance">{6AAEB05A-2B38-482E-BC3A-FE2705E99709}</Value>
  </Property>
   </ArrayOfProperty>
 </Customization>
  </Field>
</Fields>


You will see that the ID of the "Note" field is also added to the Customization metadata of the TaxonomyField. This tells it what note field to use.

5. Add the the field to the View of the List

Add your fields to the <ViewFields> section of every <View> like this:

<ViewFields>
  <FieldRef Name="MyTextField"></FieldRef>
  <FieldRef Name="MyUserField"></FieldRef>
  <FieldRef Name="MyTaxonomyField"></FieldRef>
</ViewFields>


6. Add the Event Receiver

Right click on your project and click Add > New Item...

Select the Event Receiver template from the SharePoint 2010 templates and click on Add. Select "List Events" as the type of receiver you want and choose the "A list was added" handler.
Add a reference to the Microsoft.SharePoint.Taxonomy.dll and add a using statement at the top of your class file.
Add the following method to your class that will connect the taxonomy field:

public void ConnectTaxonomyField(Guid fieldId, string listName, string termGroup, string termSetName, SPListEventProperties properties, string termName)
{
   if (properties.Web.Lists[listName].Fields.Contains(fieldId))
   {
    TaxonomySession session = new TaxonomySession(properties.Web.Site);

    if (session.DefaultKeywordsTermStore != null)
    {
     // get the default metadata service application               
     var termStore = session.DefaultKeywordsTermStore;
     var group = termStore.Groups.GetByName(termGroup);
     var termSet = group.TermSets.GetByName(termSetName);
     var term = termSet.GetByName(termName);

     TaxonomyField field = properties.Web.Lists[listName].Fields[fieldId] as TaxonomyField;
     // connect the field to the specified term               
     field.SspId = termSet.TermStore.Id;
     field.TermSetId = termSet.Id;
     field.TargetTemplate = string.Empty;
     field.AnchorId = term.Id;
     field.Update();
    }
    else
    {
     throw new TermStoreNotFoundException(string.Format("DefaultKeywordsTermStore not found in site {0}", properties.Web.Site.Url));
    }
   }
   else
   {
    throw new ArgumentException(string.Format("Field {0} not found in list {1}", fieldId, listName), "fieldId");
   }
}


Make a call to the ConnectTaxonomyField() method in your ListAdded event handler.

// Parameters are: Field ID, List Name, Term Group, Term Set, SPListEventProperties, Term you want to connect to
ConnectTaxonomyField(new Guid("{B72AF39A-1F54-4041-93C2-F045CA8EDF04}"), properties.ListTitle, "TermGroup", "TermSet", properties, "Term");


The last parameter is the Term you want to connect to in the Term Set. Not specifying this for a field will show you all the terms in that field.

Add the following classes to your file that will handle exceptions and create extension methods:

[Serializable]
public class TermStoreNotFoundException : Exception
{
 public TermStoreNotFoundException() { }
 public TermStoreNotFoundException(string message) : base(message) { }
 public TermStoreNotFoundException(string message, Exception inner)
  : base(message, inner) { }
 protected TermStoreNotFoundException(
  System.Runtime.Serialization.SerializationInfo info,
  System.Runtime.Serialization.StreamingContext context)
  : base(info, context) { }
}

public static class Extensions
{
 public static Group GetByName(this GroupCollection groupCollection, string name)
 {
  if (string.IsNullOrEmpty(name))
  {
   throw new ArgumentException("Not a valid group name", "name");
  }

  foreach (var group in groupCollection)
  {
   if (group.Name == name)
   {
    return group;
   }
  }
  throw new ArgumentOutOfRangeException("name", name, "Could not find the group");
 }

 public static TermSet GetByName(this TermSetCollection termSetCollection, string name)
 {
  if (string.IsNullOrEmpty(name))
  {
   throw new ArgumentException("Term set name cannot be empty", "name");
  }

  foreach (var termSet in termSetCollection)
  {
   if (termSet.Name == name)
   {
    return termSet;
   }
  }
  throw new ArgumentOutOfRangeException("name", name, "Could not find the term set");
 }

 public static Term GetByName(this TermSet termSet, string name)
 {
  if (string.IsNullOrEmpty(name))
  {
   throw new ArgumentException("Term name cannot be empty", "name");
  }

  for (int i = 0; i < termSet.Terms.Count; i++)
  {
   if (termSet.Terms[i].Name == name)
   {
    return termSet.Terms[i];
   }
  }
  throw new ArgumentOutOfRangeException("name", name, "Could not find the term set");
 }
}


7. Deploy the solution

You can simply deploy the solution by right clicking the project and clicking on Deploy.

That's it! You can now create an instance of the list to see your fields. It seems like a lot of work for just two fields, but once you get the basics right it goes quite fast.

1 comment: