Programmatically configuring menu items in SharePoint 2010 revisited

Exception displayed in a PowerShell window
A while ago I wrote about how you can programmatically configure menu items in SharePoint 2010 using PowerShell. Unfortunately it turns out that it doesn’t always the way you would expect it to. Find out how to get the script working in your SharePoint environment.

Being able to programmatically configure menu items is an quite important part of structured and repeatable deployment. Particularly if you’re building and deploying frequently, configuring navigation can save you quite some time and make it easier for you to run automated tests.

Back in June I shared with you a PowerShell script that allowed you to automatically configure navigation on a Publishing Site built on the SharePoint 2010 platform. While that script worked on my development machine, at some point I started receiving some error reports about it failing in other environments. Every time you would execute the script you would get the following error:

Exception calling "GetNavigationChildren" with "6" argument(s): "Object reference not set to an instance of an object."

An exception displayed in a PowerShell window

As you might have expected, the navigation would remain in its original state.

What is wrong and how to fix it

It turns out that while working with Navigation SharePoint also requests Audiences information. This is not that surprising considering that SharePoint allows you to target navigation item to audiences.

The exception I have just showed you is being thrown because the account that you are using to run the PowerShell script is not allowed to access Audiences information from the User Profile Service Application.

To solve this issue, you have to grant your account permission to access the User Profile Service Application. You can do this by navigating to Central Administration and from the Application Management group clicking the Manage service applications link.

The ‘Manage service applications’ link highlighted in Central Administration

Next, from the list of available Service Applications, select the User Profile Service Application and in the Ribbon, from the Sharing group, click the Permissions button.

03

Add the account that you are using to execute the navigation configuration PowerShell script (1) and give it Full Control permissions (2). Confirm your changes by clicking the OK button (3).

Granting permissions to access the User Profile Service Application to the administrator account

If you execute the script now, you should see it working as expected.

Navigation configuration PowerShell script successfully completed

One more thing

Once in a while the navigation configuration script throws another exception when retrieving navigation nodes. This happens when the Application Pool haven’t been warmed up and the information about navigation nodes has not been made available yet. To avoid this error I have extended the script with a call to the home page of the site, to start the Application Pool and ensure that all the information is in place before we start working with it.

Write-Host "Warming up site $($WebUrl)..." -NoNewline  
$wc = New-Object System.Net.WebClient
$wc.UseDefaultCredentials = $true
$wc.DownloadString($WebUrl) | Out-Null
Write-Host "DONE" -ForegroundColor Green  

The updated version of the script looks as follows:

param (  
    $SiteUrl = "http://publishing"
)

function Set-Navigation {  
    param (
        $WebUrl,
        $MenuItems
    )

    Write-Host "Warming up site $($WebUrl)..." -NoNewline
    $wc = New-Object System.Net.WebClient
    $wc.UseDefaultCredentials = $true
    $wc.DownloadString($WebUrl) | Out-Null
    Write-Host "DONE" -ForegroundColor Green

    $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 -and $quickId.Length -gt 0) {
            [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" -ForegroundColor Green
        }
        else {
            Write-Host "SKIPPED" -ForegroundColor Yellow
        }
    }

    $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 = "/site1", "/site2", "/site3", "/site4", "/site5", "/site5"
Set-Navigation $SiteUrl $menuItems  
Write-Host "Navigation Configuration completed" -ForegroundColor Green  

Summary

Configuring navigation as a part of structured and repeatable deployment can save you time and help you automate the testing process. In order to configure navigation on Publishing Sites in SharePoint 2010 the account that executes the configuration script must be granted access to the User Profile Service Application which contains information about Audiences which is used by SharePoint 2010 to target navigation items to specific groups of users.

Comments

comments powered by Disqus