blog header-cdh2.png

Insights Blog

Providing thought leadership around hot topics in technology

Real Cross Site Collection Navigation in SharePoint Using the Term Store

Posted by C/D/H Consultant | Apr 23, 2014 10:01:00 AM

cross1.jpg

Navigation within SharePoint can often cause confusion and frustration among users and admins.  Whether it’s users not understanding where they are, or Admins not knowing how to structure things in a sensible way, Navigation can be challenging. Today we are going to take a look at a fairly common (and frustrating) scenario – cross site collection navigation.   By default SharePoint 2010 and 2013 do not allow for cross site collection navigation, meaning the same navigation set or structure isn’t available to an entire web application.  One site collection cannot easily link to items in another without using custom, hard coded links.

There were hopes that SharePoint 2013 would address this issue, but it has not – not really, anyway.  It’s true that managed navigation is improved in SharePoint 2013 – meaning you can use the term store to build a navigation set that can be displayed on your site collections.  However, a term set can only be attached to one site collection at a time, so in essence this is still limited to one Site Collection.  Yes, there are ways of stapling a duplicate term set to the master term set, but this copying process is very error prone and does not update child sets properly in all scenarios.  Some people live with this implementation but I feel it’s a huge mess.  I wanted to figure out something more manageable.

It turns out that accessing the term store through the SharePoint object model eliminates a lot of the weird limitations imposed through the UI.  For example, we can access a term set from the root site collection of a web application and surface the data inside of a user control that can be injected across ALL site collections using a custom Master Page.  Let’s explore how this all works together using a custom solution in Visual Studio.

Create a new Empty SharePoint 2010 or 2013 project in Visual Studio called GlobalNav and choose to deploy it as a farm solution.  This scenario will not work in SharePoint Online or environments that require Sandbox solutions, since these do not allow User Controls.  I am also connecting to a Publishing Site since that will allow us to easily assign our custom Master Page, though you could also use a Team Site with a  few extra steps.  The code shown will be running in 2013 but should work in 2010 as well.

After your solution is built, right click the project and add a User Control.  You can call it “GlobalNav” if you want all of the code samples included here to work, or you can go rogue with your own naming and just borrow the concepts.  Visual Studio will automatically create the ControlTemplates directory for SharePoint and add your User Control.

cross2.jpg

Open GlobalNav.ascx and add the following line of code below all of the text Visual Studio generates for you:

<asp:Label ID="GlobalNavContainer" runat="server" ViewStateMode="Disabled" CssClass="GlobalNavContainer"></asp:Label>

This is all of the content that will be added to GlobalNav.ascx – it’s nothing more than a standard Label Control.  Our code behind file will build and inject the nav HTML into this container.

Next open up GlobalNav.ascx.cs – this is where all of our logic to access the term store will be added.  Copy and paste code below inside your class.  You may need to add a project reference to Microsoft.SharePoint.Taxonomy.   Let’s take a moment to walk through this code.

using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Taxonomy;
 
namespace GlobalNav.ControlTemplates.GlobalNav
{
    public partial class GlobalNav : UserControl
    {
        public static string html { getset; }
 
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!Page.IsPostBack)
            {
                SPSecurity.RunWithElevatedPrivileges(delegate()
                {
                    using (SPSite thisSite = new SPSite(SPContext.Current.Site.WebApplication.AlternateUrls[0].Uri.AbsoluteUri))
                    {
                        html = "";
                        TaxonomySession session = new TaxonomySession(thisSite);
                        TermStoreCollection store = session.TermStores;
 
                        try
                        {
                            foreach (TermStore termStore in session.TermStores)
                            {
                                Group navGroup = termStore.Groups["Navigation"];
                                foreach (TermSet topSet in navGroup.TermSets)
                                {
                                    html += writeTerms(topSet.Terms);
                                }
                            }
                        }
                        catch
                        {
                        }
 
                        finally
                        {
                            GlobalNavContainer.Text = "";
                            GlobalNavContainer.Text = html;
                        }
                    }
                });
            }
        }
 
        public string writeTerms(TermCollection terms)
        {
            if (terms.Count > 0)
            {
                html += "\n<ul class=\"GlobalNav\">\n";
                foreach (Term subTerm in terms)
                {
                    try
                    {
                        html += "<li><a href=\"" + subTerm.LocalCustomProperties["_Sys_Nav_SimpleLinkUrl"] + "\">" + subTerm.Name + "</a>";
                        writeTerms(subTerm.Terms);
                        html += "</li>\n";
                    }
 
                    catch
                    {
                        html += "<li><a href=\"#\">" + subTerm.Name + "</a>";
                        writeTerms(subTerm.Terms);
                        html += "</li>\n";
                    }
                }
 
                html += "</ul>\n";
            }
            return html;
        }
    }
}

