SPSiteDataQuery and Managed Navigation Filtering

July 15, 2013

In this post I'll highlight my recent "battle" with SPSiteDataQuery and Managed Metadata used for cross-site navigation, with the hope that someone will find it useful and avoid banging your head against a wall.

The Requirements

The site I was working on has a whole hierarchy of subsites and every subsite has different document libraries. The libraries use a single content type for the documents that are uploaded to them. I also use Content Organizer to automatically classify the uploaded document to the right site and document library. (I wrote about that few weeks ago).

What I wanted to achieve is to use the top navigation in SharePoint to navigate the subsites (the "physical" navigation) and to use the current navigation (left navigation area) to do cross-site queries against a specific tag. The documents were tagged with a Managed Metadata term, with its own hierarchy.

I also enabled Managed Metadata Navigation, using the same term set that I use for tagging the documents. The managed navigation is displayed correctly but what I could not achieve is to use the managed metadata value as a filter in a Content Query web part, so that when the user clicks on a managed navigation term the page it navigates to (the same page for all the terms) will automatically filter and show only the documents that have been tagged with that term (or its descendants). The CQWP does not allow you to use managed navigation values as a filter, as it doesn't recognize them.

I had to code my idea in Visual Studio.

The Solution

What I ended up doing is crafting a web part that parses the managed navigation term, queries all the sites in the site collection for documents tagged with that term and then fills a SPGridView with the results.

The tricky parts were:

Finding the Metadata Navigation Value

As the user clicked on a managed navigation term, SharePoint would create a SEO-friendly URL and internally it would invoke a single page. The managed metadata framework has the ability to "look up" the friendly URL and retrieve the corresponding term that has been used in the navigation.

In C#, the code that does that is as follows:

TaxonomyNavigationContext tnx = TaxonomyNavigationContext.Current;  
if (tnx.HasFriendlyUrl)  
{ Guid termId = tnx.FriendlyUrlTerm.Id;}

The TaxonomyNavigationContext defined in Microsoft.SharePoint.Publishing.dll assembly will give us the Term ID (a Guid) that corresponds to the term that was clicked in a managed navigation menu.

Filtering the SPSiteDataQuery with the managed metadata value

In order to filter a SPSiteDataQuery (or any CAML query) with a managed metadata value, you have to know the local lookup value for the metadata. What is this "local lookup" about?

When you add a metadata column in SharePoint, it goes to the root site for that site collection and creates a hidden list. It then uses that list as the source for a lookup field that will represent the managed metadata. When you tag an item with a term, SharePoint will add that term as a list item in the hidden list and update the tagged item accordingly.

So, if you want to filter a list or a library for tagged values, you have to know their local IDs in the lookup fields. Fortunately, there is a method that does just that: GetWssIdsOfTerm.

As we already have the TaxonomyNavigationContext and the term ID, we will collect two more IDs: Term Store ID (usually only one in whole deployment) and a Term Set ID. These IDs are already available in the FriendlyUrlTerm property of TaxonomyNavigationContext.

Guid termStoreId = tnx.FriendlyUrlTerm.TermSet.TermStoreId;  
Guid termSetId = tnx.FriendlyUrlTerm.TermSet.Id;  
int\[\] wssIds = TaxonomyField.GetWssIdsOfTerm(SPContext.Current.Site, termStoreId, termSetId, termId, true, 100);

In the wssIds array we now have the corresponding values for the term set and its descendants (that's what the boolean parameter in the method does).

The only thing missing is to convert that IDs into a CAML query "<Where>" condition to filter against the column named "Section". For that we use CAML *<In>* clause and a list of values created dynamically from the lookup ID array wssIds:

StringBuilder values = new StringBuilder();for (int i = 0; i < wssIds.Length; i++){  
 values.Append("<Value Type="Integer">");  
 values.Append(wssIds\[i\].ToString()); values.Append("</Value>");  
}  
string where = String.Format("<Where><In><FieldRef Name='Section' LookupId='TRUE'/><Values>{0}</Values></In></Where>", values);

Caveat

One strange issue that happens with this approach is that when you click a term that hasn't been used for tagging, the hidden list does not contain the lookup ID and the GetWssIdsOfTerm method returns an empty string. You have to manage this condition and return an empty result without trying to build a CAML query as you already know that there are no tagged items in the site collection.

This behaviour is very similar to the SPUser object when the user hasn't entered the SharePoint site collection yet, where we use SPWeb.EnsureUser method to fill the user object into the site.


Profile picture

Written by Edin Kapić Insatiably curious code-writing tinkerer. Geek father. Aviation enthusiast. Cuisine journeyman. Follow me on Twitter