Programmatically configuring menu items in SharePoint 2010


Configuring menu items is something that you have to do probably every time you’re scripting your deployment. After having created the site structure and maybe even pages, by default they all appear in the navigation. The odds are high however, that your customer has a different idea about presenting the navigation options, and so configuring menu items becomes something you have to take care of.

Depending on how big the structure of your website is, you might choose to either script it or do it manually. One thing that you have to consider though, is that if you are using nightly builds, someone will have to rearrange the menu items every day if you choose the manual approach. Although the order or visibility of particular menu items doesn’t seem that important at first, it depends a lot on your requirements how important it really is. Depending on your project it may clutter your screen or maybe even break your design, which in both cases is not something people want to see.

Where are my menu items?

Rearranging or hiding menu items using the SharePoint 2010 API is not difficult. To change the order of menu items you can retrieve the menu items using the PublishingWeb.Navigation.GlobalNavigationNodes property. To hide particular menu items, all you have to do is to get the reference to the menu item that you want to hide, and call the PublishingWeb.Navigation.ExcludeFromNavigation method – piece of cake. There is however one problem.

The first time that you try to get the value of the PublishingWeb.Navigation.GlobalNavigationNodes property, the returned collection is empty. Even though the menu on your site properly displays the whole structure on your site, no nodes are being returned in the code!

Screenshot of a site and code. Although menu items are properly displayed on the site, code doesn’t return any menu items

In fact, the collection of menu items remains empty until you change the navigation settings in the Site Actions > Site Settings > Look and Feel > Navigation options.

The Navigation Settings page in SharePoint 2010

What’s happening behind the scenes

The first time you go to the Navigation settings of your site, SharePoint loads all the menu items from the Global Navigation Provider (for the Global Navigation; for the Current Navigation, the Current Navigation Provider is being used). No matter if you change anything in the Navigation Editing and Sorting section or not, as soon as you click the OK button, SharePoint will fill the PublishingWeb.Navigation.GlobalNavigationNodes property will all the nodes as shown in the Navigation Editing and Sorting section of the Navigation Settings page. From that moment on, you can retrieve references to menu items using the API I mentioned before.

When thinking in terms of structured and repeatable deployment, a few challenges become clear. First of all there is no API available to prefill the GlobalNavigationNodes property, so if we want to configure menus as a part of automated deployment, we have to find a way to prefill the navigation information ourselves. However, as scripted deployments are often being executed outside the w3wp.exe process, there is no SharePoint context which is required by the Global Navigation Provider to work.

Getting things done: configuring SharePoint 2010 menu items using PowerShell

You can configure menu items in SharePoint 2010 using the following PowerShell script:

Important: This script is provided as-is and you’re using at your own risk. You should test it in your test environment prior to executing it in the production environment.

