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.

Refinement Panel Customization Saving Woes

Applies To: SharePoint 2010

Editing the Filter Category Definition of a Refinement Panel web part can make your search result pages so much better. This is one of the first things we customize and every time we do, I get tripped up by a really annoying setting in the web part.

Problem Scenario

I replace the XML in the Filter Category Definition property in the Refinement properties section of the web part. I hit Apply and everything validates. I save the page and run the results – No custom refinements! In fact, when I open the web part back up for editing, my custom Filter Category Definition is totally wiped out! Why isn’t it saving!?!?!?! Why am I weeping ever so softly?!?!! Why would I confess that on the internets?!?!?!?!

Solution

There’s a checkbox at the bottom of the Refinement properties section labeled, Use Default Configuration. This is checked by default. Unless you uncheck this when you place your custom XML in the property, it is going to completely ignore you and replace it with the default XML.

RefinementSettings

I can see why things work this way, but it is extremely unintuitive. It would seem that the web part should recognize there is custom XML in the Filter Category Definition and understand that’s what it should use. Then a button (NOT a checkbox) that says something along the lines of “Revert to Default Configuration” would be used to reset that XML when needed.

Oh well, nobody is asking my opinion anyway. So do yourself a favor and remember to uncheck that box whenever customizing the Filter Category Definition.

Broken Add New Document Links

Applies To: SharePoint 2010

We ran into an interesting problem the other day where a user called and said that everytime they clicked the Add New Document link they got a giant error message. I went to the site and was able to confirm that it was indeed blowing up.

This was only happening when the document library was being exposed through a web part on another page. Going directly to the library and clicking New Document or Upload Document in the ribbon worked just fine. The problem was obviously with the web part toolbar, which in this case was set to Summary Toolbar.

It appears this is something left over from our upgrade from SharePoint 2007 to SharePoint 2010. I would’ve thought the Visual Upgrade would have fixed this and it’s amazing no one noticed for well over a year! It seems that Microsoft changed not only the look of the link (switching the icon from a little square to a green plus and removing the word new) the underlying address also changed.

AddNewDocumentLink
Broken Old Style Link
NewDocumentLink
Correct Style Link

Taking a look at where these links were pointed, I could see that the old style link was pointed at listform.aspx:

/_Layouts/listform.aspx?PageType=8&ListId={LISTGUID}?RootFolder=

While the new style link was pointed at Upload.aspx:

/_layouts/Upload.aspx?List=LISTGUID&RootFolder

Fixing this is pretty easy. Just edit the web part and change the Toolbar Type to No Toolbar and click Apply:

ToolBarTypeNone

Then immediately switch the Toolbar Type back to Summary Toolbar and press OK:

ToolBarTypeSummary

Interestingly, other web parts sometimes show the old style link too (links, announcements, etc.) but since these all seem to still use the listform.aspx they continue to work. However, you can use the above technique to get them to match visually as well (green plus icon).

Dates With Relative Countdowns and Pretty Colors in List Views

Applies To: SharePoint 2010

Every list in SharePoint automatically comes with two date columns (Created & Modified). Often times other date columns get added in there and it can be nice to format these to make the lists more intuitive. Out of the box you can format these a few different ways (mostly whether to show the time or not). With a little XSL you can use the ddwrt:formatdatetime function to really customize things (you’ll see a couple of examples of this below) – but can’t we do more?

Large lists of data (whether they are numbers, statuses, dates, etc.) can be overwhelming. One of the greatest improvements we can make is to provide visual clues or analysis on this data to help end-users understand what they are looking at. When it comes to dates, what most people really want to know is what that date means relative to now. This is especially true when it comes to Due Dates such as seen in a Tasks list:

TaskList

We’ve greatly improved the readability of this list with some quick icons as demonstrated in my previous post: Showing Icons in a List View, but those due dates don’t really mean much at quick glance. We can do a couple of things to make these instantly understandable. We can add some color to indicate when the due date is near and/or missed. Even more powerful is showing how much time is left until the Due Date.

Adding Some Color

Flagging these dates with some quick color is pretty straightforward using SharePoint Designer. Designer will be generating some XSL and we’ll take a look at it at the end of this section, but we’ll be using the wizards and so no knowledge of XSL will be needed.

Open the site in SharePoint Designer (Site Actions > Edit in SharePoint Designer) and browse to the page/view you want to edit, or if this is a specific view just choose the Modify View dropdown and select Modify in SharePoint Designer (Advanced):

ModifyView

In Design view, click on one of the date values in the list and choose Format Column in the Conditional Formatting dropdown on the Options tab in the ribbon:

FormatDate

Our goal is to turn the cell red if the due date has passed; So in the Condition Criteria dialog set Field Name to Due Date, the Comparison to Less Than and the Value should remain the default of [Current Date]. Then press the Set Style button:

ConditionCriteriaPastDue

This formula reads: if the Due Date is older than (less) than now, set this style. We’re now setting up that style in the Modify Style dialog. In the Font category set the font-weight to bold (I’m also setting the color to Black since the default theme’s grayish font color doesn’t look great with a red background). Switch to the Background category and select a shade of red for the background-color and press OK:

