Monday, January 30, 2012

How To Customize Current Navigation (Left Navigation) in SharePoint 2010 To Show Multiple Levels?

What?

SharePoint 2010 has a limitation that it only shows up to 2 levels of navigation items at any given time in any page in the current navigation (aka current navigation).

Let us take a typical scenario where I have my site structure as defined below:

Root site
  Team Site
  Subsite Level 1
    Subsite Level 2
      Subsite Level 3

The above requirement is typical for especially public/internet facing sites where it is useful to show the structure of your sites/pages exactly in the manner above.

Why?

The below screenshot shows an example of what is expected in an easily navigable public facing SharePoint 2010 site.


The limited OOB behavior in SharePoint 2010 does not allow us to show navigation the way we want through UI settings.

Here is a series of screen shots that I have taken from an OOB publishing site in SharePoint 2010.

Let us take a scenario where I have my site structure as defined below:

Root site
  Team Site
  Subsite Level 1
    Subsite Level 2
      Subsite Level 2 Page
      Subsite Level 3
        Subsite Level 3

Here is what SharePoint 2010 gave me.

The root site:


Subsite Level 1:

Subsite Level 2:

Subsite Level 3:


As you may have noticed, SharePoint does not show any more than 2 levels at any time.

How?


With a combination of little bit of jQuery, CSS & a Custom Control, this can be accomplished easily.
The control that I am going to demonstrate in this post can handle any number of levels to display.

The Custom Control:


Create a new custom control that inherits System.Web.UI.WebControls.HierarchicalDataBoundControl
Use the code provided to build the control:

using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using Microsoft.SharePoint.WebControls;

namespace SharePoint2010.Navigation.Current
{
    [ParseChildren(false), PersistChildren(true)]
    public class LeftNavController : System.Web.UI.WebControls.HierarchicalDataBoundControl
    {
        #region Private Members
        private int currentNodeLevel = 0;
        private SiteMapNode currentNode;
        #endregion

        #region Overrides
        /// <summary>
        /// Allows for debugging information to be written to the page source.
        /// </summary>
        /// <param name="writer"></param>
        protected override void Render(HtmlTextWriter writer)
        {
            base.Render(writer);

            if (Page.ClientQueryString.Contains("show_nav_debug"))
            {
                writer.Write(string.Format("<!--Current Level:{0} Current Key:{1}-->", currentNodeLevel, currentNode.Key));
            }
        }

        /// <summary>
        /// Dynamically set properties of the AspMenu control based on the current location.
        /// </summary>
        /// <param name="e"></param>
        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);

            SiteMapDataSource dataSource;

            try
            {
                // Find AspMenu controls in the child collection
                foreach (Control c in this.Controls)
                {
                    // Check child for proper type
                    AspMenu menu = c as AspMenu;

                    // Continue when child is correct type
                    if (menu != null)
                    {
                        // Extract AspMenu's data source
                        this.DataSourceID = menu.DataSourceID;

                        // Check data source for proper type
                        dataSource = this.GetDataSource() as SiteMapDataSource;

                        // Continue when data source is proper type
                        if (dataSource != null)
                        {
                            // Get current site node
                            currentNode = dataSource.Provider.CurrentNode;

                            // Determine level of current site node
                            currentNodeLevel = DetermineLevel(currentNode, ref currentNodeLevel);

                            // Set properties of the AspMenu and it's data source
                            dataSource.StartFromCurrentNode = true;
                            dataSource.StartingNodeUrl = "";
                            dataSource.ShowStartingNode = false;
                            menu.MaximumDynamicDisplayLevels = 0;

                            if (currentNodeLevel <= 2)
                            {
                                // Show only children in navigation
                                dataSource.StartingNodeOffset = 0;
                                menu.StaticDisplayLevels = 2;
                            }

                            if (currentNodeLevel == 3)
                            {
                                // Show children and siblings in navigation
                                dataSource.StartingNodeOffset = -2;
                                menu.StaticDisplayLevels = 3;
                            }

                            if (currentNodeLevel == 4)
                            {
                                // Show children and siblings in navigation
                                dataSource.StartingNodeOffset = -3;
                                //menu.StaticDisplayLevels = 4;
                                menu.StaticDisplayLevels = 10;
                            }

                            if (currentNodeLevel == 5)
                            {
                                // Show children and siblings in navigation
                                dataSource.StartingNodeOffset = -4;
                                menu.StaticDisplayLevels = 10;
                            }

                            if (currentNodeLevel == 6)
                            {
                                // Show children and siblings in navigation
                                dataSource.StartingNodeOffset = -5;
                                menu.StaticDisplayLevels = 10;
                            }

                            if (currentNodeLevel == 7)
                            {
                                // Show children and siblings in navigation
                                dataSource.StartingNodeOffset = -6;
                                menu.StaticDisplayLevels = 10;
                            }

                            if (currentNodeLevel >= 8)
                            {
                                // Show children and siblings in navigation
                                dataSource.StartingNodeOffset = -6;
                                menu.StaticDisplayLevels = 10;
                            }

                            //Treat pages as same level as site
                            if (currentNode.Key.ToUpper().Contains("/PAGES/"))
                            {
                                //if (currentNodeLevel >= 5)
                                if (currentNodeLevel >= 3)
                                    dataSource.StartingNodeOffset = dataSource.StartingNodeOffset++;
                                else
                                    dataSource.StartingNodeOffset--;
                                //menu.StaticDisplayLevels++;
                            }
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                throw;
            }
        }
        #endregion

        #region Private Methods
        /// <summary>
        /// Calculate the depth of a site map node by walking back up the tree.
        /// </summary>
        /// <param name="item">The site map node to evaluate</param>
        /// <param name="level">Level to pass between recursive calls</param>
        /// <returns>An integer representing the depth of the node</returns>
        private int DetermineLevel(SiteMapNode item, ref int level)
        {
            if (item != null && item.ParentNode != null)
            {
                level++;
                DetermineLevel(item.ParentNode, ref level);
            }
            return level;
        }
        #endregion
    }
}

When the control is compiled, deployed and is ready to be accessed by SharePoint, we need to make sure that it is registered in the master page.

<%@ Register TagPrefix="CurrentNavController" Namespace="SharePoint2010.Navigation.Current" Assembly="SharePoint2010.Navigation.Current,Version=1.0.0.0, Culture=neutral, PublicKeyToken=xxxxxxxxxxxxxxxx" %>

Locate the ContentPlaceHolder with id "PlaceHolderLeftNavBar" and replace its content as shown below:

<asp:ContentPlaceHolder id="PlaceHolderLeftNavBar" runat="server">
 <CurrentNavController:LeftNavController runat="server" ID="lNavController">
        <!-- Secondary Navigation Data Source -->
        <PublishingNavigation:PortalSiteMapDataSource
   ID="SiteMapDS"
   runat="server"
   EnableViewState="false"
   SiteMapProvider="CurrentNavigation"
   StartFromCurrentNode="true"
   StartingNodeOffset="0"
   ShowStartingNode="false"
   TrimNonCurrentTypes="Heading"/>
        <!-- end Secondary Navigation Data Source -->
        <!-- Secondary Navigation Menu -->
        <nav>
   <SharePoint:AspMenu
    ID="CurrentNav"
    EncodeTitle="false"
    runat="server"
    EnableViewState="false"
    DataSourceID="SiteMapDS"
    UseSeparateCSS="false"
    UseSimpleRendering="true"
    Orientation="Vertical"
    StaticDisplayLevels="3"
    MaximumDynamicDisplayLevels="2"
    CssClass="s4-ql" 
    SkipLinkText="<%$Resources:cms,masterpages_skiplinktext%>"/>
  </nav>
        <!-- end Secondary Navigation Menu -->
    </CurrentNavController:LeftNavController>
 </asp:ContentPlaceHolder>

The CSS:

/* Hide all ul.static elements */
.s4-ql ul.static > li.static > ul.static > li.static > ul.static {
 display:none;
}
.s4-ql ul.static > li.static > ul.static > li.static > ul.static > li.static > ul.static {
 display:none;
}
.s4-ql ul.static > li.static > ul.static > li.static > ul.static > li.static > ul.static > li.static > ul.static {
 display:none;
}
.s4-ql ul.static > li.static > ul.static > li.static > ul.static > li.static > ul.static > li.static > ul.static > li.static > ul.static {
 display:none;
}
.s4-ql ul.static > li.static > ul.static > li.static > ul.static > li.static > ul.static > li.static > ul.static > li.static > ul.static > li.static > ul.static {
 display:none;
}
.s4-ql ul.static > li.static > ul.static > li.static > ul.static > li.static > ul.static > li.static > ul.static > li.static > ul.static > li.static > ul.static > li.static > ul.static {
 display:none;
}

The jQuery/JavaScript Code:

$(document).ready(function(){
  HandleCurrentNavigation();
});

//Hide unwanted current navigation items
function HandleCurrentNavigation() {
 $(".s4-ql li.selected").parent().show();
 $(".s4-ql li.selected > ul.static").show();
 
 $(".s4-ql li.selected").parent().parent().show();
 $(".s4-ql li.selected").parent().parent().parent().show();
 $(".s4-ql li.selected").parent().parent().parent().parent().show();   
 $(".s4-ql li.selected").parent().parent().parent().parent().parent().show();   
 $(".s4-ql li.selected").parent().parent().parent().parent().parent().parent().show();   
 $(".s4-ql li.selected").parent().parent().parent().parent().parent().parent().parent().show();      
}

Make sure that you reference the CSS & jQuery/JavaScript code from your master page properly.

If every thing is done properly, you can expect to see multiple levels of navigation in your current navigation control as shown below:


Thanks to Scott Tindall for all the hard work he put for building the custom control.

12 comments:

  1. Thanks a lot! that's exactly what I'm looking for!

    ReplyDelete
  2. nice work, thanks!

    ReplyDelete
  3. Hi,
    I'm not understand your approach...maybe i miss something...For instance you can get more than 2 levels displayed on Left Navigation by simply modifying these properties StaticDisplayLevels, MaximumDynamicDisplayLevels of Sharepoint:AspMenu used by quicklaunch

    ReplyDelete
  4. Thank you for the great post, but could i kindly ask you where should i place the css,jquery file in the same directory of the custom dll registred? many thanks

    ReplyDelete
  5. No. You can store the css, jquery files anywhere in the site. For example in the Style Library of the root site. Then add references to the js and css files in your master page.

    ReplyDelete
  6. Thank you for your quick response, where should i write the C# code?

    My though is to create a user control (MainPage.ascx) then paste your C# code, then create a class as following calling the user control.
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Web.UI;
    using System.Web.UI.WebControls;

    namespace WBOLibraryDLL
    {
    public class WBOLibraryDLL : System.Web.UI.WebControls.WebParts.WebPart
    {
    Control control;
    protected override void CreateChildControls()
    {
    this.control = this.Page.LoadControl("~/WBOLibrary/MainPage.ascx");
    this.Controls.Add(this.control);
    }
    }
    }


    then implement the dll into web.config then site actions - webpart - galleries then i found the new webpart installed.

    Please let me know if the above steps going right to proceed?

    Many thanks

    ReplyDelete
  7. The custom navigation must inherit from System.Web.UI.WebControls.HierarchicalDataBoundControl and after it is deployed, you must replace the OOB navigation control in SharePoint's master page with this new navigation control as outlined in my post.

    Hope that this helps

    ReplyDelete
  8. Yes, but i should have to create usercontrol inherit from System.Web.UI.WebControls.HierarchicalDataBoundControl and paste your code inside?

    ReplyDelete
  9. No.. You need to create a custom web control (composite control) not a user control

    ReplyDelete
  10. Hi Chaitu,

    Where would you go to add the headings and names for the different levels? Would it be in the normal place Site Settings -> Navigation or do we have to do it with code?

    Thanks!!

    ReplyDelete
    Replies
    1. Just go to site settings > navigation and set up headings, sub-headings and links there

      Delete
  11. Why write a custom control when you can

    "I'm not understand your approach...maybe i miss something...For instance you can get more than 2 levels displayed on Left Navigation by simply modifying these properties StaticDisplayLevels, MaximumDynamicDisplayLevels of Sharepoint:AspMenu used by quicklaunch"

    ReplyDelete