Monday, June 20, 2011

Configurable multi-level treeview left navigation in SharePoint

It’s a very common requirement in SharePoint to implement a multilevel Left or Top navigation. And another requirement is that it should be configurable i.e. user able to add or remove the navigation links.

I have used treeview control and SharePoint list to implement Left navigation of the site.


Step 1:
    Create a SharePoint list (LeftNavigation) with following columns
    1. Title – Single Line of Text (Name to be displayed in navigation) 
    2. URL – Single Line of Text (Relative URL)
    3. Hierarchy – Lookup column (Parent node)

Step 2:
Add new Class Library project or WSPBuilder project. Then add a User control inside the CNTROLTEMPLATES folder (as shown below).


Give proper name to the user control, here my user controls name is ‘SPNavUserControl’

Step 3: Copy this code in SPNavUserControl.ascx file
<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="SPNavUserControl.ascx.cs"
    Inherits="SPNavControl.SPNavUserControl,SPNavControl,Version=1.0.0.0,Culture=neutral,PublicKeyToken=d3f00d350c249841" %>
<div id="navPanel">
    <asp:XmlDataSource ID="xmlDS" runat="server" />
    <asp:TreeView BackColor="White" ID="treeViewLeftNav" runat="server" DataSourceID="xmlDS"
        ShowLines="true" ShowExpandCollapse="true" SelectedNodeStyle-Font-Bold="true"
        HoverNodeStyle-Font-Bold="true" SelectedNodeStyle-ForeColor="Red" NodeStyle-Width="100%"
        NodeStyle-Height="100%" NodeStyle-Font-Size="11px" NodeStyle-Font-Names="verdana"
        NodeIndent="15" RootNodeStyle-BackColor="#d6e8ff" RootNodeStyle-Width="100%"
        RootNodeStyle-BorderColor="#add1ff" RootNodeStyle-BorderStyle="Solid" RootNodeStyle-BorderWidth="1px"
        LeafNodeStyle-BorderStyle="None">
        <DataBindings>
            <asp:TreeNodeBinding DataMember="menu" TextField="name" NavigateUrlField="url" />
        </DataBindings>
    </asp:TreeView>
</div>

Step 4: Write following code in code-behind file (.cs) to generate XML string (data source) from the SharePoint List and bind the Data source to the treeview control.

namespace SPNavControl
{
    public partial class SPNavUserControl : System.Web.UI.UserControl
    {
        #region Variable Declaration
        XPathExpression expr = null;
        StringBuilder xmlDataSource = null;
        string tempString = string.Empty;
        XmlDocument xmlDoc = null;
        XPathDocument doc = null;
        #endregion

        protected override void OnInit(EventArgs e)
        {
            treeViewLeftNav.PreRender += new EventHandler(treeViewLeftNav_PreRender);
            treeViewLeftNav.DataBinding += new EventHandler(treeViewLeftNav_DataBinding);
            treeViewLeftNav.TreeNodeDataBound += new TreeNodeEventHandler(treeViewLeftNav_TreeNodeDataBound);
        }
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                //Generate XML file from the SharePoint list
                xmlDataSource = new StringBuilder();
                xmlDataSource.Append("<?xml version='1.0' encoding='iso-8859-1' ?>");

                using (SPSite spSite = new SPSite(SPContext.Current.Site.ID))
                {
                    using (SPWeb spWeb = spSite.OpenWeb())
                    {
                        SPList list = spWeb.GetList(spWeb.Url + "/Lists/LeftNavigation");
                        DataTable dtList = list.Items.GetDataTable();
                        DataRow[] dtListItems = dtList.Select("Hierarchy = ''");
                        foreach (DataRow dtRowItem in dtListItems)
                        {
                            tempString = dtRowItem["Title"].ToString();
                            //Recursive method to create a XML file from list
                            GenerateXMLDS(xmlDataSource, dtRowItem, dtList);
                        }
                    }
                }
            }
        }

        void treeViewLeftNav_TreeNodeDataBound(object sender, TreeNodeEventArgs e)
        {
            if (string.IsNullOrEmpty(e.Node.NavigateUrl))
            {
                e.Node.SelectAction = TreeNodeSelectAction.Expand;
            }
        }

        private void GenerateXMLDS(StringBuilder xmlDataSource, DataRow dtRowItem, DataTable dtList)
        {
            try
            {
                xmlDataSource.Append("<menu name='" + dtRowItem["Title"] + "' url='" + (String.IsNullOrEmpty(dtRowItem["URL"].ToString().Trim('/')) ? String.Empty : (SPContext.Current.Site.Url + "/" + dtRowItem["URL"].ToString().Trim('/'))) + "' level='" + tempString + "'>");
                DataRow[] dtListItems = dtList.Select("Hierarchy = '" + dtRowItem["Title"] + "'");
                if (dtListItems.Length != 0)
                {
                    foreach (DataRow dtRowItemNew in dtListItems)
                    {
                        tempString += "/" + dtRowItemNew["Title"];
                        GenerateXMLDS(xmlDataSource, dtRowItemNew, dtList);
                    }
                }
                if (-1 != tempString.LastIndexOf('/'))
                    tempString = tempString.Remove(tempString.LastIndexOf('/'));

                xmlDataSource.Append("</menu>");
            }
            catch (Exception)
            {
                // Handle and Log your exception here.
            }
        }

        void treeViewLeftNav_DataBinding(object sender, EventArgs e)
        {
            try
            {
                //Bind XML data source to the Treeview control
                xmlDS.Data = xmlDataSource.ToString();
            }
            catch (Exception)
            {
                // Handle and Log your exception here.
            }

        }

        void treeViewLeftNav_PreRender(object sender, EventArgs e)
        {
            try
            {
                //Code block to maintain the previous state of treeview control after redirection
                treeViewLeftNav.CollapseAll();

                xmlDoc = new XmlDocument();
                xmlDoc.LoadXml(xmlDataSource.ToString());
                doc = new XPathDocument(new XmlNodeReader(xmlDoc.DocumentElement));
                XPathNavigator nav = doc.CreateNavigator();

                string currentUrl = Request.Url.AbsoluteUri.Replace("%20", " ");
                string pageTitle = Request.QueryString["title"];

                expr = nav.Compile("//menu[@name='" + pageTitle + "' and @url='" + currentUrl.Trim('/') + "']");
                XPathNodeIterator iterator = nav.Select(expr);

                while (iterator.MoveNext())
                {
                    string currentNodeToSelect = iterator.Current.GetAttribute("level", nav.NamespaceURI);
                    TreeNode treeNodeToExpand = treeViewLeftNav.FindNode(currentNodeToSelect.Trim('/'));
                    if (null != treeNodeToExpand)
                    {
                        treeNodeToExpand.Selected = true;

                        while (null != treeNodeToExpand)
                        {
                            treeNodeToExpand.Expand();
                            treeNodeToExpand = treeNodeToExpand.Parent;
                        }
                    }
                }
            }
            catch (Exception)
            {
                // Handle and Log your exception here.
            }
        }
    }
}

Step 5: Add this User Control in master page.

Note: There is limitation of the treeview control. It will not maintain a state after redirection. To overcome this limitation, I have introduced ‘Hierarchy’ column which always point’s to parent node.
When user clicks on any of the links from navigation it is redirected to the URL specified in NavigationUrl property of Treeview. As the user control is placed in master page, in treeView_PreRender event with the help of the current URL I am finding the level of the clicked node and expanding it based on that. 

Wednesday, June 15, 2011

Extend the Default Navigation Provider of SharePoint 2010 in 5 easy steps

There are many limitations, when you want to tweak the SharePoint’s default navigation to add multiple levels in left/top navigation.

SharePoint supports only 2 level of fly-out navigation. For multilevel navigation there are following approaches:
1. Create your own user control
2. Extend the SharePoint Navigation Provider

I have extended the default navigation provider PortalSiteMapProvider to show multilevel fly-out navigation.

Step 1: Create a C# class library project with the following code:

using System.Web;
using Microsoft.SharePoint.Publishing;
using Microsoft.SharePoint.Publishing.Navigation;

namespace CustomNavProvider
{
public class Navigation : PortalSiteMapProvider
{
SiteMapNodeCollection siteMapNodeColl = null;

public override SiteMapNodeCollection GetChildNodes(System.Web.SiteMapNode node)
{
    PortalSiteMapNode pNode = node as PortalSiteMapNode;
    if (pNode != null)
    {
        if (pNode.Type == NodeTypes.Area)
        {
            SiteMapNodeCollection nodeColl = base.GetChildNodes(pNode);

            //We can use SharePoint list or XML file to make our navigation configurable.

            SiteMapNode childNode = new SiteMapNode(this, "<http://www.mainsite.com>",
            "<http://www.mainsite.com>", "Root site");

            SiteMapNode childNode1 = new SiteMapNode(this,"<http://www.level1site.com>",
            "<http://www.level1site.com>", "Level 1 Site");

            SiteMapNode childNode2 = new SiteMapNode(this,"<http://www.level2site.com>",
            "<http://www.level2site.com>", "Level 2 Site");

            SiteMapNode childNode11 = new SiteMapNode(this,
"<http://www.level11site.com>", "<http://www.level11site.com>", "Subsite level 11");

SiteMapNode childNode12 = new SiteMapNode(this, "<http://www.level12site.com>",
            "<http://www.level12site.com>", "Subsite level 12");

SiteMapNode childNode111 = new SiteMapNode(this, "<http://www.level111site.com>",
            "<http://www.level111site.com>", "Site Pages 1");

SiteMapNode childNode112 = new SiteMapNode(this, "<http://www.level112site.com>",
            "<http://www.level112site.com>", "Site Pages 2");

            nodeColl.Add(childNode);

            siteMapNodeColl = new SiteMapNodeCollection();
            siteMapNodeColl.Add(childNode111);
            siteMapNodeColl.Add(childNode112);

            childNode12.ChildNodes = siteMapNodeColl;

            siteMapNodeColl = new SiteMapNodeCollection();
            siteMapNodeColl.Add(childNode11);
            siteMapNodeColl.Add(childNode12);

            childNode1.ChildNodes = siteMapNodeColl;

            siteMapNodeColl = new SiteMapNodeCollection();
            siteMapNodeColl.Add(childNode1);
            siteMapNodeColl.Add(childNode2);
            childNode.ChildNodes = siteMapNodeColl;

            return nodeColl;
        }
        else
            return base.GetChildNodes(pNode);
    }
    else
        return new SiteMapNodeCollection();
}
}
}

Step 2: Deploy the solution or Copy the dll in GAC.

Step 3: Add following entry in web.config for your web application

<siteMap defaultProvider="CurrentNavigation" enabled="true">
      <providers>
<add name="MyCustomNavigationProvider" type="CustomNavProvider.Navigation, CustomNavProvider, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b8dd7d04a4a7e53e"  NavigationType="Current" />

Step 4: Create a custom master page or update the v4.master with SharePoint designer 2010, and then add the following code under the left navigation’s ContentPlaceHolder element.

<SharePoint:AspMenu
ID="LeftNavigationMenu"
  Runat="server"
  DataSourceID="leftSiteMap1"
  EnableViewState="false"
  AccessKey="<%$Resources:wss,navigation_accesskey%>"
    Orientation="Vertical"
    StaticDisplayLevels="1"
    MaximumDynamicDisplayLevels="4"
    DynamicHorizontalOffset="0"
    StaticPopoutImageUrl="/_layouts/1033/images/next.gif"
    StaticPopoutImageTextFormatString=""
    DynamicHoverStyle-BackColor="#CBE3F0"
    SkipLinkText=""
    StaticSubMenuIndent="0"
    CssClass="ms-topNavContainer">
    <StaticMenuStyle/>
    <StaticMenuItemStyle CssClass="ms-topnav" ItemSpacing="0px"/>
    <StaticSelectedStyle CssClass="ms-topnavselected" />
    <StaticHoverStyle CssClass="ms-topNavHover" />
    <DynamicMenuStyle BackColor="#F2F3F4" BorderColor="#A7B4CE"
      BorderWidth="1px"/>
    <DynamicMenuItemStyle CssClass="ms-topNavFlyOuts"/>
    <DynamicHoverStyle CssClass="ms-topNavFlyOutsHover"/>
    <DynamicSelectedStyle CssClass="ms-topNavFlyOutsSelected"/>
  </SharePoint:AspMenu>

<asp:SiteMapDataSource
  ShowStartingNode="False"
  SiteMapProvider="MyCustomNavigationProvider"
  id="leftSiteMap1"
  runat="server"
  StartFromCurrentNode="true"/>

Step 5: Recycle your application pool and your site should now displaying the updated navigation.