PastDueStyle

I also like to provide a little warning before things get past due; So let’s make things turn yellow on the Due Date. So again, click on one of the date values in the list and choose Format Column in the Conditional Formatting dropdown on the Options tab in the ribbon. In the Condition Criteria dialog set Field Name to Due Date, the Comparison to Equal and the Value should remain the default of [Current Date]. Then press the Set Style button:

ConditionCriteriaWarning

This formula reads: if the Due Date is today, set this style. We’re now setting up that style in the Modify Style dialog. In the Font category set the font-weight to bold (I’m also setting the color to Black since the default theme’s grayish font color doesn’t look great with the yellow background either). Switch to the Background category and select a shade of yellow for the background-color and press OK:

WarningStyle

Save the view in Designer and refresh the view in the browser and you should see something similar to this (The date this screenshot was taken was 2/7/2013):

TaskListWithColors

For those that are interested, here’s the XSL that designer generated:

<xsl:attribute name="style">
	<xsl:if test="ddwrt:DateTimeTick(ddwrt:GenDisplayName(string($thisNode/@DueDate))) &lt; ddwrt:DateTimeTick(ddwrt:GenDisplayName(string($Today)))"
	 ddwrt:cf_explicit="1">
		font-weight: bold; background-color: #DF1515; color: #000000;
	</xsl:if>
	<xsl:if test="ddwrt:DateTimeTick(ddwrt:GenDisplayName(string($thisNode/@DueDate))) = ddwrt:DateTimeTick(ddwrt:GenDisplayName(string($Today)))"
	 ddwrt:cf_explicit="1">
		font-weight: bold; color: #000000; background-color: #FAE032;
	</xsl:if>
</xsl:attribute>

Relative Dates

With Due Dates there are 2 things you want to know: which ones have been missed and how much time is left. Quick color indicators are very effective in drawing the eye to important information and the red and yellow rules we put in place above help quickly answer the first question.

So how do we communicate how much time is left? Calculated columns are no help here (you can’t use Today and even when you hack it, they only get evaluated on modifications, NOT on view). The answer is some XSL tweaking. We won’t be using the same wizard-like interface as above, but I promise the type of XSL we’re going to be doing isn’t too scary.

The first thing we need to do is add some XSL templates to help us perform some basic date calculations. The easiest way is to use Andy Lewis’ DateTemplates. We’re going to pull out the needed templates and paste them directly into our XSL (since I’ve had a lot of trouble referencing external XSL when using Designer). Here’s the templates we want:

<xsl:template name="getDayDelta">
	<xsl:param name="paramDateA"/>
	<xsl:param name="paramDateB"/>
	<xsl:variable name="dateADays">
		<xsl:call-template name="countDaysInDateWithLeapYearDays">
			<xsl:with-param name="paramDate" select="$paramDateA"/>
		</xsl:call-template>
	</xsl:variable>
	<xsl:variable name="dateBDays">
		<xsl:call-template name="countDaysInDateWithLeapYearDays">
			<xsl:with-param name="paramDate" select="$paramDateB"/>
		</xsl:call-template>
	</xsl:variable>
	<xsl:value-of select="number($dateADays) - number($dateBDays)"/>
</xsl:template>

<xsl:template name="countDaysInDateWithLeapYearDays">
	<xsl:param name="paramDate"/>
	<xsl:variable name="year" select="substring-before($paramDate,'-')"/>
	<xsl:variable name="month" select="substring(substring-after($paramDate,'-'),1,2)"/>
	<xsl:variable name="day" select="substring(substring-after(substring-after($paramDate,'-'),'-'),1,2)"/>
	<xsl:variable name="rawYearDays" select="number($year) * 365"/>
	<xsl:variable name="rawLeapYears" select="floor($year div 4)"/>
	<xsl:variable name="centurySpan" select="floor($year div 100)"/>
	<xsl:variable name="fourCenturySpan" select="floor($year div 400)"/>
	<xsl:variable name="boolYearLeap">
		<xsl:call-template name="isLeapYear">
			<xsl:with-param name="paramYear" select="$year"/>
		</xsl:call-template>
	</xsl:variable>
	<xsl:variable name="yearLeapAdjust">
		<xsl:choose>
			<xsl:when test="$boolYearLeap = 1 and (($month = 1) or ($month = 2 and $day != 29))">-1</xsl:when>
			<xsl:otherwise>0</xsl:otherwise>
		</xsl:choose>
	</xsl:variable>
	<xsl:variable name="yearDays" select="$rawYearDays + $rawLeapYears - $centurySpan + $fourCenturySpan + $yearLeapAdjust "/>
	<xsl:variable name="monthDays">
		<xsl:call-template name="ConvertMonthToTotalDays">
			<xsl:with-param name="paramMonth" select="$month"/>
		</xsl:call-template>
	</xsl:variable>
	<xsl:variable name="totalDays" select="$yearDays + number($monthDays) + number($day)"/>
	<xsl:value-of select="$totalDays"/>
