Multi-Level Top Link Bar Navigation (Sub Menus)

Applies To: SharePoint 2010

The Top Link Bar (sometimes called the Tab Bar) allows you to include a mix of links and headers (with child links). By default, this results in a single sub menu and handles most situations well.

IMG_0347

However, the need to have multiple levels (additional sub menus) often comes up. There are several solutions out there that suggest replacing the Top Navigation Menu control altogether using javascript or CSS menus. These work but are also totally unnecessary. The Top Link Bar uses a standard ASP.NET menu control and comes with several attributes that can be customized.

In the v4.master (SharePoint 2010) the section we’re interested in can be found inside the PlaceHolderTopNavBar ContentPlaceHolder (Specifically the SharePoint:AspMenu control TopNavigationMenuV4):

					<asp:ContentPlaceHolder id="PlaceHolderTopNavBar" runat="server">
					<h2 class="ms-hidden">
					<SharePoint:EncodedLiteral runat="server" text="<%$Resources:wss,topnav_pagetitle%>" EncodeMethod="HtmlEncode"/></h2>
							<asp:ContentPlaceHolder id="PlaceHolderHorizontalNav" runat="server">
<SharePoint:AspMenu
  ID="TopNavigationMenuV4"
  Runat="server"
  EnableViewState="false"
  DataSourceID="topSiteMap"
  AccessKey="<%$Resources:wss,navigation_accesskey%>"
  UseSimpleRendering="true"
  UseSeparateCss="false"
  Orientation="Horizontal"
  StaticDisplayLevels="2"
  MaximumDynamicDisplayLevels="1"
  SkipLinkText=""
  CssClass="s4-tn"/>
<SharePoint:DelegateControl runat="server" ControlId="TopNavigationDataSource" Id="topNavigationDelegate">
	<Template_Controls>
		<asp:SiteMapDataSource
		  ShowStartingNode="False"
		  SiteMapProvider="SPNavigationProvider"
		  id="topSiteMap"
		  runat="server"
		  StartingNodeUrl="sid:1002"/>
	</Template_Controls>
</SharePoint:DelegateControl>
							</asp:ContentPlaceHolder>
					</asp:ContentPlaceHolder>

The key attribute can be found on line 355 above. The MaximumDynamicDisplayLevels defaults to 1. Setting this to 2 or higher will control how many levels of sub menus you want to allow. Once you’ve made this change to your Master Page (saved, checked-in, and approved), SharePoint will be able to display multiple sub menus!

Unfortunately, the navigation settings found in Site Actions > Site Settings > Navigation (under Look and Feel) only allow top level Header entries and won’t display or allow you to edit navigation nodes at levels greater than 2:

NavigationSettingsArrgg

So even though we made the correct change to the control we have no native way to edit these sub menus. Fortunately this can be done with PowerShell (Or just plain .NET) which will be the subject of the next post. However, this is a necessary first step towards Multi-Level Top Bar Links.

SharePoint Top Link Bar Synchronization

Applies To: SharePoint

The Top Link Bar (sometimes called the Tab bar) provides global navigation for a site collection. It’s a great spot to link to subsites, other site collections and more. It’s also relatively easy to use and customize. On a standard team site you can go to Site Actions > Site Settings and then choose the Navigation link under Look and Feel to customize. This all works really well. However, it can be difficult to maintain consistent navigation between multiple site collections. Add in the complication of relative URLs, audiences and attempting to keep those sites in sync with any more than 2 or 3 site collections – suddenly any navigational changes become a full project!

IMG_0347

Fortunately, using the below PowerShell script you can easily setup navigation on a single site collection using the out of the box tools and then propagate those changes to as many additional site collections as needed:

The Script

You can copy the script below and paste it into notepad and save it as NavigationPropagation.ps1

$sourceweb = "http://somesite.com"
$destwebs = "http://somesite.com/sites/humanresources", "http://somesite.com/panda", "http://someothersite.com/pickles"

$keepAudiences = $true #set to false to have it totally ignore audiences (good for testing)

