Thursday, May 7, 2009

MOSS custom navigation provider for left nav

As promised, here is the code for the custom left navigation provider in MOSS. It uses a similar concept as the custom top navigation provider I detailed in this post, but there are some differences. Unlike a top or global navigation which is fairly consistent across your brand, the left navigation can change from site across site collections. It can even be different in different Webs. So based on that consideration, this solution handles those cases. Again, this navigation reads from a SharePoint list on a site. The navigation can read from a list in the root site of a site collection, the current site, or a site below the root site (this was a specific requirement) - all this is in a case statement so pretty easy to change.

Again, I have used this before successfully for a busy site. There are various settings in the Web.config that allow you to customize as per your needs.

This is demonstrated in the picture below. Only the Quick view is changeable on a site per site basis. The Quick Links section is consistent across the brand.





Again, the navigation is being read from a list in SharePoint, such as this. This doesn't have the same data, but should get the point across anyway.




#region Code Comment Header
/*******************************************************************************************
*
*
* $History: CustomLeftNavProvider.cs $
*
* *****************************************************************************************/
#endregion
using System;
using System.Configuration;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.Web.Caching;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Security;
using Microsoft.SharePoint.Publishing;
using Microsoft.SharePoint.Publishing.Navigation;
using Microsoft.SharePoint.Administration;
using CompanyXXX.ExceptionManagement;
using System.Security.Permissions;
using System.Security;

namespace CompanyXXX.Dept.MOSS.Utilities.Navigation.Providers
{
//Assign the neccessary security permissions
[AspNetHostingPermissionAttribute(SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)]
[SharePointPermissionAttribute(SecurityAction.LinkDemand, ObjectModel = true)]
[AspNetHostingPermissionAttribute(SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal)]
[SharePointPermissionAttribute(SecurityAction.InheritanceDemand, ObjectModel = true)]
public class CustomLeftNavProvider : PortalSiteMapProvider
{
//Create the in memory objects for storage and fast retreival
protected SiteMapNodeCollection siteMapNodeColl;
protected ArrayList childParentRelationship;
protected ArrayList topLevelNodes;
//protected SPSite sourceListSite;
//protected SPWeb sourceListWeb;
//protected bool needDisposing = false;

///
/// Override the initialize method of the superclass
///

///
///
public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
{
// Verify that config isn't null
if (config == null)
throw new ArgumentNullException("config is null");

// Assign the provider a default name if it doesn't have one
if (String.IsNullOrEmpty(name))
name = "CustomLeftNavProvider";

// Add a default "description" attribute to config if the
// attribute doesn’t exist or is empty
if (string.IsNullOrEmpty(config["description"]))
{
config.Remove("description");
config.Add("description", "CompanyXXX custom current left navigation provider");
}

base.Initialize(name, config);
}


///
/// This method will be called for all nodes and subnodes that can have children under them. For eg, NodeTypes.AuthoringLink type node
/// cannot have child nodes.
///

/// The node to find child nodes for
/// The SiteMapNodeCollection which contains the children of the child nodes
public override SiteMapNodeCollection GetChildNodes(System.Web.SiteMapNode node)
{
return ComposeNodes(node);
}

///
/// Find the web and the list from where to read navigation
///

///
public void FindWeb(ref SPWeb sourceListWeb, ref SPSite sourceListSite, ref bool needDisposing)
{
//string currentNavList = ConfigurationManager.AppSettings["CurrentLeftNavigationListName"];
//inheritNav = true;

string navListWeb = String.Empty;

try
{
//Get the web where the current list is created
navListWeb = ConfigurationManager.AppSettings["LeftNavigationListWeb"];

//if the list is at the root site of the collection, the current web or even a different
//site - this is specified in the config file
switch (navListWeb) {
case "ROOT":
sourceListWeb = SPContext.Current.Site.RootWeb;
//leftNavList = sourceListWeb.Lists[currentNavList];
break;

case ".":
sourceListWeb = SPContext.Current.Web;
//leftNavList = sourceListWeb.Lists[currentNavList];
//inheritNav = false;
break;

case "LEVEL2":
//The list will be located one level below the top level
SPWeb cmsweb = SPContext.Current.Web;
SPWeb holder = null;
SPWeb rootWeb = SPContext.Current.Site.RootWeb;

//if current web is at second level or at root, pick the respective lists from there
if (cmsweb.ID == rootWeb.ID || cmsweb.ParentWeb.ID == rootWeb.ID)
sourceListWeb = cmsweb;
else
{
//else traverse up to the second level
while (cmsweb.ID != rootWeb.ID)
{
holder = cmsweb;
cmsweb = cmsweb.ParentWeb;
}
sourceListWeb = holder;
}
break;

default:
//instantiate sites and lists.
sourceListSite = new SPSite(navListWeb);
sourceListWeb = sourceListSite.OpenWeb();
//leftNavList = sourceListWeb.Lists[currentNavList];
//inheritNav = false;
needDisposing = true;
break;
}
}
catch (Exception ex)
{
System.Collections.Specialized.NameValueCollection exc =
new System.Collections.Specialized.NameValueCollection();

exc.Add("Method", "FindWeb");

if (!String.IsNullOrEmpty(navListWeb))
exc.Add("navListWebFromAppSetting", navListWeb);
else
exc.Add("navListWebFromAppSetting", "null");
if (sourceListSite != null)
exc.Add("sourceListSite", sourceListSite.Url);
else
exc.Add("sourceListSite", "null");
if (sourceListWeb != null)
exc.Add("sourceListWeb", sourceListWeb.Url);
else
exc.Add("sourceListWeb", "null");

throw ex;
}
}

///
/// Compose nodes when the method is called. At a minimum, this method gets called with the root node of every
/// site collection. We must attach the top level nodes to the root node for this method to get called for those
/// nodes as well.
///

///
///
public virtual SiteMapNodeCollection ComposeNodes(System.Web.SiteMapNode node)
{
//Create the SiteCollection to return
SiteMapNodeCollection children = new SiteMapNodeCollection();
SortedList orderedNodes = new SortedList();

SPWeb sourceListWeb = null;
SPSite sourceListSite = null;
SPList leftNavList = null;
bool cacheLeftNav = false;
bool needDisposing = false;
string currentNavList = String.Empty;

try
{
FindWeb(ref sourceListWeb, ref sourceListSite, ref needDisposing);

string cacheNav = ConfigurationManager.AppSettings["CacheLeftNavigation"];
if (String.Compare(cacheNav, "true", true) == 1)
cacheLeftNav = true;

//Account for the PortalWebSiteMapNode
PortalWebSiteMapNode prtlWebSiteMapNode = null;
int counter = 100;

currentNavList = ConfigurationManager.AppSettings["CurrentLeftNavigationListName"];

//If a rootnode, then add the top level children which are the same for every site collection
if (node == node.RootNode)
{
//Serve it from cache if possible
if (cacheLeftNav)
{
object topNodes = HttpRuntime.Cache["LeftNavTopNodes" + sourceListWeb.ID.ToString()];
if (topNodes != null && topNodes is SiteMapNodeCollection)
return ((SiteMapNodeCollection)topNodes);
}

//else find the list
leftNavList = sourceListWeb.Lists[currentNavList];

//Create the query to browse the list
SPQuery qry = new SPQuery();

SPFolder startFolder = null;

//Get the folder to start from
startFolder = leftNavList.RootFolder.SubFolders[ConfigurationManager.AppSettings["NavigationListStartFolderName"]];

//Get the query for this folder
qry.Folder = startFolder;

//Get all the items under the start folder using the query
SPListItemCollection ic = sourceListWeb.Lists[startFolder.ParentListId].GetItems(qry);

//Get the PortalWebSiteMapNode
if (node != null && node is PortalWebSiteMapNode)
{
prtlWebSiteMapNode = node as PortalWebSiteMapNode;

//Construct the nodes
foreach (SPListItem subitem in ic)
{
//Change the nodeTypes to Authored link for leaf nodes so that the GetChildNodes method is not called for those nodes.
NodeTypes ntypes = NodeTypes.Default;
if (subitem.Folder == null)
ntypes = NodeTypes.AuthoredLink;

//Create a PortalSiteMapNode
PortalSiteMapNode psmn = new PortalSiteMapNode(prtlWebSiteMapNode, subitem.ID.ToString(), ntypes,
subitem.GetFormattedValue(ConfigurationManager.AppSettings["UrlLink"]), subitem.Title,
subitem.GetFormattedValue(ConfigurationManager.AppSettings["UrlDescription"]));

//Order the nodes
try
{
int order = Convert.ToInt32(subitem.GetFormattedValue(ConfigurationManager.AppSettings["ItemOrder"]));
orderedNodes.Add(order, psmn);
}
catch (Exception ex)
{
orderedNodes.Add(counter++, psmn);
}
}

//Copy nodes in the right order
foreach (object portalSiteMapNode in orderedNodes.Values)
{
children.Add(portalSiteMapNode as PortalSiteMapNode);
}

//Add them to the cache
if (cacheLeftNav)
HttpRuntime.Cache["LeftNavTopNodes" + sourceListWeb.ID.ToString()] = children;
}
}
else
//Else this is a subnode, get only the children of that subnode
{
string nodeKey = node.Key;

if (cacheLeftNav)
{
//Get the children for this nodeKey from cache if they exist there
object subNodes = HttpRuntime.Cache["LeftNavChildNodes" + sourceListWeb.ID.ToString() + nodeKey];
if (subNodes != null && subNodes is SiteMapNodeCollection)
return ((SiteMapNodeCollection)subNodes);
}

leftNavList = sourceListWeb.Lists[currentNavList];

//Create the query to browse the list
SPQuery qry = new SPQuery();

//Convert the nodeKey into an integer
int ID = Convert.ToInt32(nodeKey);

//Find the current item
SPListItem item = leftNavList.GetItemById(ID);

//Find the item that and get its subitems
if (item != null && item.Folder != null)
{
SPFolder curFolder = item.Folder;

qry.Folder = curFolder;

//Get all the items in the current folder using the query
SPListItemCollection ic = sourceListWeb.Lists[curFolder.ParentListId].GetItems(qry);

//To get the PortalWebSiteMapNode, ask the root node
if (node.RootNode != null && node.RootNode is PortalWebSiteMapNode)
{
//Cast node to PortalWebSiteMapNode
prtlWebSiteMapNode = node.RootNode as PortalWebSiteMapNode;

foreach (SPListItem subitem in ic)
{
//Change the nodeTypes to Authored link for leaf nodes so that the GetChildNodes method is not called
//for those nodes.
NodeTypes ntypes = NodeTypes.AuthoredLink;
if (subitem.Folder != null)
ntypes = NodeTypes.Default;

//Create a PortalSiteMapNode
PortalSiteMapNode psmn = new PortalSiteMapNode(prtlWebSiteMapNode, subitem.ID.ToString(),
ntypes, subitem.GetFormattedValue(ConfigurationManager.AppSettings["UrlLink"]), subitem.Title,
subitem.GetFormattedValue(ConfigurationManager.AppSettings["UrlDescription"]));


//Order the nodes
try
{
int order = Convert.ToInt32(subitem.GetFormattedValue(ConfigurationManager.AppSettings["ItemOrder"]));
orderedNodes.Add(order, psmn);
}
catch (Exception ex)
{
orderedNodes.Add(counter++, psmn);
}
}

//Copy nodes in the right order
foreach (object portalSiteMapNode in orderedNodes.Values)
{
children.Add(portalSiteMapNode as PortalSiteMapNode);
}

//Add them to the cache
if (cacheLeftNav)
{
HttpRuntime.Cache["LeftNavChildNodes" + sourceListWeb.ID.ToString() + nodeKey] = children;
}
}
}
}
}
catch (Exception ex)
{
System.Collections.Specialized.NameValueCollection exc =
new System.Collections.Specialized.NameValueCollection();

exc.Add("Method", "ComposeNodes");

if (!String.IsNullOrEmpty(currentNavList))
exc.Add("CurrentLeftNavigationListNameFromAppSettings", currentNavList);
else
exc.Add("CurrentLeftNavigationListNameFromAppSettings", "null");

if (sourceListSite != null)
exc.Add("sourceListSite", sourceListSite.Url);
else
exc.Add("sourceListSite", "null");

if (sourceListWeb != null)
exc.Add("sourceListWeb", sourceListWeb.Url);
else
exc.Add("sourceListWeb", "null");

ExceptionManager.Publish(ex, exc);

//Do not dispose of the web or site referenced from SPContext

//Test that finally does get called even though a return statement is executed here
//Return empty collection
return new SiteMapNodeCollection();
}
finally
{
if (needDisposing)
{
if (sourceListWeb != null)
sourceListWeb = null;
if (sourceListSite != null)
sourceListSite = null;
}
}
return children;
}
}
}


Enjoy!!

4 comments:

h20 said...

Being a complete novice to Sharepoint, I can tell that custom left nav would be something beneficial for our site, but I haven't the slightest clue how to implement the code you have provided.

From a very basic overview level, can you explain to SP newbies like me, the steps needed to make this work.

Does all the code go on the masterpage?

How do I point the navigation to the list for a particular site/subsite?

Do I create lists for each site/subsite with appropriate list names - like "NavAboutUs", "NavOurProducts" and then add a left nav. webpart to each site/subsite?

Once I put the code in the master page, does this automatically create a web part that can be added from the SP site setting and configured at that point?

As you can tell from my questions, I am lost. I'd greatly appreciate a reply that would further my understanding.

Bobby said...

Faraz,

Would it be possible to get a copy of the code and list template you used for this and the custom navigation provider for the top nav? email is myspam75 (at) gmail.com

Vinny said...

Faraz, any chance you can post the code for this?

Meenu said...

Hi Faraz,

I need custom left navigation with subchilds. for e.g I need to diplay subsitesname,then name of document library and then alll folders inside libray and subfolders etc.
I am having issue with the same.Do you have any code related to this?