MOSS custom navigation provider
Has there been a customer requirement for you where you wanted to create the left or Top navigation based on a custom recordset or from a List (stored in Sharepoint so that easy to update) in a WCM using Sharepoint 2007 / MOSS. Did you have a requirement to Expand and Collapse the menu (server side, not AJAX style) using the custom provider? Are you using .Net framework 3.5? If all this is your requirement then you should look at the great work Jonathan Dibble has put here: https://sharepoint.wanderingleaf.net/professional/Lists/Posts/Post.aspx?ID=13
It was pleasure working with Jonathan on this project, where the Left Navigation Provider was really annoying and Jonathan with his magic solved this issue and not only solved it but also improvised it with LINQ.
Great Job Jonathan
Following is Jonathan Dibble's post:
1/17/2009
Recently, i was asked to write a navigation provider that used a SharePoint List as the data source. Since SharePoint is built upon ASP.Net, adding a new navigation provider was a fairly straightforward task.
As always, let’s dive right into the code and work the details out from there.
Step 1: Create the Data Source
Let’s define a List called NavLinks. The structure is simple:
Figure 1 |
Url, Title and Description need no explanation. SortOrder determines the order in which to display sibling nodes. Parent is a lookup back to this list. This allows users to easily manipulate the tree. After the list is built, go ahead and populate it.
Figure 2 |
First, we’ve defined a root node. This becomes the menu starting point. Beyond that, we have a list of nodes with a parent/child relationship. Notice the Urls, they are relative to the current web application. All local Urls should be entered as relative Urls. We’ll talk about this more when we dive into the code.
I added a Group By to the view to make the navigation a bit easier to follow.
Figure 3 |
Verification
Upon completion of this step, you should have a list called NavList (https://yourSharePointSite/<PathToWeb>/Lists/NavList)
Step 2: Create the Basic Provider
There are literally dozens of articles out there describing how to do this, so I am just going to glance over it briefly.
- Open Visual Studio 2008 (my provider uses LINQ so you need at least VS 2008 with all the latest goodies) and create a new Class Library Project.
- Add references to System.Web, System.Configuration and Microsoft.SharePoint if not already present.
- Create a new class using System.Web.SiteMapProvider as the base class. Don’t bother with any code just yet, just create the skeleton class.
- Sign the Assembly.
- Build and Deploy to GAC.
What we’re doing here is just creating something that compiles so we can get the public key and finish configuring SharePoint. A tip I like to do is use Post-Build commands to help us out.
Figure 4 |
What this does is uninstall the existing assembly (/u), installs the new assembly (/i) and restarts the SharePoint application pool (iisapp.) You will have to change the 3rd statement to match your app pool. Run IISAPP with no command lines to get a list of running application pools. Once you build your DLL, everything should be registered and the build output should show the public key name . Really, the only slightly irky thing is you have to build twice to get the public key. GacUtil only shows the key when you are uninstalling, not when you are installing. No biggie.
Figure 5 |
Verification
At the end of step 2, you should have a DLL that compiles and registered in the GAC. You should have the type name (your class name) and the assembly name (the name of the DLL) and you should also have the public key from the assembly (output from the build.)
Step 3: Register the Provider with SharePoint
This one is actually pretty quick and easy.
- Open the web.config for your SharePoint site.
- Search for the siteMap node.
- Add your entry.
1: <siteMap defaultProvider="CurrentNavSiteMapProvider" enabled="true">
2: <providers>
3:
4: <!--
5: CacheTimeout = How long to keep in cache
6: HideCousins = Display Cousins?
7: HideNephews = Display Nephews?
8: NavListUrl = The Site Collection relative navigation list.
9: -->
10: <add
11: name="SPListNavigationProvider"
12: type="JGD.Web.SPListNavigationProvider, SPListNavigationProvider, Version=1.0.0.0, Culture=neutral, PublicKeyToken=060e356544c1c245"
13: CacheTimeout="1"
14: HideCousins="True"
15: HideNephews="True"
16: NavListUrl="Lists/NavList"
17: IgnoreQuery="True"
18: />
19:
20: <add name="SPNavigationProvider" type="Microsoft.SharePoint.Navigation.SPNavigationProvider, Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
Lines 1,2 and 20 already exist and are shown so you know where to insert the custom code. The good bits are lines 10-18. Line 11 is the provider name, can be whatever you want, just remember what is is as you will use it on whatever pages you want to use this provider. Line 12, that’s our provider. The first part is the type name, second part assembly name, and the 3rd is our public key. Lines 13-17 are custom properties for our provider. Go ahead and add them now, it won’t harm anything.
Verification
At this point, you should have a modified web.config file.
Sidebar: Cousins and Nephews?The Publishing Provider has some nice features. For example, when you are on a Node, it shows you just that node downward, so only the selected node’s siblings and children. To mimic this behavior, I added some properties, HideNephews and HideChildren. I suppose I could have simplified it with a ShowOnlyDescendants property, but I didn’t. You can fix it later.
Here we see all the S nodes are cousins to each other. All the P nodes are also cousins. The S nodes are also nephews to their non-parent nodes (S2 is a nephew to P1 and P3 and a child of P2.) What this means is if we are on Node P1, we will only see the S1 nodes (and siblings of P1) if HideNephews is true. If we are on node S3 and HideCousins is true, we will only see the siblings of S3 and the parent of S3 (P3) and the siblings of the Parent (P2 and P1) but not the children of the siblings of the parent (we will not see S2.x or S1.x) Confused? Well try it out and see. |
Step 4: The Magic
To make all this happen, we’re going to:
- Configure the Provider upon application startup
- Define an in-memory record reflecting each SharePoint List Item
- Read the items from the SharePoint List when the provider is called
- Cache the items for x minutes for performance reasons
- Use LINQ to query our in memory collection of Nodes
Let’s take each task in Sequence.
Configure the Provider upon application startup
First thing we need to do is define some variables to hold our configuration values:
1: /// <summary>
2: /// Config key name for CacheTimeout option.
3: /// </summary>
4: const string CACHETIMEOUT = "CacheTimeout";
5:
6: /// <summary>
7: /// Config key name for HideNephews option.
8: /// </summary>
9: const string HIDENEPHEWS = "HideNephews";
10:
11: /// <summary>
12: /// Config key name for HideCousins option.
13: /// </summary>
14: const string HIDECOUSINS = "HideCousins";
15:
16: /// <summary>
17: /// Config key name for NavListUrl option.
18: /// </summary>
19: const string NAVLISTURL = "NavListUrl";
20:
21: /// <summary>
22: /// Config key name for the IgnoreQuery option.
23: /// </summary>
24: const string IGNOREQUERY = "IgnoreQuery";
25:
26: /// <summary>
27: /// Gets/Sets the number of minutes to cache the nodes.
28: /// in memory.
29: /// </summary>
30: public double CacheTimeout { get; set; }
31:
32: /// <summary>
33: /// Gets/Sets the value used to sort the in memory dataset.
34: /// </summary>
35: public string SortOrder { get; set; }
36:
37: /// <summary>
38: /// Determines if nephews are hidden. If true, provider
39: /// will only return the current node, children of the current node
40: /// and siblings of the current node.
41: /// </summary>
42: public bool HideNephews { get; set; }
43:
44: /// <summary>
45: /// Determines if cousins are hidden. If true, cousin nodes
46: /// will not be shown.
47: /// </summary>
48: public bool HideCousins { get; set; }
49:
50: /// <summary>
51: /// Gets/Sets the Url used to drive list navigation.
52: /// Once cached, the nodes are not re-read, even if this
53: /// value changes.
54: /// </summary>
55: public string NavListUrl { get; set; }
56:
57: /// <summary>
58: /// Determines if the Query (?...) is igniored when doing a node lookup
59: /// </summary>.
60: public bool IgnoreQuery { get; set; }
We’re using the new get; set; goodies from .Net 3.0. This will automagically generate the properties for us. Now we need to set the values upon initialization.
1: /// <summary>
2: /// Sets up the provider.
3: /// </summary>
4: /// <param name="name"></param>
5: /// <param name="config"></param>
6: public override void Initialize(string name,
7: System.Collections.Specialized.NameValueCollection providerConfig) {
8: Mark();
9:
10: CacheTimeout = tryParse(providerConfig[CACHETIMEOUT], 1.0);
11:
12: HideNephews = tryParse(providerConfig[HIDENEPHEWS], true);
13: HideCousins = tryParse(providerConfig[HIDECOUSINS], true);
14:
15: IgnoreQuery = tryParse(providerConfig[IGNOREQUERY], true);
16:
17: NavListUrl = getValue(providerConfig[NAVLISTURL], "Lists/NavList");
18: }
19:
20: private bool tryParse(string value, bool defaultValue) {
21: bool retVal;
22: return bool.TryParse(value, out retVal) ? retVal : defaultValue;
23: }
24:
25: private double tryParse(string value, double defaultValue) {
26: double retVal;
27: return double.TryParse(value, out retVal) ? retVal : defaultValue;
28: }
29:
30: private string getValue(object value, string defaultValue) {
31: if (value == null)
32: return defaultValue;
33:
34: return value.ToString();
35: }
Initialize is one of the routines provided by the base class. We’re passed a name value collection and we assign our properties. You can ignore line 8, that is for debugging. See the full source to see what that line does. tryParse is a helper function which will either return the value from the config file or a default value if the desired key doesn’t exist.
Define an in-memory record reflecting each SharePoint List Item
1: public class SPListItemSiteNode {
2: public string Key { get; set; }
3: public string Title { get; set; }
4: public string Description { get; set; }
5: public string Url { get; set; }
6: public double SortOrder { get; set; }
7: public SiteMapNode SiteMapNode { get; set; }
8:
9: public string ParentKey { get; set; }
10: public SPListItemSiteNode Parent { get; set; }
11: }
This is a very simple class that contains the properties we need to build our navigation.
Read the items from the SharePoint List when the provider is called
1: /// <summary>
2: /// Reads the node from a SharePoint list and return it as a List<>
3: /// </summary>
4: /// <returns></returns>
5: private List<SPListItemSiteNode> buildNodeTree() {
6:
7: var nodeIMDB = new List<SPListItemSiteNode>();
8:
9: string navListUrl = NavListUrl;
10:
11: Microsoft.SharePoint.SPContext spContext = Microsoft.SharePoint.SPContext.Current;
12: //Don't dispose of this object - we didn't create it, we don't destroy it.
13: Microsoft.SharePoint.SPSite currentSite = spContext.Site;
14:
15: using (SPWeb listWeb = currentSite.OpenWeb(navListUrl, false)) {
16:
17: SPList navList = listWeb.GetList(listWeb.ServerRelativeUrl + "/" + navListUrl);
18:
19: if (navList != null) {
20:
21: //Future functionality: appending the navlist url
22: //so we can aggregate multiple lists.
23: string keyPrefix = navListUrl;
24:
25: foreach (SPListItem listItem in navList.Items) {
26: var newNode = new SPListItemSiteNode() {
27: Title = listItem.Title,
28: Description = getValue(listItem["Description"], string.Empty),
29: Url = listItem["Url"].ToString().ToLower(),
30: Key = keyPrefix + listItem.ID.ToString(),
31: SortOrder = (Double)listItem["SortOrder"],
32: ParentKey = listItem["Parent"] == null ? ROOTID :
33: keyPrefix + (listItem["Parent"] as string).Split(';')[0]
34: };
35:
36: newNode.SiteMapNode = new SiteMapNode(this, newNode.Key, newNode.Url, newNode.Title, newNode.Description);
37: nodeIMDB.Add(newNode);
38: }
39:
40: //Set Parents
41: foreach (var row in nodeIMDB)
42: if (row.ParentKey != ROOTID) {
43: //Look for the parent row
44: var parentRow = nodeIMDB.Find(n => n.Key == row.ParentKey);
45:
46: row.Parent = parentRow;
47: row.SiteMapNode.ParentNode = parentRow.SiteMapNode;
48: }
49: }
50: }
51:
52: return nodeIMDB;
53: }
This is fairly straightforward, read from the SharePoint list, load it into the List<> object and return it. The only thing slightly funky is the two-pass reader to set the parents. Before we can build the tree, we have to have all the nodes loaded, which is why we have it in two loops. Also check out line 44, we’re using a Lambda expression to go and find the parent row.
Cache the items for x minutes for performance reasons
To achieve this, we’re going to create a public property which calls our buildNodeTree function and stores the result. As the provider runs, it will access this property which will be responsible for the caching.
1: /// <summary>
2: /// Returns the Cached Nodes list.
3: /// The nodes are read from the SPList and then caches the results for better
4: /// performance.
5: /// </summary>
6: public List<SPListItemSiteNode> AllNodes {
7: get {
8: attach();
9:
10: //Build our cached key. We use the provider name in case we loaded
11: //multiple copies
12: string cacheKey = this.Name + "allNodes";
13:
14: var nodes = (List<SPListItemSiteNode>)HttpContext.Current.Cache[cacheKey];
15:
16: if (nodes == null)
17: //Simple double check lock to update cache
18: lock (HttpContext.Current.Cache) {
19: nodes = (List<SPListItemSiteNode>)HttpContext.Current.Cache[cacheKey];
20: if (nodes == null) {
21: nodes = buildNodeTree();
22: HttpContext.Current.Cache.Add(cacheKey, nodes, null,
23: DateTime.Now.AddMinutes(CacheTimeout),
24: System.Web.Caching.Cache.NoSlidingExpiration, System.Web.Caching.CacheItemPriority.Normal, null);
25: }
26:
27: }
28:
29: return nodes;
30: }
31: }
You can ignore line 8, that’s some debug code to attach the debugger, if you’d like to see it, check out the full source.
Now we can leverage the AllNodes everywhere and not worry about caching anywhere else.
Use LINQ to query our in memory collection of Nodes
1: /// <summary>
2: /// Returns the root node
3: /// </summary>
4: /// <returns></returns>
5: protected override SiteMapNode GetRootNodeCore() {
6: Mark();
7:
8: //Find the node with no parent
9: SiteMapNode rootNode = AllNodes.First<SPListItemSiteNode>(n => n.Parent == null).SiteMapNode;
10:
11: rootNode.ParentNode = null;
12:
13: return rootNode;
14: }
1: /// <summary>
2: /// Given the URL, finds the appropriate node.
3: /// </summary>
4: /// <param name="rawUrl"></param>
5: /// <returns></returns>
6: public override SiteMapNode FindSiteMapNode(string rawUrl) {
7: Mark(rawUrl);
8:
9: rawUrl = rawUrl.ToLower();
10:
11: if (IgnoreQuery && rawUrl.Contains('?')) {
12: int offset = rawUrl.IndexOf('?');
13: rawUrl = rawUrl.Substring(0, offset);
14: }
15:
16: try {
17: var row = AllNodes.First<SPListItemSiteNode>(i => i.Url.Contains(rawUrl));
18:
19: return row.SiteMapNode;
20:
21: } catch {
22: return null;
23: }
24: }
1: /// <summary>
2: /// Given a node, return the parent node.
3: /// </summary>
4: /// <param name="node"></param>
5: /// <returns></returns>
6: public override SiteMapNode GetParentNode(SiteMapNode node) {
7: Mark(node);
8:
9: var row = AllNodes.First<SPListItemSiteNode>(n => n.Key == node.Key);
10:
11: if (row.Parent == null)
12: return null;
13: else
14: return
15: AllNodes.First<SPListItemSiteNode>(n => n.Parent.SiteMapNode == node).SiteMapNode;
16: }
1: /// <summary>
2: /// Gets all children for a specific node
3: /// </summary>
4: /// <param name="node"></param>
5: /// <returns></returns>
6: public override SiteMapNodeCollection GetChildNodes(SiteMapNode node) {
7:
8: Mark(node.Title);
9:
10: SiteMapNodeCollection childNodes = new SiteMapNodeCollection();
11:
12: //Don't show nephew nodes -
13: if (HideNephews &&
14: node.ParentNode == CurrentNode.ParentNode && node != CurrentNode) {
15: return childNodes;
16: }
17:
18: //Don't show cousins
19: if (HideCousins && node != CurrentNode) {
20: SiteMapNode myParent = CurrentNode.ParentNode;
21:
22: if (myParent != null) {
23: SiteMapNode myGrandparent = myParent.ParentNode;
24:
25: if (myGrandparent != null &&
26: (node.IsDescendantOf(myGrandparent) && node != myParent))
27: return childNodes;
28:
29: }
30: }
31:
32: ///Find all nodes with this parent
33: var childRows = from n in AllNodes
34: where n.Parent != null && n.Parent.SiteMapNode == node
35: orderby n.SortOrder
36: select n;
37:
38: if (childRows != null) {
39: foreach (var childRow in childRows) {
40: childNodes.Add(childRow.SiteMapNode);
41: childRow.SiteMapNode.ParentNode = node;
42: }
43: }
44:
45: return childNodes;
46: }
Lines 33-36 do the most work. It’s our LINQ query to return the nodes for this parent.
That’s It! We now have our provider.
Verification
At this point, the code for the provider is complete, everything should compile and deploy to the GAC.
Step 5: Put it to use
Add the following code to a master page, or wherever you’d like to use it.
Figure 6 |
The key control is the SiteMapDataSource. That acts as the glue between our provider and the AspMenu control. The SMDS also allows us to change the starting node and adds some other UI based functionality (like ShowStartingNode.) Try playing around with it and see how it affects the UI. Pay attention to that StartingNodeOffset.
When it’s all said and done, lets see a page with it in action.
Figure 7 |
Here I’ve put our new menu on top and the Out-of-Box menu below. You can see how the OOB is just a flat view and our custom menu has levels. Needless to say, I am not a UI guy, so my only styles are color changes, but you can really take this far. In fact, you can use the Dynamic properties to have flyout menus as well.
Figure 8 |
After navigating to Parent List 2, we can see how the cousin/nephew hiding is working. We’re only seeing the child nodes of Parent List 2.
Comments
Anonymous
October 13, 2010
Hi, It's really a good implementation of custom site navigation and I am implementing the same in our project. I am not able to access the pictures as the link no longer exists. Would it be possible to send me the complete article along witth images on my email id:sksmart21@yahoo.com.Anonymous
May 10, 2011
Hi I am in the need of just such a custom navigation for a client and I was wondering if there is a way to provide me with the original pictures for this blog post because the picture links in this current post are dead. my e-mail is avm_81@passagen.seAnonymous
March 20, 2012
Hi Ketaanh thanks for sharing knowledge can you please image in this blog as they are not showing up and please share code to download if possible Thanks Ronak