Function CreateNode($node,$dWeb,$destCollection){
    if ($node.Properties.NodeType -ne [Microsoft.SharePoint.Publishing.NodeTypes]::Area -and $node.Properties.NodeType -ne [Microsoft.SharePoint.Publishing.NodeTypes]::Page) {

        Write-Host "    Node: " + $node.Title

        #make URLs relative to destination
    	$hnUrl = $node.Url
    	#Write-Host "O URL: $hnUrl"
        $IsHeading = $false
        if($node.Properties.NodeType -eq [Microsoft.SharePoint.Publishing.NodeTypes]::Heading){
            $IsHeading = $true
        }
    	$hnUrl = SwapUrl $hnUrl $sourceWeb $dWeb $IsHeading
    	#Write-Host "N URL: $hnUrl"

        $hnType = $node.Properties.NodeType
        if($hnType -eq $null){
            $hnType = "None"
        }

        $hNode = [Microsoft.SharePoint.Publishing.Navigation.SPNavigationSiteMapNode]::CreateSPNavigationNode($node.Title,$hnUrl,[Microsoft.SharePoint.Publishing.NodeTypes]$hnType, $destCollection)
    	$hNode.Properties.Description = $node.Properties.Description
        if($keepAudiences){
    	   $hNode.Properties.Audience = $node.Properties.Audience
        }
    	$hNode.Properties.Target = $node.Properties.Target
        $hNode.Update()

        if($node.Children.Count -gt 0) {
    	   foreach($child in $node.Children) {
                CreateNode $child $dWeb $hNode.Children
    	   }
        }
    }
}

Function SwapUrl([string]$currentUrl,[string]$sourceRoot,[string]$destRoot,[boolean]$isHeading) {
	if ($currentUrl -ne "/") {
		if ($currentUrl.StartsWith("/")) {
			$currentUrl = $sourceRoot + $currentUrl
		} elseif ($currentUrl.StartsWith($destRoot)) {
			$currentUrl = $currentUrl.Replace($destRoot, "")
		}
	} else {
		$currentUrl = [System.String]::Empty
        if(!$isHeading){
            Write-Host "        This is a blank non-heading! Using $sourceRoot" -ForegroundColor Yellow
            $currentUrl = $sourceRoot
        }
	}
	$currentUrl
}

$sw = Get-SPWeb $sourceweb
$psw = [Microsoft.SharePoint.Publishing.PublishingWeb]::GetPublishingWeb($sw)

foreach ($destweb in $destwebs) {

	Write-Host "***************************************" -ForegroundColor Cyan
	Write-Host "Propogatin' That There $destweb" -ForegroundColor Cyan
	Write-Host "***************************************" -ForegroundColor Cyan

	$dw = Get-SPWeb $destweb
	$pdw = [Microsoft.SharePoint.Publishing.PublishingWeb]::GetPublishingWeb($dw)

	$nodeCount = $pdw.Navigation.GlobalNavigationNodes.Count
	Write-Host "  Removing Current Nodes ($nodeCount)..." -ForegroundColor DarkYellow
	for ($i=$nodeCount-1; $i -ge 0; $i--) {
		$pdw.Navigation.GlobalNavigationNodes[$i].Delete()
	}

	Write-Host "  Propagating to $destweb..." -ForegroundColor DarkYellow
	foreach($n in $psw.Navigation.GlobalNavigationNodes){
		[void](CreateNode $n $destweb $pdw.Navigation.GlobalNavigationNodes)
	}

	$dw.dispose()
}

$sw.dispose()

What it Does and How to Use it

Since this is a script that you will typically run with the same parameters over and over I just made them variables at the top of the script. There are just 3 of them:

  • $sourceweb: This is the url of the site collection to copy the navigation from
  • $destwebs: This is a comma separated (array) list of urls to copy the navigation to
  • $keepAudiences: When $true audiences are propagated along with the links, when $false then these are totally ignored. Setting this to $false can be really helpful for testing just to make sure everything is being copied correctly before you add in the complication of audience filtering

Once you set those and save, open the SharePoint Management Shell (You’ll want to run-as a farm administrator) and navigate to the location of the script you can simply run it by typing: .\NavigationPropagation.ps1

IMG_0349

The script will cycle through each of the $destwebs and delete all the Global Navigation Nodes from the site. It will then copy all the nodes (excluding Area and Page types since these are automatic depending on your settings) from the $sourceweb. Each URL is made relative or adjusted to be fully qualified as needed. This is done recursively allowing any number of levels of child nodes (more on this in an upcoming post).

Typical use case for us is to save the script with all the parameters set and whenever we make a change on the $sourceweb we simply run the script and everything gets synchronized. If you’re making frequent updates then you could always use Task Scheduler to run the script automatically.

How it Works

There are 2 functions in the script: CreateNode (lines 4-40) and SwapUrl (lines 42-57). CreateNode copies a passed node into the specified collection mapping the necessary properties and calling SwapUrl which ensures relative Urls are fully qualified and Urls that should be relative are made relative. The main thing to note is that CreateNode is recursive and will copy all child nodes as well.

The main execution of the script starts at line 59 and is pretty straightforward. It retrieves the source web and then loops through the destination webs. For each destination web it deletes all the existing navigation nodes and then calls CreateNode passing each of the source web’s nodes and the destinations node collection.

 

