Cross-Site Collection Navigation

Background

SharePoint uses the provider model for supplying data to its navigation controls. The v4.master, for instance, uses an AspMenu control for the top navigation whose DataSourceId is set declaratively to an instance of a SiteMapDataSource. The data source control is wrapped in a delegate control so it can be easily swapped out using a feature.

<SharePoint:AspMenu
  ID="TopNavigationMenuV4"
  Runat="server"
  EnableViewState="false"
  DataSourceID="topSiteMap"
  AccessKey="<%$Resources:wss,navigation_accesskey%>"
  UseSimpleRendering="true"
  UseSeparateCss="false"
  Orientation="Horizontal"
  StaticDisplayLevels="2"
  MaximumDynamicDisplayLevels="1"
  SkipLinkText=""
  CssClass="s4-tn" />
<SharePoint:DelegateControl runat="server" ControlId="TopNavigationDataSource" Id="topNavigationDelegate">
    <Template_Controls>
        <asp:SiteMapDataSource
          ShowStartingNode="False"
          SiteMapProvider="SPNavigationProvider"
          id="topSiteMap"
          runat="server"
          StartingNodeUrl="sid:1002"/>
    </Template_Controls>
</SharePoint:DelegateControl>

The data source control has a SiteMapProvider property which can be set to any of the named sitemap providers declared in the web.config file.

<siteMap defaultProvider="CurrentNavigation" enabled="true">
  <providers>
    <add name="SPNavigationProvider" type="Microsoft.SharePoint.Navigation.SPNavigationProvider, Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
    <add name="SPSiteMapProvider" type="Microsoft.SharePoint.Navigation.SPSiteMapProvider, Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
    <add name="SPContentMapProvider" type="Microsoft.SharePoint.Navigation.SPContentMapProvider, Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
    <add name="SPXmlContentMapProvider" siteMapFile="_app_bin/layouts.sitemap" type="Microsoft.SharePoint.Navigation.SPXmlContentMapProvider, Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
    <add name="ExtendedSearchXmlContentMapProvider" description="Provider for navigation in Extended Search pages" siteMapFile="_app_bin/layouts.sitemap" type="Microsoft.Office.Server.Search.Extended.Administration.Common.ExtendedSearchXmlContentMapProvider, Microsoft.Office.Server.Search, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
    <add name="AdministrationQuickLaunchProvider" description="QuickLaunch navigation provider for the central administration site" type="Microsoft.Office.Server.Web.AdministrationQuickLaunchProvider, Microsoft.Office.Server.UI, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
    <add name="SharedServicesQuickLaunchProvider" description="QuickLaunch navigation provider for shared services administration sites" type="Microsoft.Office.Server.Web.SharedServicesQuickLaunchProvider, Microsoft.Office.Server.UI, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
    <add name="GlobalNavSiteMapProvider" description="CMS provider for Global navigation" type="Microsoft.SharePoint.Publishing.Navigation.PortalSiteMapProvider, Microsoft.SharePoint.Publishing, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" NavigationType="Global" EncodeOutput="true" />
    <add name="CombinedNavSiteMapProvider" description="CMS provider for Combined navigation" type="Microsoft.SharePoint.Publishing.Navigation.PortalSiteMapProvider, Microsoft.SharePoint.Publishing, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" NavigationType="Combined" EncodeOutput="true" />
    <add name="CurrentNavSiteMapProvider" description="CMS provider for Current navigation" type="Microsoft.SharePoint.Publishing.Navigation.PortalSiteMapProvider, Microsoft.SharePoint.Publishing, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" NavigationType="Current" EncodeOutput="true" />
    <add name="CurrentNavSiteMapProviderNoEncode" description="CMS provider for Current navigation, no encoding of output" type="Microsoft.SharePoint.Publishing.Navigation.PortalSiteMapProvider, Microsoft.SharePoint.Publishing, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" NavigationType="Current" EncodeOutput="false" />
    <add name="GlobalNavigation" description="Provider for MOSS Global Navigation" type="Microsoft.SharePoint.Publishing.Navigation.PortalSiteMapProvider, Microsoft.SharePoint.Publishing, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" NavigationType="Combined" Version="14" />
    <add name="CurrentNavigation" description="Provider for MOSS Current Navigation" type="Microsoft.SharePoint.Publishing.Navigation.PortalSiteMapProvider, Microsoft.SharePoint.Publishing, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" NavigationType="Current" Version="14" />
    <add name="SiteDirectoryCategoryProvider" description="Site Directory category provider" type="Microsoft.SharePoint.Portal.WebControls.SiteDirectoryCategoryProvider, Microsoft.SharePoint.Portal, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
    <add name="MySiteMapProvider" description="MySite provider that returns areas and based on the current user context" type="Microsoft.SharePoint.Portal.MySiteMapProvider, Microsoft.SharePoint.Portal, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
    <add name="MySiteLeftNavProvider" description="MySite Left Nav provider that returns areas and based on the current user context" type="Microsoft.SharePoint.Portal.MySiteLeftNavProvider, Microsoft.SharePoint.Portal, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
    <add name="MySiteSubNavProvider" description="MySite Sub Nav provider that returns areas and based on the current user context" type="Microsoft.SharePoint.Portal.MySiteSubNavProvider, Microsoft.SharePoint.Portal, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
  </providers>
