Building breadcrumbs the way you want it in SharePoint 2010


A while ago I wrote an article about creating breadcrumbs using the Mavention Simple SiteMapPath control. And while I thought that the control would be sufficient to cover the most common scenarios just recently it turned out that the control has one major flaw. Find out how to build breadcrumbs in SharePoint really the way you want it.

Create SharePoint breadcrumbs with Mavention Simple SiteMapPath

One of the challenges while building Internet-facing websites is rendering breadcrumbs the way they are defined in the user experience. Out-of-the-box you can use either the ListSiteMapPath control provided with SharePoint 2010 or the SiteMapPath control which is a part of the .NET framework.

The ListSiteMapPath control uses nested unordered lists to describe the current position within the site. While it’s a good idea from the semantic point of view the rendered markup is far from ideal and unfortunately fixed, which makes using the control with custom user experiences challenging.

A good alternative to the SharePoint’s ListSiteMapPath control is the standard ASP.NET SiteMapPath control. Although standard Master Pages provided with SharePoint 2010 contain some examples of how you can use the SiteMapPath control to render breadcrumbs the real benefit can be achieved by using the templates to render breadcrumb nodes. And while, in terms of rendering, the SiteMapPath is more flexible than the ListSiteMapPath, if you take a closer look at its output, you will find some additional spans rendered which, depending on your HTML, might be problematic.

Given the limitations of the ListSiteMapPath and SiteMapPath controls a while ago I decided to create the Mavention Simple SiteMapPath control. The main idea behind the control was to have it render as clean as possible HTML – something you would expect from a webdesigner coding the user experience. Although Mavention Simple SiteMapPath didn’t offer as rich configuration capabilities as the SiteMapPath control, it did exactly what it was supposed to do and it did it good.

The flaw of Mavention Simple SiteMapPath

Taking the most common approach to rendering breadcrumbs on the web, the Mavention Simple SiteMapPath control used an unordered list as the markup base. And this is exactly why it’s flawed: although the control is simple, it does one and one thing only and it does it certain way which cannot be configured without rebuilding the code.

Building breadcrumbs the way you want it

To avoid the limitations of the Mavention Simple SiteMapPath control and to be able to support any kind of breadcrumbs possible, I have created the Mavention Templated SiteMapPath.

Mavention Templated SiteMapPath

The Mavention Templated SiteMapPath consists of the following three files:

using System;
using System;
using System.Collections.Generic;
using System.Web;
using System.Web.UI;

namespace Mavention.SharePoint.Controls {
    [ParseChildren(true), PersistChildren(false)]
    public class TemplatedSiteMapPath : Control  {
        public bool SkipRootNode { get; set; }
        public string SiteMapProvider { get; set; }

        public ITemplate HeaderTemplate { get; set; }
        public ITemplate FooterTemplate { get; set; }
        public ITemplate RootNodeTemplate { get; set; }
        public ITemplate NodeTemplate { get; set; }
        public ITemplate CurrentNodeTemplate { get; set; }
        public ITemplate SeparatorTemplate { get; set; }

        private SiteMapProvider provider;

        protected override void OnInit(EventArgs e) {
            SetSiteMapProvider();
        }

        private void SetSiteMapProvider() {
            if (String.IsNullOrEmpty(SiteMapProvider)) {
                provider = SiteMap.Provider;
            }
            else {
                provider = SiteMap.Providers[SiteMapProvider];
            }

            if (provider == null) {
                throw new ArgumentOutOfRangeException(String.Format("SiteMap Provider '{0}' not found", SiteMapProvider ?? "default"));
            }
        }

        protected override void CreateChildControls() {
            List<SiteMapPathNodeItem> breadcrumbs = new List<SiteMapPathNodeItem>();
            breadcrumbs.Add(new SiteMapPathNodeItem(Int32.MaxValue, SiteMapPathNodeItemType.Footer));

            SiteMapNode currentNode = provider.CurrentNode;
            SiteMapNode node = provider.CurrentNode;
            SiteMapNode rootNode = currentNode.RootNode;
            int i = 0;
            while (node != null) {
                if (node.Key != rootNode.Key) {
                    if (breadcrumbs.Count > 1) {
                        breadcrumbs.Add(new SiteMapPathNodeItem(i--, SiteMapPathNodeItemType.Separator));
                    }

                    breadcrumbs.Add(new SiteMapPathNodeItem(i--, node.Key == currentNode.Key ? SiteMapPathNodeItemType.CurrentNode : SiteMapPathNodeItemType.Node) { SiteMapNode = node });
                }
                else {
                    if (!SkipRootNode) {
                        if (breadcrumbs.Count > 1) {
                            breadcrumbs.Add(new SiteMapPathNodeItem(i--, SiteMapPathNodeItemType.Separator));
                        }

                        breadcrumbs.Add(new SiteMapPathNodeItem(i, SiteMapPathNodeItemType.RootNode) { SiteMapNode = node });
                    }
                }

                node = node.ParentNode;
            }
            
            breadcrumbs.Add(new SiteMapPathNodeItem(Int32.MinValue, SiteMapPathNodeItemType.Header));
            breadcrumbs.Reverse();

            if (breadcrumbs.Count > 2) {
                breadcrumbs[1].ItemType = SiteMapPathNodeItemType.RootNode;
            }

            foreach (SiteMapPathNodeItem breadcrumb in breadcrumbs) {
                InitializeItem(breadcrumb);
            }
        }

        public virtual void InitializeItem(SiteMapPathNodeItem item) {
            ITemplate nodeTemplate = null;
            SiteMapPathNodeItemType itemType = item.ItemType;

            switch (itemType) {
                case SiteMapPathNodeItemType.Header:
                    nodeTemplate = HeaderTemplate;
                    break;
                case SiteMapPathNodeItemType.Footer:
                    nodeTemplate = FooterTemplate;
                    break;
                case SiteMapPathNodeItemType.RootNode:
                    nodeTemplate = RootNodeTemplate ?? NodeTemplate;
                    break;
                case SiteMapPathNodeItemType.Node:
                    nodeTemplate = NodeTemplate;
                    break;
                case SiteMapPathNodeItemType.CurrentNode:
                    nodeTemplate = CurrentNodeTemplate ?? NodeTemplate;
                    break;
                case SiteMapPathNodeItemType.Separator:
                    nodeTemplate = SeparatorTemplate;
                    break;
                default:
                    break;
            }

            if (nodeTemplate != null) {
                nodeTemplate.InstantiateIn(item);
                Controls.Add(item);
                item.DataBind();
            }
        }
    }
}
namespace Mavention.SharePoint.Controls {
    public enum SiteMapPathNodeItemType {
        Header,
        Footer,
        RootNode,
        Node,
        CurrentNode,
        Separator
    }
}
using System.Web;
using System.Web.UI;

namespace Mavention.SharePoint.Controls {
    public class SiteMapPathNodeItem : Control, IDataItemContainer, INamingContainer {
        public SiteMapPathNodeItemType ItemType { get; set; }
        public SiteMapNode SiteMapNode { get; set; }
        public int ItemIndex { get; set; }

        public SiteMapPathNodeItem(int itemIndex, SiteMapPathNodeItemType itemType) {
            ItemIndex = itemIndex;
            ItemType = itemType;
        }

        public object DataItem {
            get { return SiteMapNode; }
        }

        public int DataItemIndex {
            get { return ItemIndex; }
        }

        public int DisplayIndex {
            get { return ItemIndex; }
        }
    }
}

The first file – TemplatedSiteMapPath.cs, is the Mavention Templated SiteMapPath control itself. If you compare its code to the Mavention Simple SiteMapPath you can see that the code is much simpler and the biggest part of it supports rendering using Templates.

The second file – SiteMapPathNodeItemType.cs contains the definition of the enumeration that describes the type of the breadcrumb node. The node type is being used to apply the right rendering template while rendering breadcrumbs.

Finally there is the SiteMapPathNodeItem.cs file which contains the definition of a control in which a breadcrumb node is wrapped. The important part here is that this class inherits from the Control class, and not WebControl like the standard ASP.NET SiteMapNode class, so no additional spans or other markup for that matter is rendered.

Using the Mavention Templated SiteMapPath

The following code snippet illustrates how you can use the Mavention Templated SiteMapPath control:

<Mavention:TemplatedSiteMapPath runat="server">
    <HeaderTemplate><div id="breadcrumbs"></HeaderTemplate>
    <RootNodeTemplate><asp:Hyperlink Text='home' NavigateUrl='<%# Eval("Url") %>' runat="server"/></RootNodeTemplate>
    <NodeTemplate><asp:Hyperlink Text='<%# Eval("Title") %>' NavigateUrl='<%# Eval("Url") %>' runat="server"/></NodeTemplate>
    <CurrentNodeTemplate><asp:Literal Text='<%# Eval("Title") %>' runat="server"/></CurrentNodeTemplate>
    <SeparatorTemplate> / </SeparatorTemplate>
    <FooterTemplate></div></FooterTemplate>
</Mavention:TemplatedSiteMapPath>

Using this snippet will render breadcrumbs as hyperlinks, separated with a / (slash) where the current node is rendered as plain text. As you can imagine with some slight modifications you could change the breadcrumbs to be rendered as an unordered list should you need it, without touching the code!

Dealing with Variations

One of the common challenges when building breadcrumbs for a public-facing websites is dealing with Variations. Whenever a site is using the Variations capability to deliver multilingual content the desired way of rendering breadcrumbs is to have the first node point to the home page of the current Variation. Unfortunately the standard Navigation Providers available with SharePoint 2010 render the root site as the first node, so instead of:

home > subsite > page

you get:

root > home > subsite > page

This happens no matter how your navigation settings are configured and even if you choose to break navigation inheritance for the Variation root sites.

The Mavention Templated SiteMapPath control allows you to easily work around this issue. By setting the SkipRootNode property to true, the root node is omitted and the second node in hierarchy is promoted as the root node so that it can be properly rendered as a root node (TemplatedSiteMapPath.cs lines 55-61 and 70-72). As a result breadcrumbs will be rendered as you would expect them to.

It’s all about permissions

Using the code samples above you can implement the Mavention Templated SiteMapPath in your solution. One important thing that you should keep in mind is, that the code should be included in an assembly deployed to GAC. Without it, in some scenarios depending on how your website is built, you might experience exceptions being thrown while instantiating the ExtendedSearchXmlContentMapProvider navigation provider which is one of the standard navigation providers delivered with SharePoint 2010.

Summary

Implementing custom user experiences in public-facing websites built on the SharePoint 2010 platform knows many challenges. One of those challenges is building breadcrumbs. Using the Mavention Templated SiteMapPath control you can build virtually any kind of breadcrumbs without the need to modify the control’s code.

Others found also helpful: