Microsoft MVP Logo

This post is part of a series of posts... jump to the overview post that explains what it is all about and to get links on all the other posts here: SharePoint 2010 Metadata-Based Navigation in Publishing Sites - Series Overview.

I'm guessing that if you are reading this post you likely read the one before it where I talked about the virtues of implementing a MetaNav. In this post I'm going to talk about what you're going to have to build in order to implement a MetaNav in your SharePoint 2010 (SP2010) Publishing site (aka: WCM site). At the core there are three major things you have to address:

  • Navigation
  • Rollup/Landing Pages & Controls
  • Detail Pages

Building the Navigation Components

It wouldn't be a MetaNav if you didn't have navigation now, would it? Of course not. SP2010 implements navigation by leveraging the ASP.NET Navigation Provider framework. In this there are three main components:

  • Navigation Provider - This piece is responsible for talking to some entity and creating the hierarchical collection of navigation nodes. Some out of the box providers include those that can look at an XML file or a SharePoint site. In our case we need to create one that looks at the SP2010 Managed Metadata Service (MMS) to extract a taxonomy.
  • Data Source - The data source control is responsible for obtaining the navigation hierarchical structure from the navigation provider & figuring out what nodes should be included in the rendering... it essentially is filtering the nav. From here you do things like tell it to start at the top-most node or the current node, if the top-most node should be shown and things of that nature.
  • Navigation Web Control - This control takes what the data source has filtered down and renders it out to HTML. From here you do things like control the CSS, indention and things of that nature.

As far as a MetaNav is concerned, there's only one thing you need to build: a new navigation provider. As I said above, the job of this provider is to look at the taxonomy and build a collection of navigation nodes representing it. This is actually fairly easy to do as you're just building an ASP.NET navigation provider.

Create a new class that inherits the PortalSiteMapProvider from the Publishing namespace and override just one method: GetChildNodes(). Then fill this method with the following code. This gets called for every node you create and it has the sole responsibility of returning the immediate child nodes under it. As you can see from the following class, I check to see if we're at the top node and if we are, we start at the top of the taxonomy. Otherwise it's a GUID, the ID of the term in the nav I created, I find the term in the term set & get all it's children.

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Web;
   5: using Microsoft.SharePoint;
   6: using Microsoft.SharePoint.Publishing.Navigation;
   7: using Microsoft.SharePoint.Taxonomy;
   8: using Microsoft.SharePoint.Publishing;
   9:  
  10: namespace CriticalPathTraining.SharePoint.Samples {
  11:   public class MetadataBasedNavProvider : PortalSiteMapProvider {
  12:  
  13:     public override SiteMapNodeCollection GetChildNodes(SiteMapNode node) {
  14:       SiteMapNodeCollection navNodes = new SiteMapNodeCollection();
  15:  
  16:       // cast it to a portal node from a regular .NET node
  17:       PortalSiteMapNode portalNode = node as PortalSiteMapNode;
  18:  
  19:       // make sure it's a portal node
  20:       if (portalNode == null)
  21:         return navNodes;
  22:  
  23:       // get refrerence to the taxonomy term store
  24:       TaxonomySession taxonomySession = new TaxonomySession(SPContext.Current.Site);
  25:       TermStore termStore = taxonomySession.TermStores[0];
  26:       Group termGroup = termStore.Groups["Transportation"];
  27:       TermSet termSet = termGroup.TermSets["Automobile"];
  28:  
  29:       // site root
  30:       if (node.Key.ToLower() == SPContext.Current.Web.ServerRelativeUrl.ToLower()) {
  31:         foreach (var term in termSet.Terms) {
  32:           navNodes.Add(ProcessTerm(portalNode, term));
  33:         }
  34:       } else {
  35:         var subTerm = termSet.GetTerm(new Guid(node.Key));
  36:         foreach (var term in subTerm.Terms) {
  37:           navNodes.Add(ProcessTerm(portalNode, term));
  38:         }
  39:       }
  40:  
  41:       return navNodes;
  42:     }
  43:  
  44:     private SiteMapNode ProcessTerm(PortalSiteMapNode portalNode, Term termNode) {
  45:       var navNode = new PortalSiteMapNode(portalNode.WebNode, 
  46:                       termNode.Id.ToString(), 
  47:                       NodeTypes.Heading,
  48:                       String.Format("{0}/Pages/rollup.aspx?tid={1}", SPContext.Current.Web.Url, termNode.Id), 
  49:                       termNode.Name, 
  50:                       string.Empty);
  51:       return navNode;
  52:     }
  53:   }
  54: }

After building the project and deploying the DLL to the GAC, you next need to register the provider. Add the following to the <sitemap> element collection in the site's web.config. This makes the site aware of the provider.

   1: <add name="MetadataNavProvider"
          type="CriticalPathTraining.SharePoint.Samples.MetadataBasedNavProvider,
                    CriticalPathTraining.SharePoint.Samples.MetadataBasedNavProvider,
                    Version=1.0.0.0, Culture=neutral,
                    PublicKeyToken=31ad9365ecb382fd"
          NavigationType="Global"
          EncodeOutput="true" />

Lastly, you change the data source in the master page or where ever you are using it and poof, you got it working! From the pictures below, you can see the taxonomy on the left and the nav on the right:

Building the Rollup/Landing Pages & Controls

Sometimes your navigation has the actual detail page links in them, but all the time your navigation points to rollup or landing pages. The goal of these rollup/landing pages is to present links to the other pages in your site. These pages (which could contain reusable controls) work best when you leverage search to find the pages that match the criteria on the landing page.

Below I've used a Web Part to show how to build the rollup. It is checking the taxid QueryString parameter to find out what term was passed along. Here's what it looks like:

   1: using System;
   2: using System.ComponentModel;
   3: using System.Data;
   4: using System.Web.UI;
   5: using System.Web.UI.WebControls;
   6: using System.Web.UI.WebControls.WebParts;
   7: using Microsoft.Office.Server.Search.Query;
   8: using Microsoft.Office.Server.Search.Administration;
   9: using Microsoft.SharePoint;
  10: using Microsoft.SharePoint.Taxonomy;
  11:  
  12: namespace CriticalPathTraining.SharePoint.Samples.SearchTermRollupWebPart.SearchTermRollupWebPart {
  13:   [ToolboxItemAttribute(false)]
  14:   public class SearchTermRollupWebPart : WebPart {
  15:     protected override void CreateChildControls() {
  16:       // get term id from querystring
  17:       string taxid = this.Page.Request.QueryString["tid"].ToString();
  18:  
  19:       // get the term name from MMS
  20:       // get refrerence to the taxonomy term store
  21:       TaxonomySession taxonomySession = new TaxonomySession(SPContext.Current.Site);
  22:       // get reference to first term store (can also get by name)
  23:       TermStore termStore = taxonomySession.TermStores[0];
  24:       Group termGroup = termStore.Groups["Transportation"];
  25:       TermSet termSet = termGroup.TermSets["Automobile"];
  26:  
  27:       var matchingTerm = termSet.GetTerm(new Guid(taxid));
  28:       if (matchingTerm != null)
  29:         this.Controls.Add(new LiteralControl("<h2>" + matchingTerm.Name + "</h2>"));
  30:  
  31:       // execute query to get all pages matching the term
  32:       SearchServiceApplicationProxy proxy = (SearchServiceApplicationProxy)SearchServiceApplicationProxy
          .GetProxy(SPServiceContext.GetContext(SPContext.Current.Site));
  33:       KeywordQuery keywordQuery = new KeywordQuery(proxy);
  34:       keywordQuery.ResultsProvider = SearchProvider.Default;
  35:       keywordQuery.ResultTypes = ResultType.RelevantResults;
  36:       keywordQuery.EnableStemming = false;
  37:       keywordQuery.TrimDuplicates = true;
  38:  
  39:       // create query
  40:       string pivotField = this.Page.Request.QueryString["pt"] != null ? 
  41:         this.Page.Request.QueryString["pt"].ToString().ToLower() : 
  42:         matchingTerm.Name.ToLower();
  43:  
  44:       string query = "";
  45:       switch (pivotField) {
  46:         case "car type":
  47:           query = string.Format("{0}:{1}", ManagedProperties.CarType, taxid);
  48:           break;
  49:         case "drivetrain":
  50:           query = string.Format("{0}:{1}", ManagedProperties.Drivetrain, taxid);
  51:           break;
  52:         case "engine type":
  53:           query = string.Format("{0}:{1}", ManagedProperties.EngineType, taxid);
  54:           break;
  55:         case "make":
  56:           query = string.Format("{0}:{1}", ManagedProperties.Manufacturer, taxid);
  57:           break;
  58:       }
  59:       keywordQuery.QueryText = query;
  60:  
  61:       ResultTableCollection searchResults = keywordQuery.Execute();
  62:       ResultTable relevantResults = searchResults[ResultType.RelevantResults];
  63:  
  64:       DataTable relevantResultsTable = new DataTable();
  65:       relevantResultsTable.Load(relevantResults, LoadOption.OverwriteChanges);
  66:       
  67:       // write results
  68:       foreach (DataRow searchResult in relevantResultsTable.Rows) {
  69:         this.Controls.Add(new LiteralControl("<div>&raquo; "));
  70:         this.Controls.Add(new HyperLink() { NavigateUrl = searchResult[5].ToString(), Text = searchResult[2].ToString() });
  71:         this.Controls.Add(new LiteralControl("</div>"));
  72:       }
  73:     }
  74:   }
  75: }

Building Detail Pages

Generally there isn't much of a problem in having to set this up. The pages are created like normal and the links to them will be visible in the search results. Thus when you create the rollup pages & controls, you'll just link to these detail pages. However if you want to show content not just from your site collection, but also from other site collections If you want to show pages that don't live in the site collection, such as those that live in another site collections or outside SharePoint. I doubt you'll want to redirect your users from your site to wherever this content lives... you'll likely want to surface the content in your site to maintain your user experience. One way to address this is to create your own version on the profile page capability that Business Connectivity Services (BCS) offers.

Download the Code!

You can grab the source code to a sample MetaNav from the Critical Path Training site, specifically the Members section after you login (which is free). Look in the Code Samples section for AC's SharePoint 2010 Sample MetaNav for Publishing Sites.

Comments powered by Disqus