I've moved my blog to!. Please update your links. This blog is no longer in use--you can find all posts and comments at my new blog; I will no longer be posting to this site and comments have been disabled.

Monday, August 13, 2007

Site Navigation Settings

Setting the navigation is another common post upgrade/deployment task that should be easy to script. One of the challenges in using the commands I created was that it's necessary to know the node ID of the navigational elements. There's probably ways to change this to use the name rather than the ID but you can run into trouble when dealing with items with the same name. To set the navigation using the web interface go to your top level site ->Site Settings->Navigation.

This one took me a while to figure out as the code that Microsoft wrote to handle this was cryptic at best. It was very difficult to wade through some of the oddness. In the end I figured out that you simply had to get a PublishingWeb object and manipulate the GlobalNavigationNodes collection or the CurrentNavigationNodes collection depending on your intentions. In order to add a new node you need to know the existing node IDs so I created an enumerate command to get that information (again there's probably another way to approach this but this worked for my needs). The two commands I created are called gl-enumnavigation and gl-addnavigationnode.

1. gl-enumnavigation

The code for this turned out to be pretty simple. The main part of the code is a recursive routine which goes through all the nodes and their children. I added some indenting to give it more of a tree layout (UPDATE 9/4/2007: I added an xml switch to allow the output to be specified as XML instead of flat text - the results can also be dumped to an output file specified via an outputfile parameter).

   1: public int Run(string command, StringDictionary keyValues, out string output)
   2: {
   3:  ...
   4:  string url = keyValues["url"];
   5:  using (SPSite site = new SPSite(url))
   6:  {
   7:   using (SPWeb web = site.OpenWeb())
   8:   {
   9:    PublishingWeb pubweb = PublishingWeb.GetPublishingWeb(web);
  11:    StringBuilder sb = new StringBuilder();
  12:    sb.Append("Global Navigation:\r\n");
  13:    EnumerateCollection(pubweb.GlobalNavigationNodes, ref sb, 1);
  15:    sb.Append("Current Navigation:\r\n");
  16:    EnumerateCollection(pubweb.CurrentNavigationNodes, ref sb, 1);
  18:    output += sb.ToString();
  19:   }
  20:  }
  21:  ...
  22: }
  23: private static void EnumerateCollection(SPNavigationNodeCollection nodes, ref StringBuilder sb, int level)
  24: {
  25:     if (nodes == null || nodes.Count == 0)
  26:         return;
  28:     string indent = "";
  30:     for (int i = 0; i < level; i++)
  31:     {
  32:         indent += "  ";
  33:     }
  34:     if (level > 0)
  35:         indent += "- ";
  37:     foreach (SPNavigationNode node in nodes)
  38:     {
  39:         sb.AppendFormat("{0}{1}: {2}\r\n", indent, node.Id, node.Title);
  40:         foreach (DictionaryEntry d in node.Properties)
  41:             sb.Append(indent + "   :" + d.Key + "=" + d.Value + "\r\n");
  42:         EnumerateCollection(node.Children, ref sb, level+1);
  43:     }
  44: }

As you can see from the code above, the EnumerateCollection static method does most of the work. It takes in an SPNavigationNodeCollection and iterates through the collection, passing any children back into the same method recursively for processing. The syntax of the command can be seen below:

C:\>stsadm -help gl-enumnavigation

stsadm -o gl-enumnavigation

Returns the site navigation hierarchy.

        -url <site collection>
        [-outputfile <file to output results to>

Here’s an example of how to return the site navigation:

stsadm –o gl-enumnavigation –url "http://intranet/"

The results of running the above command can be seen below (note that I'm also output all the properties associated with each node object which can be useful when trying to determine how the various NodeType enums are assigned:

C:\>stsadm -o gl-enumnavigation -url "http://intranet/

Global Navigation:
  - 2018: Topics
  - 2019: News
  - 2021: Sites
Current Navigation:
  - 2013: Topics
  - 2014: News
  - 2016: Sites

2. gl-addnavigationnode

Figuring out how to add a new node took a little more digging. Turns out that you need to use SPNavigationSiteMapNode.CreateSPNavigationNode() which is a static method that returns back an SPNavigationNode object. You pass into this static method the name, link, type, and nodes collection that the new node belongs to (global or current). Once the node is created you set the remaining properties and call Update(). At this point the node exists as the last item - now you simply move it to where you want (no need to update again):

   1: string url = keyValues["url"];
   2: string linkUrl = keyValues["linkurl"];
   3: string name = keyValues["name"];
   4: string description = keyValues["description"];
   5: NodeTypes type = (NodeTypes)Enum.Parse(typeof(NodeTypes), keyValues["type"]);
   6: bool global = keyValues.ContainsKey("global");
   7: bool current = keyValues.ContainsKey("current");
   8: bool addAsFirst = keyValues.ContainsKey("addasfirst");
   9: bool addAsLast = keyValues.ContainsKey("addaslast");
  10: bool addAfter = keyValues.ContainsKey("addafter");
  11: bool newWindow = keyValues.ContainsKey("newwindow");
  12: string previousNodeID = string.Empty;
  13: if (addAfter)
  14:     previousNodeID = keyValues["addafter"];
  16: using (SPSite site = new SPSite(url))
  17: {
  18:     using (SPWeb web = site.OpenWeb())
  19:     {
  20:         PublishingWeb pubweb = PublishingWeb.GetPublishingWeb(web);
  22:         SPNavigationNodeCollection nodes;
  23:         if (global)
  24:             nodes = pubweb.GlobalNavigationNodes;
  25:         else if (current)
  26:             nodes = pubweb.CurrentNavigationNodes;
  27:         else
  28:             throw new ApplicationException("Unknown error occured.");
  30:         SPNavigationNode node = SPNavigationSiteMapNode.CreateSPNavigationNode(
  31:             name, linkUrl, type, nodes);
  33:         node.Properties["CreatedDate"] = DateTime.Now;
  34:         node.Properties["LastModifiedDate"] = DateTime.Now;
  35:         node.Properties["Description"] = description;
  36:         if (newWindow)
  37:             node.Properties["Target"] = "_blank";
  38:         else
  39:             node.Properties["Target"] = string.Empty;
  41:         node.Update();
  43:         if (addAsFirst)
  44:             node.MoveToFirst(nodes);
  45:         else if (addAsLast)
  46:             node.MoveToLast(nodes);
  47:         else if (addAfter)
  48:         {
  49:             SPNavigationNode previousNode = web.Navigation.GetNodeById(int.Parse(previousNodeID));
  50:             if (previousNode == null)
  51:             {
  52:                 output = "Previous node was not found.";
  53:                 return 0;
  54:             }
  55:             node.Move(nodes, previousNode);
  56:         }
  57:     }
  58: }

The syntax of the command can be seen below:

C:\>stsadm -help gl-addnavigationnode

stsadm -o gl-addnavigationnode

Adds a new node to the site hierarchy.

        -url <site collection url>
        -name <navigation name>
        -linkurl <link url>
        -type <link type: None | Area | Page | List | ListItem | PageLayout | Heading | AuthoredLinkToPage | AuthoredLinkToWeb | AuthoredLinkPlain | AuthoredLink | Default | Custom | All>
        -global / -current
        -addasfirst / -addaslast / -addafter <previous node id>
        [-description <description text>]

Here’s an example of how to add a new node after an existing node:

stsadm –o gl-addnavigationnode–url "http://intranet/" -name "Help Desk" -linkurl "http://helpdesk/" -type AuthoredLinkPlain -global -addafter 2019 -newwindow -description "Corporate Help Desk Application"


Gary Lapointe said...

I've updated the enumnavigation command to allow the results to be returned as XML by passing in "-xml" as a switch. The code has also been refactored to support this. I needed to do this so that I can create another command that I need which would allow me to completely reset the navigation using a passed in xml file (so I'd output the current navigation, manipulate the xml how I want it, and then call this yet to be created command to reset everything).

Anonymous said...

If you try this on a new site collection the method returns null as the navigation is still stored at the site collection level. If you manually move an item the items will then be returned. it's the same problem as repairsitecollectionimportedfromsubsite.

Gary Lapointe said...

There is a way around this - Microsoft only stores the navigation if there has been a change - otherwise they just iterate the collection of web sites and pages and check whether each has been included in navigation. I could do the same so that something is always returned but as I haven't had any requests to do so and it's currently meeting my needs I'll hold off on making that change.

WavyRazr said...

Having the same problem, what method would you need to force SharePoint save the navigation data so that it's accessable? I'm trying to modify a site collection after it's been created from an install script, but the navigation nodes are unavailable using the normal approach.

Gary Lapointe said...

If you know how you want the entire navigation structure to be then you can use the setnavigationnodes command ( that I created to pass in an XML structure. Note that for the sub-sites you can pass in <AutoAddSubSites /> and it will force the adding of all sub-sites to the navigation.

Karnaj said...

Gary would we use this one if we have an "orphaned" collection? So we have a where orphancollection doesn't show up in the main site hierarchy, but is actually a site collection -- as part of the main app -- We have a few of these, and we're trying to "re-attach" them to the main site. Our worry is ensuring they become a child of the parent.

Gary Lapointe said...

A site collection can never be a child of another site collection. You can use this command (or just the browser) to link the site collection via the navigation or you can use the site directory to link to your site collection. The "parent/child" relationship with site collections is merely a logical one and not a physical one (you can only have one root site collection but many other site collections may exist under one or more managed paths but other than potentially sharing a managed path there's no relationship from one site collection to another).

Ivan said...

Can you change the following hard-coded visibility to that of the node's visibility instead.

Under EnumerateCollection method:
// Set the default visibility to true.
xmlChildNode.SetAttribute("IsVisible", "True");
xmlChildNode.SetAttribute("IsVisible", node.IsVisible.toString());

What happens is, if the web is not publishing but is included in the navigation, the code fails to set its visibility correct.

If hope you will consider making this change.

Gary Lapointe said...

Ivan - I've made that change and pushed out a new release. Thanks for the feedback.

Ivan said...

Gary mate,
I checked the source and it is updated but I downloaded and deployed the wsp and it seems that its not updated.

Also, have you thought about writing a command for setting master page for site?
There is a command on msdn blog but its crap. Its simple to write and I anticipate great use of it.

Cheers mate.

Gary Lapointe said...

Ivan - I did another push - hopefully it's there now.

Anonymous said...

Hi Gary

Would it be possible to add an "-addbeneath" option to this command, to allow the creation of new nodes under existing headings?


Anonymous said...

Hi Gary. Please ignore my "-addbeneath" suggestion - I've since discovered that the gl-setlistproperties achieves the same effect for me. Many thanks.

John said...

I tried using this STSADM command, but it is not an option. Was this operation depreciated?

Gary Lapointe said...

They're not in the WSS build - you need the MOSS build of the commands for them to work.

Anonymous said...

Hi gary,

Any support for french in this extention, please? the exportes xml file is not usable in a french install. Thanks.

Czayen said...

Hi Gary,
I am using a batch script to add multiple subsites using "stsadm -o createweb" but this doesn't support options like " Display this site on the top link bar of the parent site?" or "Use the top link bar from the parent site?" available from GUI.

I have tried using your "gl-addnavigationnode" script but it gives me the following error "Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))" Any idea what am I doing wrong?

Kind Regards