Wednesday, August 31, 2011

SharePoint 2010 Code Tips – Setting a Managed Metadata Field with the Client Object Model

Technorati Tags: ,,

I have seen many requests on how to set a managed metadata field using the client object model. Most of the questions revolve around the fact that many remote applications do not have access to the required information for setting a managed metadata field. A managed metadata field contains a term and the term is stored with three pieces of information. The first piece is the ID that represents the ID of the term in the local site collection’s TaxonomyHiddentList. The second piece is the term value itself which is usually the label the user tagged it with. The final piece is the term’s ID which is in the form of a GUID. This represents the unique ID of the term in the term store of the site collection. This term store is published from the associated metadata service application. Here is an example of how the value is stored for a term named escalade.

2;#escalade|5224eacc-371c-4022-b485-ff84b9d198f8

Remote applications have the term they want to add but do not have the ID to the TaxonomyHiddenList  list and the unique ID of the term. The ID to the term in the TaxonomyHiddenLIst can be ignored by just using –1 in its place. However, you must have the ID of the term. One solution was to query the TaxonomyHiddenList list for it. Unfortunately, the term is only put there after it has been used in the site collection. This is done for performance reasons.  In order to set the managed metadata field from CSOM you must use the TaxonomyClientService web service to obtain the ID of the term. In this post I will show you how to use both the managed CSOM and the TaxonomyClientService web service to set the managed metadata field, also, I will show you how to do this with a multi-valued managed metadata field.

Getting all the information

Setting a managed metadata field requires multiple steps in order to gather the required information.

  1. Get the managed metadata field of the list.
  2. Get the shared service ID (GUID of the metadata service application) and the term set ID from the field’s schema.
  3. Get the set of terms for the term set from the TaxonomyClientService web service.
  4. Parse the returned xml and obtain the term ID for the term you are going to use.

Four simple steps. I wish they were. The biggest problem is understanding how to parse the returned xml from the GetTermSets method of the TaxonomyClientService. The xml schema has no discernable reason or order to it. The schema has arbitrary attribute names. Below is an example:

 

The term set I am using in this example is called Cadillac and contains a group of terms for different models of Cadillac cars. Each term is contained in a “T “ node and the “a9” attribute represents the ID of the term. The term label is contained in the child “TL” node and the label is denoted by the “a32” attribute. The code below calls the TaxonomyClientService and  searches for term using the term label sent in as an argument.



public static string GetTermInfo(string siteUrl, Field field, string term, ref string textField)
{

string sspId = string.Empty;
string termSetId = string.Empty;

XElement schemaRoot = XElement.Parse(field.SchemaXml);

foreach (XElement property in schemaRoot.Descendants("Property"))
{
XElement name = property.Element("Name");
XElement value = property.Element("Value");

if (name != null && value != null)
{
switch (name.Value)
{
case "SspId":
sspId = value.Value;
break;
case "TermSetId":
termSetId = value.Value;
break;

case "TextField":
textField = value.Value;
break;

}
}
}

string termSetXml = GetTerms(siteUrl, sspId, termSetId);
XElement termSetElement = XElement.Parse(termSetXml);

var termId = from t in termSetElement.Descendants("T")
where t.Descendants("TL").Attributes("a32").First().Value.ToUpper() == term.ToUpper()
select t.Attribute("a9");


if (termId != null && termId.Count() > 0)
return termId.First().Value.ToString();
else
return string.Empty;


}

private static string GetTerms(string siteUrl, string sspId, string termSetId)
{

termsservice.Taxonomywebservice ts = new termsservice.Taxonomywebservice();
ts.UseDefaultCredentials = true;
ts.Url = siteUrl +"/_vti_bin/taxonomyclientservice.asmx";

string timeStamp;



string termSetXml = ts.GetTermSets(WrapXml(sspId.ToString()),
WrapXml(termSetId.ToString()),
CultureInfo.CurrentUICulture.LCID,
WrapXml(DateTime.Now.ToUniversalTime().Ticks.ToString()),
WrapXml("0"), out timeStamp);



return termSetXml;

}

private static string WrapXml(string value)
{
return string.Format("<is><i>{0}</i></is>", value);
}



 



Setting all the information



The code below puts it all together by setting the required information. In the case of using the client object model you must set two fields for the update to be successful. Every managed metadata field has a corresponding hidden taxonomy text field. The GetTermInfo method returns the ID of this hidden field so the code can also update its value. This field holds the same information as the managed metadata field but is used for internal purposes. Notice also the code checks to see if the current value supports multiple values by checking if the type can be assigned to an object array. If it can then you must add terms to a  list an convert the values back  to an array. You must also append all the values with a semi-colon for the hidden taxonomy field. Finally you will only need to set the ID value to a –1 and the server side code will look it up for you. So there are no extra calls needed to look up the value from the TaxonomyHiddentList.








public static void SetManagedMetaDataField_ClientOM(string siteUrl,string listName, string itemID, string fieldName, string term)
{

ClientContext clientContext = new ClientContext(siteUrl);
List list = clientContext.Web.Lists.GetByTitle(listName);
FieldCollection fields = list.Fields;
Field field = fields.GetByInternalNameOrTitle(fieldName);

CamlQuery camlQueryForItem = new CamlQuery();
string queryXml = @"<View>
<Query>
<Where>
<Eq>
<FieldRef Name='ID'/>
<Value Type='Counter'>!@itemid</Value>
</Eq>
</Where>
</Query>
</View>"
;

camlQueryForItem.ViewXml = queryXml.Replace("!@itemid", itemID);

ListItemCollection listItems = list.GetItems(camlQueryForItem);


clientContext.Load(listItems);
clientContext.Load(fields);
clientContext.Load(field);
clientContext.ExecuteQuery();

string hiddentTextFieldID = string.Empty;
string termId = GetTermInfo(siteUrl, field, term, ref hiddentTextFieldID);


if (!string.IsNullOrEmpty(termId))
{
ListItem item = listItems[0];
string termValue = string.Empty;
string termHTVal = string.Empty;
object itemValue = null;

List<object> objectVals = null;


if (item[fieldName] != null && item[fieldName].GetType().IsAssignableFrom(typeof(object[])))
{

termValue = term + "|" + termId;
objectVals = ((object[])item[fieldName]).ToList<object>();
objectVals.Add(termValue);
itemValue = objectVals.ToArray<object>();

foreach (object val in objectVals)
{ termHTVal += "-1;#" + val + ";"; }
termHTVal = termHTVal.Substring(0, termHTVal.Length - 1);

}
else
{
termValue = "-1" + ";#" + term + "|" + termId;
termHTVal = termValue;
itemValue = termValue;

}



Field hiddenTextField = fields.GetById(new Guid(hiddentTextFieldID));
clientContext.Load(hiddenTextField);
clientContext.ExecuteQuery();

item[hiddenTextField.InternalName] = termHTVal;
item[fieldName] = itemValue;

item.Update();
clientContext.Load(item);
clientContext.ExecuteQuery();

}

}



Advanced Scenarios



In summary this example will take the term label, site URL, managed metadata field name, list name and the ID of the item you want to update. It will use this information to update the managed metadata field with the term. You can use this example to build more sophisticated scenarios where you may want to replace a particular term with another if the column holds multiple values. The taxonomy web service has methods to lookup terms based on a culture for multi-lingual cases, and the ability to search for terms where there are more than one term store. Your best bet is to build a term picker using the information from the taxonomy web service. In this case users would pick from a dialog what term they want to tag a value with. Using a term picker the term ID would already be available to use and would allow the user to easily navigate complicated term stores.



I hope this helps in understanding all the extra steps that must be taken when setting managed metadata from remote applications. Hopefully, this will become much easier in future versions of SharePoint.

9 comments:

anand said...

Thank you for this great post!

>> you will only need to set the ID value to a –1 and the server side code will look it up for you. So there are no extra calls needed to look up the value from the TaxonomyHiddentList.

If the term has never been used before (and therefore not on the TaxonomyHiddenList), the update does nothing (no error, but no value either). Any idea how to handle those cases ?
Also, I have come across document libraries where the managed metadata field exists but the corresponding Notes field (+TaxHTField0) does not.

Jas Kauldhar said...

Great post. One question though, were do you get the sspID and termSetID values in your GetTerms() method? Both of these values are GUIDs.

thanks

Anonymous said...

Jas, the GetTerms is called by GetTermId method getting both those values from the Field object's SchemaXml property.

Anonymous said...

Hi Steve, This is great post! Thank you! However I get error for the 'ManagedCSOMManagedMetadata', it says it doesn't exists in the current context. What dlls should be adding? many thanks.

Marco Saltimbanco said...

Thank you for this usefull article.

I see that you use "termsservice" and "SharePointClientApiTest" class. I found "termsservice" class , where can i find "SharePointClientApiTest" class?

Thx!

Steve Curran said...

Marco, I don't see SharePointClientApiTest referenced anywhere.

Jurgen Wiersema said...

Instead of taxonomywebservice class, do you mean this: http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.taxonomy.webservices.taxonomyclientservice(v=office.14).aspx ?
Or how do I add the reference for the class in this line: termsservice.Taxonomywebservice ts = new termsservice.Taxonomywebservice(); ?

Dmitry Doronin said...
This comment has been removed by the author.
Anonymous said...

How would we reference termsservice.Taxonomywebservice ts = new termsservice.Taxonomywebservice(); in this code.?
-PCM

Post a Comment