function Set-Navigation {
    param (
        $WebUrl,
        $MenuItems
    )
    
    $site = New-Object Microsoft.SharePoint.SPSite($WebUrl)
    $web = $site.OpenWeb()
    
    # fake context
    [System.Web.HttpRequest] $request = New-Object System.Web.HttpRequest("", $web.Url, "")
    $sw = New-Object System.IO.StringWriter
    $hr = New-Object System.Web.HttpResponse($sw)
    [System.Web.HttpContext]::Current = New-Object System.Web.HttpContext($request, $hr)
    [Microsoft.SharePoint.WebControls.SPControl]::SetContextWeb([System.Web.HttpContext]::Current, $web)
    
    # initalize what has to be initialized
    $pweb = [Microsoft.SharePoint.Publishing.PublishingWeb]::GetPublishingWeb($web)
    $dictionary = New-Object "System.Collections.Generic.Dictionary``2[[System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[Microsoft.SharePoint.Navigation.SPNavigationNode, Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c]]"
    $collection = $pweb.Navigation.GlobalNavigationNodes
    
    # get current nodes
    $globalNavSettings = New-Object System.Configuration.ProviderSettings("GlobalNavSiteMapProvider", "Microsoft.SharePoint.Publishing.Navigation.PortalSiteMapProvider, Microsoft.SharePoint.Publishing, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c")
    $globalNavSettings.Parameters["NavigationType"] = "Global"
    $globalNavSettings.Parameters["EncodeOutput"] = "true"
    [Microsoft.SharePoint.Publishing.Navigation.PortalSiteMapProvider] $globalNavSiteMapProvider = [System.Web.Configuration.ProvidersHelper]::InstantiateProvider($globalNavSettings, [type]"Microsoft.SharePoint.Publishing.Navigation.PortalSiteMapProvider")
    [Microsoft.SharePoint.Publishing.Navigation.PortalSiteMapNode] $currentNode = $globalNavSiteMapProvider.CurrentNode
    $children = $currentNode.GetNavigationChildren([Microsoft.SharePoint.Publishing.NodeTypes]::Default, [Microsoft.SharePoint.Publishing.NodeTypes]::Default, [Microsoft.SharePoint.Publishing.OrderingMethod]::Manual, [Microsoft.SharePoint.Publishing.AutomaticSortingMethod]::Title, $true, -1);
    
    # reorder nodes
    [Array]::Reverse($menuItems)
    $menuNodes = New-Object System.Collections.ObjectModel.Collection[Microsoft.SharePoint.Publishing.Navigation.PortalSiteMapNode]
    foreach ($node in $children) {
        $menuNodes.Add($node)
    }
    
    foreach ($menuItem in $menuItems) {
        $node = $null
        foreach ($p in $menuNodes) {
            if ($p.InternalUrl -eq $menuItem) {
                $node = $p
                break
            }
        }
        
        if ($node -ne $null) {
            [void] $menuNodes.Remove($node)
            [Void] $menuNodes.Insert(0, $node)
        }
    }
    
    foreach ($node in $menuNodes) {
        Write-Host "$($node.InternalUrl)..." -NoNewline
        $quickId = Get-QuickId $node
        if ($quickId -ne $null) {
            [string]$typeId = $null;
            if (($node.Type -eq [Microsoft.SharePoint.Publishing.NodeTypes]::Area) -or ($node.Type -eq [Microsoft.SharePoint.Publishing.NodeTypes]::Page)) {
                if ($node.PortalProvider.NavigationType -eq [Microsoft.SharePoint.Publishing.Navigation.PortalNavigationType]::Current) {
                    $typeId = [Microsoft.SharePoint.Publishing.Navigation.PortalNavigationType]::Current.ToString() + "_" + $node.Type.ToString()
                }
                else {
                    $typeId = [Microsoft.SharePoint.Publishing.Navigation.PortalNavigationType]::Global.ToString() + "_" + $node.Type.ToString()
                }
            }
            else {
                $typeId = $node.Type.ToString();
            }

            $id = $quickId.Split(',');
            $objId = New-Object Guid($id[0]);
            $nodeId = [System.Int32]::Parse($id[1]);

            $navigationNode = Get-NavigationNode $objId $nodeId $node.InternalTitle $node.InternalUrl $node.Description $node.Type $node.Target $node.Audience $collection $dictionary
            $containsNode = $false
            foreach ($mi in $menuItems) {
                if ($mi -eq $node.InternalUrl) {
                    $containsNode = $true
                    break
                }
            }
            
            if ($containsNode) {
                $pweb.Navigation.IncludeInNavigation($true, $objId)
            }
            else {
                $pweb.Navigation.ExcludeFromNavigation($true, $objId)
            }
        }
        Write-Host "DONE"
    }

    $pweb.Web.Update()

    [System.Web.HttpContext]::Current = $null
}

function Get-QuickId {
    param (
        [Microsoft.SharePoint.Publishing.Navigation.PortalSiteMapNode] $node
    )
    
    $quickId = $null
    
    $portalSiteMapNodeType = $node.GetType()
    $QuickId = $portalSiteMapNodeType.GetProperty("QuickId", [System.Reflection.BindingFlags] "Instance, NonPublic")
    $quickId = [string] $QuickId.GetValue($node, $null)
    
    $quickId
}

