February 4, 2013

Updating the Global Navigation Menu Programatically in a Publishing Site Collection

When the SharePoint Publishing Infrastructure (site collection) feature is is turned on, administrators can manually configure global navigation links (as well as the quick launch menu) on the Site Settings > Navigation Settings page for each site. This is easy enough to do manually, so it doesn't always seem necessary to automate, but in some cases, as on a recent project, you might need to have this configured across a large number of sites. And it's always nice to automate as much as possible for a client anyway.

Since the navigation is configured differently with publishing turned on, the first step is to obtain an instance of the Microsoft.SharePoint.Publishing.PublishingWeb class. You'll likely have to add a new project reference to do this, as Microsoft.SharePoint.Publishing isn't referenced by default when creating a new SP2010 project in Visual Studio. Then, before you start adding and removing nodes, you'll want to break inheritance of the navigation menu and also the ordering method to manual.

SPWeb web; // Obtain an instance of this however you need to.
web.AllowUnsafeUpdates = true;
PublishingWeb pweb = PublishingWeb.GetPublishingWeb(web);
pweb.Navigation.InheritGlobal = false;
pweb.Navigation.OrderingMethod = OrderingMethod.Manual;

Once you're ready to start manipulating the navigation nodes, there are a few important "gotchas" to be aware of:
  1. SharePoint doesn't like the SPNavigationNodeCollection to be empty. Ever.
  2. When adding nodes, you have to call the update() method often and at specific times.
Due to Point #1, you can't simply empty the collection and then add the desired nodes - you'll get an SPException (lame). So you'll need to go to the trouble of copying the existing nodes to a new collection, adding your new nodes, and then deleting the old ones at the end.

// Store original global nav nodes.
SPNavigationNode[] oldNodes = new SPNavigationNode[pweb.Navigation.GlobalNavigationNodes.Count];
if (oldNodes.Length > 0)
{
  pweb.Navigation.GlobalNavigationNodes.CopyTo(oldNodes, 0);
}

Once you've saved off a copy of each of the existing nodes, you can add your desired items to the list. A couple of tips here as well:
  1. new SPNavigationNode() gives you an instance which is not fully initialized until it is added to a collection.
  2. If nesting items, you must call update() on the parent node before adding the child.
  3. The optional third parameter to the SPNavigationNode constructor is important.
What #1 means is that you can't build all the nodes, nest them, and then add the parents to the collection at the end, which is typically my desired approach. You have to add the parent node to the collection first to fully initialize it, call the update() method on it, and THEN you can safely add the child nodes.

SPNavigationNode ndParent = new SPNavigationNode("Parent Node", null, true);
ndParent = pweb.Navigation.GlobalNavigationNodes.AddAsLast(ndParent); // Add to collection.
ndParent.Update();
SPNavigationNode ndChild = new SPNavigationNode("Child Node", "http://www.google.com/", true);
ndChild = ndParent.Children.AddAsFirst(ndChild); // Nest child under parent.

The third argument, which I set to "true" in the example above, is a flag indicating whether the link is external or not. If you're creating an external link (such as to Google or any other site outside of SharePoint) this needs to be set to true. If you don't provide a value or set it to false, SharePoint will attempt to simplify/shorten the URL to a relative path. I didn't see any real advantage to this and it was giving me some trouble when trying to link to SharePoint pages, so I simply passed in "true" every time.

Once you've added your new nodes in the desired order (using AddAsFirst(), AddAsLast(), etc.), you can safely remove the older nodes from the collection.

// Delete old nodes.
foreach (SPNavigationNode oldNode in oldNodes)
{
  pweb.Navigation.GlobalNavigationNodes.Delete(oldNode);
}

Finally, when you've got the collection populated just how you want it, call update on the publishing web object and you're all set!

pweb.Update();
web.AllowUnsafeUpdates = false;

I hope this helps! I know I'm not the first person to have done or blogged about this, but I wasn't able to find any posts which addressed the "gotchas" I kept running into. If you've found any other tricks for dealing with the publishing menus, I'd love to hear about them!

3 comments:

  1. Thank you for article. but one question. what type of share point project do I have to create to put your code? Or ca u share me the full code ?
    thank you.

    ReplyDelete
  2. Sorry that I'm unable to share my full solution, but where to put the code depends on when you want it to run. In my case I had a branding feature with a feature receiver which, when activated, set the site to use a custom master page and then ran the above code to customize the global navigation menu. I'm guessing that's probably the most common use for this code but certainly not the only place it could work.

    The following MSDN page provides a walkthrough of creating a new SharePoint 2010 project in Visual Studio and adding a feature receiver. To have the code run when the feature is activated for a site, you'd use the FeatureActivated() method in the feature receiver.

    http://msdn.microsoft.com/en-us/library/ee231604.aspx

    ReplyDelete
  3. thank you Robert. let me try it with the feature receiver.

    ReplyDelete