Wednesday 16 February 2011

Customising SharePoint Breadcrumbs

The ASP.NET SiteMapPath control displays a navigation path (which is also known as a breadcrumb) that shows the user the current page location and displays links as a path back to the home page.

SharePoint Server 2007 uses this control to display the navigation path and whilst this appears to be acceptable, under the bonnet it renders non-semantic markup that doesn't fully represent a visitor's current position in the site and is also more difficult to brand. SharePoint 2010 ships with a new control called the ListSiteMapPath that uses semantic markup to display the navigation path and although this is a landmark development, if you need much greater control over the rendered content or you want to manipulate the behaviour of your navigation path (or you're simply using SharePoint 2007) then read on.

To begin with we're going to use the good old SiteMapPath control which works in both SharePoint 2007 and SharePoint 2010 but as previously described renders non-semantic markup:
<span><a href="...>Home</a></span><span> &gt; </span><span><a href="...>Company</a></span><span> &gt; </span><span><a href="...>Press</a></span><span> &gt; </span><span><a href="...>A Lovely Day</a></span>

To a screen reader or a search engine this has no underlying context and will just appear as a chain of words so we need fix this up by transforming it into a nest of unordered lists:
<div class="AspNet-siteMap">
<ul>
     <li>
          <a href="/"  class="AspNet-SiteMap-Link">Home</a>
          <ul>
               <li>
                    <a href="… class="AspNet-SiteMap-Link">Company</a>
                    <ul>
                         <li>
                              <a href="… class="AspNet-SiteMap-Link">Press</a>
                              <ul>
                                   <li>
                                        <a href="… class="AspNet-SiteMap-Link">A Lovely Day</a>
                                   </li>
                              </ul>
                         </li>
                    </ul>
               </li>
          </ul>
     </li>
</ul>
</div>

This can then be styled using CSS for our finished breadcrumb:
.AspNet-siteMap
{
    font-family: Arial, Sans-Serif;
    font-size: 80%;
    font-weight: normal;
    color: #ffffff;
    margin: 0 0 20px 0;
}
.AspNet-siteMap ul {display: inline; margin: 0; padding: 0;}
.AspNet-siteMap ul li {display: inline; margin: 0;}
.AspNet-siteMap ul a
{
    color: #9ACD34;
    margin-right: 3px;
    text-decoration: none;
    display: inline-block; /* IE needs this for proper background image position if lines break */
    font-weight: bold;
}
.AspNet-siteMap ul ul a
{
    color: #FFF; 
    background: url(../images/BcBullet.gif) no-repeat 0 6px; 
    padding: 0 0 0 12px; /* list indent */
    font-weight: normal;
}
.AspNet-siteMap ul a:hover {text-decoration: underline;}


To get this up and running create a custom control that derives from the ASP.NET SiteMapPath control (signing the assembly and deploying it to the GAC):
Imports System
Imports System.Collections.Generic
Imports System.ComponentModel
Imports System.Text
Imports System.Web
Imports System.Web.UI
Imports System.Web.UI.WebControls
Imports System.Text.RegularExpressions

Namespace Custom.Web.UI.WebControls

    <DefaultProperty("Text"), ToolboxData("<{0}:CustomSiteMapPath runat=server></{0}:CustomSiteMapPath>")> _
    Public Class CustomSiteMapPath

        Inherits SiteMapPath

        <Bindable(True), Category("Appearance"), DefaultValue(""), Localizable(True)> Property Text() As String
            Get
                Dim s As String = CStr(ViewState("Text"))
                If s Is Nothing Then
                    Return "[" + Me.ID + "]"
                Else
                    Return s
                End If
            End Get

            Set(ByVal Value As String)
                ViewState("Text") = Value
            End Set
        End Property

        Protected Overrides Sub OnInit(ByVal e As System.EventArgs)
            If IsPublishingPage() Then
                MyBase.SiteMapProvider = "CurrentNavSiteMapProviderNoEncode"
            End If

            MyBase.OnInit(e)
        End Sub

        Public Overrides Sub RenderBeginTag(ByVal writer As System.Web.UI.HtmlTextWriter)
            writer.WriteLine()
            writer.WriteBeginTag("div")
            writer.WriteAttribute("class", "AspNet-siteMap")
            writer.Write(HtmlTextWriter.TagRightChar)
        End Sub

        Public Overrides Sub RenderEndTag(ByVal writer As System.Web.UI.HtmlTextWriter)
            writer.WriteEndTag("div")
            writer.WriteLine()
        End Sub

        Protected Overrides Sub RenderContents(ByVal writer As HtmlTextWriter)
            writer.Indent += 1
            Dim item As SiteMapPath = CType(Me, SiteMapPath)
            Dim Provider As SiteMapProvider = item.Provider
            Dim collection As New SiteMapNodeCollection()
            Dim node As SiteMapNode = Provider.CurrentNode
            While Not node Is Nothing
                collection.Add(node)
                node = node.ParentNode
            End While

            BuildItems(collection, True, writer)
            writer.Indent -= 1
            writer.WriteLine()
        End Sub

        Private Sub BuildItems(ByVal items As SiteMapNodeCollection, ByVal isRoot As Boolean, ByVal writer As HtmlTextWriter)

            If items.Count > 0 Then

                For i As Integer = items.Count - 1 To -1 + 1 Step -1
                    BuildItem(items(i), writer, (i = 0))    '0 is current node
                Next

                'close nested list
                For i As Integer = 0 To items.Count - 1
                    writer.Indent -= 1
                    writer.WriteLine()
                    writer.WriteEndTag("li")
                    writer.Indent -= 1
                    writer.WriteLine()
                    writer.WriteEndTag("ul")
                Next

            End If
        End Sub

        Private Sub BuildItem(ByVal item As SiteMapNode, ByVal writer As HtmlTextWriter, ByVal isCurrentNode As Boolean)

            If (item IsNot Nothing) AndAlso (writer IsNot Nothing) Then

                If item.Url.Length > 0 Then
                    writer.WriteLine()

                    writer.WriteFullBeginTag("ul")
                    writer.Indent += 1
                    writer.WriteLine()

                    writer.WriteFullBeginTag("li")
                    writer.Indent += 1
                    writer.WriteLine()

                    writer.WriteBeginTag("a")
                    writer.WriteAttribute("href", Page.ResolveUrl(item.Url))
                    writer.WriteAttribute("class", "AspNet-SiteMap-Link")
                    writer.Write(HtmlTextWriter.TagRightChar)
                    writer.Write(item.Title)
                    writer.WriteEndTag("a")

                End If

            End If

        End Sub

        Private Function IsPublishingPage() As Boolean

            Dim url As String = HttpContext.Current.Request.RawUrl
            Dim pattern As String = ".*\/pages\/.+\.aspx"
            Dim urlMatch As Match = Regex.Match(url, pattern, RegexOptions.IgnoreCase)

            Return urlMatch.Success

        End Function

    End Class

End Namespace

Mark the assembly as safe in the web.config file of the web application:
<SafeControl Assembly="CustomSiteMapPath, Version=1.0.0.0, Culture=neutral, PublicKeyToken=3f64c3f72c5f0c95" Namespace="Custom.Web.UI.WebControls" TypeName="*" Safe="True" />

Register the control in the master page or page layout:
<%@ Register TagPrefix="uc" Namespace="Custom.Web.UI.WebControls" Assembly="CustomSiteMapPath, Version=1.0.0.0, Culture=neutral, PublicKeyToken=3f64c3f72c5f0c95" %>

And finally add an instance of the control into the master page or page layout:
<uc:CustomSiteMapPath ID="CustomSiteMapPath1" runat="server" SiteMapProvider="SPContentMapProvider" SkipLinkText="" AdapterEnabled="False"></uc:CustomSiteMapPath>

Note that if you are using the ASP.NET CSS Friendly Control Adapters in your SharePoint site then you must set the AdapterEnabled property on your control instance to False otherwise the CSS Friendly Control Adapter will continue to take precedence.

No comments:

Post a Comment