</siteMap>


Problem

This arrangement works well when you stay within a site collection. However, if you need to show navigation nodes from another site collection you will need another approach. The problem is that the SiteMapProvider is limited to returning only nodes in the current site collection. If you have created, for example, a My Site Host site collection, and want to display the root site collection’s top navigation at the top of the page instead of the default My Site Host navigation, you need a way to read the root site collection’s navigation nodes.

Solution

The solution provided below is for a publishing site and uses the PortalSiteMapProvider. The PortalSiteMapProvider has a method called GlobalNavSiteMapProvider which can be used to get the top navigation nodes as a SiteMapNodeCollection. We can interate over these nodes in a custom control and build up the navigation for our site in whatever way we want.

But, to return a SiteMapNodeCollection from another site collection we need a reference to the PortalSiteMapProvider in the context of the other site collection. One way to do this is to use an HTTP handler that can be called from the Layouts folder (e.g., _layouts/handlers/navigation.ashx) and then add the appropriate site collection’s URL. If we look at the following three site collections, their URL’s all point to the same HTTP handler:

If we call the first URL from either the Search Center or My Site Host site collections, the PortalSiteMapProvider, if it is referenced in the handler, will return navigation nodes from the root site collection. Note that this method will only work within a single web application. If we attempt to reach across applications this would amount to cross-site scripting which is prevented for security reasons.

The custom handler can then be called asynchronously from client code and a serialized set of navigation nodes returned to the browser and formatted using client script.

    Implementation

To implement this solution, add an HTTP handler to your project. The following assumes you have a mapped Layouts folder in a Visual Studio 2010 SharePoint project. Right-click the Layouts folder and Add an ASP.NET Handler from the Web item list.

image

This will create a class that inherits from IHttpHandler which requires you to implement a single method, ProcessRequest, and a single property, IsReusable.

public void ProcessRequest(HttpContext context)
{
    SPSecurity.RunWithElevatedPrivileges(GetGlobalNav);
}

public bool IsReusable
{
    get { return true; }
}

The IsReusable property allows subsequent requests to reuse the same instance of the handler. ProcessRequest acts as our entry point and, to ensure our method that retrieves navigation nodes has sufficient privileges, we call it with RunWithElevatedPrivileges.

The GetGlobalNav method then gets a reference to the PortalSiteMapProvider which then gets a SiteMapNodeCollection based on the root node. This collection is first passed into a method that converts it into a collection that can be easily serialized and then finally we call SerializeWrite to output JSON to the client.

private void GetGlobalNav()
{
    try
    {
        //Note: PortalSiteMapProvider returns data for the current request's site collection
        var rootNode = PortalSiteMapProvider.GlobalNavSiteMapProvider.RootNode;
        if (rootNode != null)
        {
            var nodes = PortalSiteMapProvider.GlobalNavSiteMapProvider.GetChildNodes(rootNode);
            SerializeWrite(GetSerializableSiteMap(nodes));
        }
    }
    catch (Exception e)
    {
        //log exception
    }
}

Since the generic List<t> can be easily serialized if we use a custom struct, all we need to do in GetSerializableSiteMap is build up a List using the specified SiteMapNodeCollection.