Keeping your navigation consistent across the farm is essential to a good user experience but can be difficult to manage using out of the box tools. Fortunately with the above script this can be relatively simple.

jQuery OuterHTML

Applies To: jQuery 1.0+

Often in debugging I’d like to pull back the full HTML for an object and take a look at it. The html() method is great for a lot of things, but it’s limited to just the inner HTML and often doesn’t contain the stuff I’m looking for. So here’s a helper function I wrote that gives you back the full HTML as a string:

function fullHTML(jQueryElem) {
	var fullH = jQueryElem.wrap('<div>').parent().html();
	jQueryElem.unwrap();
	return fullH;
}

All it does is perform a temporary wrap around whatever jQuery Element you pass in, grab it’s parent (which is now the Div we just wrapped it in) and grab the parent’s inner HTML (which is of course the full HTML of our object). Then we just unwrap it in line 3 and return the string in line 4.

You can call it like this (this code just writes it out to the console):

console.log(fullHTML($('#myobjectID')));

It’s quick and easy and I use it in development all the time. Hopefully it helps you too.

Use SPServices in Nintex Forms 2010

Applies to: SharePoint 2010, SPServices, Nintex Forms 2010

Marc D Anderson’s SPServices (jQuery for SharePoint Web Services) can save you a ton of time and make your SharePoint sites more responsive and dynamic with very little effort. Nintex can really beef up your workflows and/or your forms. So it makes a lot of sense to bring these together!

Recently I was tasked with pulling some user profile information to display on a Nintex Form that was being used to launch a site workflow. Unfortunately, there is no way to run workflow code before showing the initial form; so all of those great Nintex Workflow Actions were unavailable to me. Fortunately, SPServices allows easy querying of the User Profile Service directly from client side script.

Nintex Forms uses a copy of the jQuery library that you can access through NWF$. So just adding SPServices to your form’s page won’t work, but since jQuery is already there it’s fairly simple to get it all hooked up.

The first thing to do is to get the appropriate version of SPServices. The version of SPServices you use depends on the version of jQuery in use. To figure out what version of jQuery Nintex Forms is using you’ll need to temporarily add some custom JavaScript to your form.

To add JavaScript to Nintex Forms, from the Form editor click the Settings button in the Ribbon and then expand the Custom JavaScript section at the bottom:

FormJavascript

You can take advantage of NWF$ here. Simply add the following to the box and click Save:

console.log(NWF$().jquery);

Open the F12 Developer tools and preview your form. In the Console you should see the version of jQuery being used by Nintex Forms. For us it was 1.6.1. So we should be fine using the latest version of SPServices.

You’ll need to download the SPServices library. Since SPServices uses an IIFE to extend the jQuery object, we’ll need to make a slight adjustment to instead extend the NWF$ object. This is actually really easy. Open up the minified version of SPServices (the one with the .min at the end of the file name) and go all the way to the very end where you’ll see (jQuery);

Just replace the jQuery portion with NWF$ and save with a different name (maybe put an NF.min.js on the end). It’ll look something like this:

SPServicesCode

Then just upload it somewhere in your site collection (The Style Library is generally the best place).

To reference that file go back to the Nintex Form editor and click that Settings button again and expand the Advanced section. Scroll down to the Custom JavaScript Includes section and add the address of the script you just uploaded (If you have multiple JavaScript Includes you just put one on each line):

SPServicesInclude

Press Save and that’s it! You can now access SPServices to do whatever fancy magic you need!

To make sure it worked, scroll back down to that Custom JavaScript section of the Form Settings dialog and add the following code:

NWF$(document).ready(function(){
     console.log(NWF$().SPServices.Version());
});

Then just make sure those F12 developer tools are open and preview the form again. You’ll see the version of SPServices logged out to the console.

Changing Web Part Properties When the Page is Unavailable

Applies To: SharePoint

The other day we made some changes that caused some issues with how one of our web parts was configured. Unfortunately, I hadn’t wrapped the problem in a try/catch and my error blew up the whole page. I’m sure I’m the only one that’s ever done that. So obviously I’ve got some code changes to make, but what do I do in the meantime? Fortunately, there’s some straight forward Powershell that lets you change web part settings (even custom properties like mine).

I found the solution to this over on Aarebrot.net where he was using the technique to change a web part that automatically redirected the user. I’ve just reproduced his code here and added some explanation and background.

When I first went to solve this problem I tried the ?contents=1 querystring trick to pull up the Web Part Administration page. If you’re looking for a quick solution you can add that query string to the end of your page’s URL and then delete the web part from the page and start over. But a more elegant solution is to just change the offending property using some easy Powershell.

