Cross-Site Collection Navigation
July 6, 2010 15 Comments
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.
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.
<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:
- http://rootsitecollection/_layouts/handlers/navigation.ashx
- http://rootsitecollection/mysites/_layouts/handlers/navigation.ashx
- http://rootsitecollection/searchcenter/_layouts/handlers/navigation.ashx
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.
This will create a class that inherits from IHttpHandler which requires you to implement a single method, ProcessRequest, and a single property, IsReusable.
{
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.
{
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.
{
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.
{
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.
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.