First we get a reference to the root site collection inside of our using statement – the url of our web application is the same as the root site collection.  Next we make sure the variable that will store our html is set to empty.  We want to gain access to our Term Stores – particularly the top level group which we will call Navigation.  I wasn’t sure of the best way to make this not hard coded, so for demo purposes just make sure you have a group called Navigation or whatever you want as your top level item.  Basically all we have to do now is loop through every term set in the navigation group and call our writeTerms method.

WriteTerms is a special recursive function that generates the HTML that will be injected into our user control.  The recursive aspect is important because it means our navigation will support unlimited levels of navigation by continually traversing down the hierarchy of each term set until it cannot find another child.  Obviously you will need appropriate CSS/Javascript to support multiple levels of navigation, but the HTML output is very simple and clean.  Generally I try to avoid string building HTML, but here it’s only a few elements.  There are a couple other details here – the try catch is so that if they leave out the link field it won’t cause an error when SharePoint tries to access it.  I am not a fan of using Try Catch blocks in production code but I’m not sure of a way to avoid it here.  You could also abstract the Try Catch into a wrapper method for cleaner looking code.

Next you need to add this user control to your Master Page.  You can just use a starter master page from codeplex for this example, or your own.  This tutorial assumes you have a general idea of how to deploy a custom Master Page, but here is a short refresher:  Right click your project and choose Add New Item.  Select an item of Module to add to your project – Modules are used to deploy files to SharePoint.  Paste your starter Master Page File into this module and make sure your Elements.XML in the module looks something like this:

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Module Name="MasterPage" Url="_catalogs/masterpage">
    <File Path="MasterPage\Starter.master" Url="Starter.master" Type="GhostableInLibrary" />
  </Module>
</Elements>

The Module URL determines where the files will deployed – in this case we want them to go to the Master Page Gallery.  Remember, File Path is the path to the file within your solution, URL is the path to the file after it’s deployed, relative to the module URL.

Open up your new Master Page and add a new Register tag at the top of the Master Page beneath the others – it should look something like this:

<%@Register TagPrefix="GlobalNav"  TagName="GlobalNav" Src="~/_controltemplates/GlobalNav/GlobalNav.ascx" %>

We can now add the actual User Control onto the Master Page. You can technically put this almost anywhere. I had to play around with it a bit to get it exactly where I wanted in the HTML, but I ended up adding it just above where the search Control starts (if you’re using the start master pages).

<GlobalNav:GlobalNav id="GlobalNav" EnableViewState="false" runat="server" />

Lastly we need to add some CSS.  For demo purposes you can just add a <style> element to GlobalNav.ascx and paste the CSS below inside of it.  If you are deploying this production at some point you should abstract this into its own CSS file and include it on the master page.  Inline CSS is bad.  This styling might not render perfectly for you depending on how your test environment it set up but hopefully it’s a good starting point.  You can also exclude this CSS entirely to see the raw html output rendered in the browser.

.GlobalNav
{
    margin0;
    padding0;
    list-stylenone;
}
 
.GlobalNav li
{
    floatleft;
    padding10px;
}
 
.GlobalNav ul
{
    displaynone;
    background#FCFCFC;
    border1px solid #eee;
    box-shadow1px 1px 7px #ccc;
    z-index10000;
}
 
.GlobalNav ul li
{
    positionrelative;
    displayblock;
    width100%;
}
 
.GlobalNav ul li:hover
{
    background#eee;
}
 
.GlobalNav > ul
{
    displayblock;
}
 
.GlobalNav > ul li
{
    displayblock;
    floatleft;
}
 
.GlobalNav > li:hover > ul
{
    displayblock;
    positionabsolute;
    width200px;
}
 
.GlobalNav > li ul li:hover > ul
{
    displayblock;
    positionabsolute;
    width200px;
    right-200px;
    top0;
}

Next we need to configure our Term Store.  First head over to Central Admin and make sure you have a Managed Meta Data Service Running with the right permissions.  I gave all authenticated users Read Permission on the service so that everyone could at least see the nav.

cross3.jpg

After you have this configured, click on the Meta Data Service text to actually navigate to the term store.  Here you can assign administrators who will be allowed to edit the actual terms.   The most important thing here is to actually build out your navigation – make sure your top level group matches the name you used in your code, which is “Navigation” in my case.  I then added  a top level term set that will contain all of our nav items, called “Master Nav”.

cross4.jpg

At this point if you build and run your solution you should have a semi-working global nav.  You may have to go into your site settings and assign the new master page as your custom master – you could also write code to apply this automatically on feature activation, but we won’t cover that here.  My results ended up something like the image below.  It’s not beautiful yet, but it’s very functional and includes unlimited levels of drop downs via hover.   And, of course, it works across site collections! Start building out your styling and your term store to see how far you can take this concept.

cross5.jpg

Topics: SharePoint

Written by C/D/H Consultant