function Get-NavigationNode {
    param (
        [Guid] $objId,
        [int] $nodeId,
        [string] $name,
        [string] $url,
        [string] $description,
        [Microsoft.SharePoint.Publishing.NodeTypes] $nodeType,
        [string] $target,
        [string] $audience,
        [Microsoft.SharePoint.Navigation.SPNavigationNodeCollection] $collection,
        $oldDictionary
    )
    
    [Microsoft.SharePoint.Navigation.SPNavigationNode] $node = $null
    if (($objId -ne [Guid]::Empty) -and ($nodeId -ge 0)) {
        if ($oldDictionary.TryGetValue($nodeId, [ref]$node)) {
            $oldDictionary.Remove($nodeId)
            $node = [Microsoft.SharePoint.Publishing.Navigation.SPNavigationSiteMapNode]::UpdateSPNavigationNode($node.Navigation.GetNodeById($node.Id), $null, $name, $url, $description, $target, $audience, $false)
            $node.MoveToLast($collection)
        }
        
        return $node
    }
    
    $node = [Microsoft.SharePoint.Publishing.Navigation.SPNavigationSiteMapNode]::CreateSPNavigationNode($name, $url, $nodeType, $collection)
    return [Microsoft.SharePoint.Publishing.Navigation.SPNavigationSiteMapNode]::UpdateSPNavigationNode($node, $null, $name, $node.Url, $description, $target, $audience, $false)
}

Write-Host "Configuring Navigation..."
$menuItems = "/site5", "/site4", "/site3", "/site2", "/site1"
Set-Navigation "http://publishing" $menuItems
Write-Host "Navigation Configuration completed"

The script consists of a number of functions which are being called at the very end (line 142). The main function called Set-Navigation accepts two parameters: the URL of the Web for which you want to configure the menu items and an array of URLs as should be seen in the menu. This array should:

  • contain all items that you want to see in the menu. All other items will be hidden.
  • contain items ordered just as you want them to appear in the menu.
  • refer to menu items as they are referred to by SharePoint. You can verify that by navigating to the Site Actions > Site Settings > Look and Feel > Navigation page, in the Navigation Editing and Sorting section selecting the particular menu item and checking its URL property:
Verifying the URL property of a menu item in the Navigation Settings page

As mentioned before, in order to prefill the navigation information we have to have access to the Global Navigation Provider. For this we need to create context information which the provider requires. So the first thing that the Set-Navigation function does, is to create context information (lines 11-15). The important thing here is that the SPWeb object passed to the SetContextWeb method (line 15) must be created using the SPSite.OpenWeb() method. If you use the Get-SPWeb cmdlet instead, the script won’t work – even if you cast the created object to Microsoft.SharePoint.SPWeb.

The next step is to initialize some objects that we will need later in the script (lines 18-20). Once we have them in place, we can start the process of prefilling the navigation information.

The first thing that we have to do is to retrieve the information about navigation nodes for the given web. That information is stored within the Global Navigation Provider so we need to instantiate it first (lines 23-26). Once we have a reference to the navigation provider we can retrieve the  current navigation node (line 27) which we then can use to get the information about all menu items available for the given web (line 28).

Before we proceed with storing the information about menu items in the web, we have to reorder the items in our collection so that they are stored in the right order (lines 31-50). With that we’re ready to prefill the GlobalNavigationNodes collection with our menu items.

First we need to obtain the ID of every navigation node (line 54). This is the only questionable part of that script. Since the ID of the navigation node isn’t available as a part of the public API it can be retrieved only using reflection (the Get-QuickId function in lines 97-109). With that we can prepare all the information required to create a navigation node object that we can add to the GlobalNavigationNodes collection (lines 56-71).

After getting a reference to the navigation node object (line 73) we can proceed with the last part of the configuration which is hiding menu items other than those specified in the array (lines 74-87).

The very last thing that we have to do is to persist changes by calling the SPWeb.Update() method (line 92) and to clean the context information (line 94).

After you execute this script, you should see your site showing menu items as specified in the array:

SharePoint 2010 Publishing Site showing menu items as specified in the array

And if you navigate to the Navigation Settings page you should see all other menu items hidden.

The Navigation Settings page with menu items hidden

Summary

Using structured and repeatable deployment with SharePoint solutions allows you to increase predictability of your solutions and saves you time required for configuring the solution after automated builds or after deploying to other environments. With the SharePoint API you can deploy your solutions preconfigured what makes it easier and more reliable to test. Although the SharePoint API has some limitations, the majority of the most frequently used components including configuring menu items can be configured in an automated fashion.

Others found also helpful: