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. 

4 comments:

  1. Thanks for sharing Nilesh
    -Ibrahim Khatri

    ReplyDelete
    Replies
    1. Hey, Thanks for the comment.
      Where are you nowdays... :)

      Delete
  2. Hi,

    I have used the code given above. There are two issues that i am facing.
    1. The tree is not generated when i name the Look up column "Hierarchy" and i get an error message Column "Hierarchy" not found. When i use "test" as look up column name the tree is generated.
    2. The tree does not maintain its state after page reload.

    Please suggest what i am doing wrong.

    Mohammad Aqeel Mirza

    Share Point Developer.

    ReplyDelete
    Replies
    1. Please provide the screenshot of the 'LeftNavigation' list.

      Delete