Using the SharePoint 2010 Management Shell, run the following commands:

$web = Get-SPWeb "http://somedomain.com/sites/someweb"
$page = $web.GetFile("default.aspx")
$page.CheckOut()
$wpm = $web.GetLimitedWebPartManager("default.aspx",[System.Web.UI.WebControls.WebParts.PersonalizationScope]::Shared)
$part = $wpm.WebParts[0]
$part.SomeProperty = "The correct setting!"
$wpm.SaveChanges($part)
$page.CheckIn("Fixed that property")
$page.Publish("Fixed that property")
$web.Close()

What Just Happened?

In line 1 we’re just getting a reference to the web site (SPWeb) where your web part lives using Get-SPWeb. Just replace the URL shown with yours.

Lines 2-3 and 8-9 are only required if the page you’re modifying is on a publishing site or check in/out is required. Feel free to skip these (go directly to line 4) if you’re just editing a simple page. If your page does require check out to be edited, line 2 is simply retrieving the file (SPFile) using the GetFile method using the relative location of the page. Then line 3 calls the CheckOut method which, of course, checks out the file.

In line 4, we’re grabbing a reference to the Web Part Manager for the page (SPLimitedWebPartManager) using the GetLimitedWebPartManager method. Just replace the first parameter with the relative location of your page. The second parameter is the PersonalizationScope enumeration and can be User or Shared. You’re going to want to use Shared to affect everybody. The Web Part Manager object is what lets us get access to all the web parts on the page and screw with em.

In line 5, we grab the web part (WebPart) we want by index using the WebParts collection. In the example above I already knew that the web part I wanted was the first one in the collection. You can also pass the uniqueID property of the web part (instead of the index). You can find out both by simply calling the WebParts collection by itself ($web.WebParts) and everything will get listed to the screen.

To see all the available properties of the web part you can just type ($part) and it will list everything out including any custom properties. Then you can just set them like we do in line 6.

Line 7 uses the Web Part Manager’s SaveChanges method to incorporate all your changes. Lines 8-9 are again only required if your pages library requires check in/out and publishing. If it’s a simple page just skip to line 10. Line 8 uses the CheckIn method which takes a string for a check-in comment. Line 9 uses the Publish method which also takes a string for a comment.

Line 11 just calls the Close method and ensures we clean up all our resources.

That’s it! Now you can wrap that up in a script to loop through multiple pages and change properties on all sorts of web parts or just one-off fix those web parts you might have broken.

Unpublished View

Applies To: SharePoint

Versioning is a great feature in SharePoint but can cause administrators headaches when it comes to something like branding or other resource files. I’ve previously shown you how to automatically publish a major version and to approve documents as your solution deploys. In a perfect world you’ll never touch these files except through your solution.

However, sometimes you’ll need to quickly fix something or tweak something and you end up in a common situation: You can see the change but no one else can. This happens when versioning is turned on and you either haven’t published a major version (versions below 1) or the changes you’ve made are in draft state (ie 1.1 instead of 2.0).

Tracking these down can be frustrating since everything works for those with the right permissions. So here’s a quick view to add to your library to help you identify these problems. We’ll be filtering the library on 2 columns: Approval Status and Checked Out To.

First thing to do is to create a new view (I’ve named mine Unpublished). Choose whatever columns you want but some helpful ones are Type, Name, Modified, Modified By, Checked Out To and Version. You’ll also want to scroll down to the Folders settings and choose Show all items without folders.

You’ll notice that your view is now showing everything in a giant list because we didn’t apply any kind of filter. The reason for this is that the browser based view editor won’t let us select either IsNotNull as our comparison type or the Approval Status column. Fortunately, both of these things can be done very easily in SharePoint Designer. So switch to the Library ribbon and choose Modify in SharePoint Designer (Advanced) from the Modify View dropdown.

In the Code view just paste the following into your View’s XML inside the Query element:

<Where>
	<Or>
		<Eq>
			<FieldRef Name="_ModerationStatus"/>
			<Value Type="ModStat">Draft</Value>
		</Eq>
		<IsNotNull>
			<FieldRef Name="CheckoutUser"/>
		</IsNotNull>
	</Or>
</Where>

Things should look similar to this:

ViewCAML

What our view is doing is showing any files that are checked out (so the CheckoutUser column is not blank) or that don’t have a major version. For whatever reason Microsoft won’t give you easy access to the _ModerationStatus column unless your library is requiring approvals, but we can still use it. When it’s set to Draft then it’s a minor version.

Save and refresh in the browser and you should only see those documents that may be causing you problems. Check them in or Publish a Major Version as needed and things should be good. We’ve found this simple view to be very helpful, hopefully you do as well.