</xsl:template>

<xsl:template name="isLeapYear">
	<xsl:param name="paramYear"/>
	<xsl:choose>
		<xsl:when test="$paramYear mod 4 = 0 and ($paramYear mod 100 != 0) or ($paramYear mod 400 = 0)">1</xsl:when>
		<xsl:otherwise>0</xsl:otherwise>
	</xsl:choose>
</xsl:template>

<xsl:template name="ConvertMonthToTotalDays">
	<xsl:param name="paramMonth"/>
	<xsl:choose>
		<xsl:when test="$paramMonth=01">0</xsl:when>
		<xsl:when test="$paramMonth=02">31</xsl:when>
		<xsl:when test="$paramMonth=03">59</xsl:when>
		<xsl:when test="$paramMonth=04">90</xsl:when>
		<xsl:when test="$paramMonth=05">120</xsl:when>
		<xsl:when test="$paramMonth=06">151</xsl:when>
		<xsl:when test="$paramMonth=07">181</xsl:when>
		<xsl:when test="$paramMonth=08">212</xsl:when>
		<xsl:when test="$paramMonth=09">243</xsl:when>
		<xsl:when test="$paramMonth=10">273</xsl:when>
		<xsl:when test="$paramMonth=11">304</xsl:when>
		<xsl:when test="$paramMonth=12">334</xsl:when>
	</xsl:choose>
</xsl:template>

There are several templates above. We’re only going to call the getDayDelta function (it calls all the others). Copy the above XSL and in the Code view for your view of SharePoint Designer find the <Xsl> element. Skip a few lines down just past the last <xsl:param> element and paste the above. It should look something like this:

DateTemplates

Just putting these templates in the XSL doesn’t actually do anything yet. So switch to the Split view and select one of the Due Date values. The corresponding XSL should be highlighted in the code section:

DueDateOriginalXSL

Replace the highlighted section from above with the following:

<xsl:variable name="DateDueDayDelta">
	<xsl:call-template name="getDayDelta">
		<xsl:with-param name="paramDateA" select="ddwrt:FormatDateTime(string($thisNode/@*[name()=current()/@Name]),1033,'yyyy-MM-dd')"/>
		<xsl:with-param name="paramDateB" select="ddwrt:FormatDateTime(string(ddwrt:Today()),1033,'yyyy-MM-dd')"/>
	</xsl:call-template>
</xsl:variable>

<xsl:choose>
	<xsl:when test="$DateDueDayDelta=0">
		<xsl:text>Today</xsl:text>
	</xsl:when>
	<xsl:when test="$DateDueDayDelta=1">
		<xsl:text>1 Day</xsl:text>
	</xsl:when>
	<xsl:when test="$DateDueDayDelta=-1">
		<xsl:text>Yesterday!</xsl:text>
	</xsl:when>
	<xsl:when test="$DateDueDayDelta&lt;-1">
		<xsl:value-of select="concat($DateDueDayDelta,' Days!')"/>
	</xsl:when>
	<xsl:when test="$DateDueDayDelta&gt;1">
		<xsl:value-of select="concat($DateDueDayDelta,' Days')"/>
	</xsl:when>
</xsl:choose>

In lines 1-6 we’re calling the getDayDelta function from the DateTemplates and storing the value in a new variable called DateDueDayDelta. This value is the number of days between the first parameter, Due Date, and the second parameter, Today. We’re using the ddwrt:FormatDateTime function to ensure the parameters are in the form expected by the template. We’re also using the ddwrt:Today() function to get the current date.

Lines 8-24 is an XSL switch statement. We’re using it to give friendly text based on the number of days between. If the dates are the same, then we print “Today“. If the Due Date is still 1 day in the future, we print “1 Day“. If the Due Date is 1 day in the past (-1), we print “Yesterday!“. If the Due Date is even further in the past (< -1), we print “# Days!“. If the Due Date is more than a day away (> 1), we print “# Days“. This will probably make more sense if you just save the view and refresh it in the browser:

TasksWithRelativeDates

WOO HOO! That’s a huge improvement – but it could be better. Although I think it makes more sense to see the dates as relative for quickly glancing at the list, I don’t like losing that information altogether. So let’s put it back in as a tooltip.

In the code view, right above where you pasted the <xsl:variable> element, paste the following:

<span>
	<xsl:attribute name="title">
		<xsl:value-of select="ddwrt:FormatDateTime(string($thisNode/@DueDate),1033,'dddd, M/d/yy ')"/>
	</xsl:attribute>

Then scroll down to the closing <xsl:choose> element and close the <span> tag. Altogether, things should look similar to this:

FinalDueDateXSL

We just wrapped everything in a span so that we could set the title attribute (tooltip). We are again using the ddwrt:FormatDateTime function so that we can format the Due Date to show not just the date but the day of the week as well since this really helps people visualize the date when a calendar isn’t available. Save the view, refresh it in the browser and you should have something like this (The date this screenshot was taken was 2/7/2013):

FinalTasksList

You can quickly see how stacking these techniques can start to make lists much more intuitive and useful. WOWEE!