private List<SerializableSiteMapNode> GetSerializableSiteMap(SiteMapNodeCollection nodes)
{
    var serializableSiteMap = new List<SerializableSiteMapNode>();

    foreach (SiteMapNode node in nodes)
    {
        var serializableSiteMapNode = new SerializableSiteMapNode()
        {
            Description = node.Description,
            HasChildNodes = node.HasChildNodes,
            Key = node.Key,
            Title = node.Title,
            Url = node.Url
        };

        if (_siteMapLevel < Depth && node.HasChildNodes)
        {
            _siteMapLevel++;
            serializableSiteMapNode.ChildNodes = GetSerializableSiteMap(node.ChildNodes);
        }

        serializableSiteMap.Add(serializableSiteMapNode);
    }

    return serializableSiteMap;
}

public struct SerializableSiteMapNode
{
    public List<SerializableSiteMapNode> ChildNodes;
    public string Description;
    public bool HasChildNodes;
    public string Key;
    public string Title;
    public string Url;
}

The result of this is passed into a method that handles writing the HTTP response. Fortunately, .NET has a JavaScript serializer which makes our job easier.

private void SerializeWrite(object objectToWrite)
{
    var outputString = String.Empty;

    var serializer = new JavaScriptSerializer();
    outputString = serializer.Serialize(objectToWrite);

    HttpContext.Current.Response.Write(outputString);
}

The custom handler can be called in a user control using the appropriate site collection URL and the response data formatted using jQuery. We have added a user control to the top of the master page in our My Site Host site collection that calls our handler using a relative reference to the root site collection ("/"). So while we display the navigation in the My Site Host site collection, the nodes will be pulled from the root (main) site collection using the URL "/_layouts/handlers/navigation.ashx". The user control loads an empty unordered list element whose list items are built up asynchronously based on the response from our handler.

<script language="javascript" type="text/javascript">
    var handlerUrl = '/_layouts/handlers/navigation.ashx?&Depth=2';
    var level = 0;

    $(document).ready(function () {
        ul = $('#navGlobal');
        ul.children().remove();

        $.get(handlerUrl, function (data) {
            if (data != '') {
                tags = eval(data);
                writeNodes(tags, ul);
            }
        });
    });

    function writeNodes(tagNodes, ulNode) {
        $(tagNodes).each(function (i, n) {
            if (n.Url == null || n.Title == null) return true;

            var a = $(document.createElement('a'));
            a.html(n.Title);
            a.attr('href', n.Url);
            if (level == 0) a.attr('class', 'top_link');

            var li = $(document.createElement('li'));
            li.append(a);

            if (n.HasChildNodes) {
                level++;
                var ul = $(document.createElement('ul'));
                writeNodes(n.ChildNodes, ul);
                li.append(ul);
            }

            ulNode.append(li);
        });
    }
</script>

<ul id="navGlobal" />

This is a simple approach to simply reading and displaying navigation nodes from one site collection in another. We have not tried to merge navigation nodes using this method but I don’t see why one could not.

In addition, we have only used our handler to return data to the client. If you need to return it to server-side code, you can simply create a WebRequest object on the server that calls the handler. The data should then be serialized as XML and not JSON so you can use the .NET XML object model to handle the response.

About these ads

About Michael Brockman
SharePoint (2010, MOSS 2007) and .NET developer.

