Customizing the Quick Launch menu with SPNavigationNode, SPNavigationNodeCollection and Audiences

I am starting to feel like I have a love hate relationship with MOSS 2007. On the one hand, the WSS API's are powerful enough to really save time when building solutions. But other times, those same API's are just so bizarre, it makes me wonder if it's even worth it.

When you install the Visual Studio 2005 extensions for Windows SharePoint Services 3.0, you get a nice tool called SharePoint Solution Generator. Using this tool, you can point in an existing SharePoint Site and it will generate a Visual Studio 2005 solution that can be compiled into a SharePoint Solution file, or wsp file that can then be used to install the solution as a site definition on any server.

Unfortunetely, the solution that the tool generate omits a lot of the manual customizations that you made to your site in the first place. I plan on writing a blog post just about this in the next couple of days. Today I want to focus on just one of those ommissions, the Quick Launch Bar.

By default, the Quick Launch bar will lose any of the manual custmization you made before exporting your site. Lucky for us, we can use the WSS object model to add custom code to re-customize the Quick Launch naviagtion section when the site is activated. We do this by adding code to the OnActivated method of the Site Definition class that was generated for you in the Site Provisioning Handler folder of the solution that was created for you.

What I discovered when I attempted to do this, is that the API for managing the quick launch navigation doesn't behave they way you would expected it. The first thing I did was try to add code to delete the existing navigation so I can add my custom navigtion without worrying what was there by default.

SharePoint refers to each navigation item as SPNavigationNode. The SPWeb object has a Navigation property which itself has a QuickLaunch property that exposes a SPNavigationNodeCollection which is a collection of those nodes which can be nested just like a tree node.

So to get the collection I wrote the folowing code: (web is the current SPWeb object for the site I was working on)

SPNavigationNodeCollection nodes = web.Navigation.QuickLaunch;

Then I figured to write  a foreach loop as follows:

foreach (SPNavigationNode node in nodes)
{
   node.Delete();
}

But when I tried to run this code, I would get a weird exception saying that it couldn't be completed. Stepping through the code, I saw that for a list of five list items, none of which had any nested items, it would always crap out after the third one. I tried to switch to a normal for loop, but had the same problem. I also noticed while debugging that the nodes "Count" property never changed, even after the first few nodes were deleted. I also noticed that if I called the Delete method of each node in the collection directly, from 0 to 5, after I deleted the "[2]" node, the  references to "[3]" and "[4]" were no longer valid. But if I deleted them backward, it worked fine. So the code I ultimatley ended up using was as follows:

            for(int i = nodes.Count - 1; i >= 0; i--)
            {
                nodes[i].Delete();
            }

This enabled me to delete the items, but I had to figure out how to create them too. There were two types of links I wanted to add: headings and normal links. I also wanted to control who can see some of the links using audiences. The normal SPNavigation node has no such properties to allow control of this. Searching google for an answer, I found another object, the SPNavigationSiteMapNode. This object had all the properties I needed, but it offerred no way of casting down to a SPNavigationNode that I could find. There was also no comprable collection object. I couldn't find a way to add an instance of that class back to the QuickLaunch SPNavgationNodeCollection object.

But the SPNavigationSiteMapNode has a static method called CreateSPNavigationNode that takes four parameters: the link name, the url, the link type, and the node collection it should be added to. The link type allowed you to specify whether it is a regular link or a heading. Here is the code I used to add a normal link:

SPNavigationNode node = SPNavigationSiteMapNode.CreateSPNavigationNode("Link Title", web.Url + "/url.aspx", Microsoft.SharePoint.Publishing.NodeTypes.AuthoredLinkPlain, nodes);

This still does not solve the audience problem. While debugging, I notced the SPNavigatonNode object had a Properties collection. This collection contained a number of key value pairs like a HashTable. While stepping through code, I noticed that the links for which I had manually assigned an audience had a property named "Audience" that had a value of four semicolons followed by the group that I had named as the audience. So I tried out the following code:

SPNavigationNode node = SPNavigationSiteMapNode.CreateSPNavigationNode("Link Title", web.Url + "url2.aspx", Microsoft.SharePoint.Publishing.NodeTypes.Heading, nodes);
node.Properties.Add("Audience", ";;;;" + "Name of SPGroup");
node.Update();
           

And it worked like a charm.

As you can see, coding against the SharePoint object model isn't as straight forward or well documented as you might think. But I guess that makes it more interesting too. Hopefully these war stories can help alleviate some of the thrashing you might otherwise face.

Happy coding.

Maping from SPList Field Types to .Net Types

Using a Custom List to Store Configuration Data