15 Responses to Cross-Site Collection Navigation

  1. maitsalah says:

    Can you publish Source Code please.

  2. Cesar says:

    Hello, I´m new in share point 2010, I´m trying to implement a top global navigation without success.
    I tried your solution but I have not found the definition of those variables _siteMapLevel and Depth.
    Can youn help me?
    Thank a lot.

    • Hi Cesar,

      _siteMapLevel is a class-level counter variable in the HTTP module class. It is initialized to zero:

      private int _siteMapLevel = 0;

      Depth is used to set the numbr of levels to recurse. In our case we only need the top two levels returned from the PortalSiteMapProvider. This is designed to be settable from the client code. So, we add a public property in our handler class and set it using a query string variable passed in from the client.

      public int Depth { get; set; }

      var depth = 1;
      int.TryParse(context.Request.QueryString[“Depth”], out depth);
      Depth = depth;

      The client code can then specify what level of depth to return navigation nodes for by simply appending the Depth query string variable:

      var handlerUrl = ‘/_layouts/ihi/handlers/getdata.ashx?DataSource=GlobalNav&Depth=2′;

  3. Shug says:

    Can you provide the source please?

  4. Shug says:

    Hi again,

    Managed to get this coded up with the help of a colleage, what do I need to put in my web.config and IIS to get this to work? Currently it fails on:
    $(document).ready(function () {
    ul = $(‘#navGlobal’);
    ul.children().remove();

    $.get(handlerUrl, function (data) {
    if (data != ”) {
    tags = eval(data);
    writeNodes(tags, ul);
    }
    });
    });

    So assume it can’t get to the handler. any pointers?

    • Hi Shug,

      You don’t need to modify the web.config or IIS to get this to work. Simply add an ASHX file to your project in Visual Studio.

      Where does the client script fail? FireBug should tell you where the error is.

      We are using jQuery in our client script; you need to add the jQuery library to your project and a script reference () to your page if you haven’t done so already.

  5. Paul says:

    Hi, the script is not working for me in Firefox. Looking at the console tab in firebug gives me a 401 error when it tries to grab the .ashx page. Any ideas?

    • Hi Paul,

      Have you tried it in IE? Depending on what you named your ASHX file and where it’s located (mine’s in Layouts subfolder called “handlers”) you may need to change the handlerUrl in the client script. The 401 error sounds like it’s just not finding the ASHX file.

      • Paul says:

        It does work in IE, and if you go to the ashx page directly in the browser it appears. The file is located in the _layouts folders, but not in a subfolder such as your handlers folder.

      • That’s strange. I have it working in both FireFox and IE. I just ran it in FF and I get a 200 OK status for the call to the handler. I’m using FF 3.6.10 and jQuery v1.3.2.

      • Paul says:

        Thank you for the help, it looks like we have found a solution using an ajax call. Thanks again.

  6. Danny says:

    Hi,

    I tried this solution but unfortunately it didn’t work for me. I checked and I debugged the navigation.ashx in Visual Studio. And I found the error on the ‘rootNode’ variable.
    ..
    private void GetGlobalNav()
    {
    try
    {
    //Note: PortalSiteMapProvider returns data for the current request’s site collection
    var rootNode = PortalSiteMapProvider.GlobalNavSiteMapProvider.RootNode;
    if (rootNode != null)
    {
    var nodes = PortalSiteMapProvider.GlobalNavSiteMapProvider.GetChildNodes(rootNode);
    //GetSerializableSiteMap(nodes);
    SerializeWrite(GetSerializableSiteMap(nodes));
    }
    }
    ..

    The value of this variable is the error code that you can see it below.

    An error occured while rendering navigation for requested URL: /_layouts/handlers/navigation.ashx. Exception message: Object reference not set to an instance of an object. Stack trace: at Microsoft.SharePoint.Publishing.CacheManager.GetManager(SPSite site, Boolean useContextSite, Boolean allowContextSiteOptimization) at Microsoft.SharePoint.Publishing.Navigation.PortalSiteMapProvider.get_ObjectFactory() at Microsoft.SharePoint.Publishing.Navigation.PortalSiteMapProvider.GetRootNodeCore()

    I didn’t use publishing template for the root sitecollection. Therefore I changed sitemapprovider in the masterpage.

    ..
    <SharePoint:AspMenu
    ID="TopNavigationMenuV4"
    Runat="server"
    EnableViewState="false"
    DataSourceID="topSiteMap"
    AccessKey="”
    UseSimpleRendering=”true”
    UseSeparateCss=”false”
    Orientation=”Horizontal”
    StaticDisplayLevels=”2″
    MaximumDynamicDisplayLevels=”1″
    SkipLinkText=””
    CssClass=”s4-tn”/>

    ..

    What do you think this error means?
    How can I fix it?

    Regards,
    Danny

  7. Gene says:

    Does this support security trimming?

  8. K says:

    Great post, thanks a lot for this!
    The only problem I had was with browser caching. My solution was to create a different request variable each time the Javascript called the handler so that a new response was given each time. Not sure if this is the best solution but it works..
    Did you come across this problem?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: