A Verbose Schema for SharePoint Column Formatting (Proposal)

Declarative customization through Column Formatting in SharePoint Online is a really cool new way to customize how fields in lists and libraries are displayed. It’s all done through JSON and it’s pretty awesome.

I think there are a few minor areas it’s currently falling short, however. Such as:

Unfortunately, although there is an open source repo of great samples, Column Formatting itself is not something we can directly contribute to (outside of issues and user voice like the above). But, I had another issue that I really wanted solved so I solved it (at least for me) and thought I’d share and suggest it (or some version of it) should be adopted officially.

While a UI for generating the JSON would be awesome, the alternative suggestion of writing your column formatter in VS Code using the schema.json is a good one. However, I really wanted better intellisense to help me track down what I can and can’t do. So, I added a bunch of stuff to the schema.json file to do exactly that.

A Verbose Schema

Using my version of the columnFormattingSchema.json (currently available as a gist), you get fancy stuff like this:

VerboseColumnFormatting

Here’s what’s in here compared to the original:

  • Valid operations toLocalString(), toLocaleDateString(), and toLocaleTimeString() are no longer marked as invalid (added them to the operator enum)
  • The style property now only allows values corresponding to supported style attributes
    • Additionally, each style property has enum values corresponding to possible values
  • Valid attributes iconName, rel, and title are no longer marked as invalid
  • class attribute provides enum values (using the predefined classes)
  • target attribute provides enum values
  • role attribute provides enum values
  • rel attribute provides enum values
  • iconName attribute provides enum values
  • Most properties (like txtContent and operators) provide special string enums (@currentField, @me, @now, etc.)

It’s important to note that every value can still be an expression and even where enums are provided for convenience (like class or txtContent), you can still supply a string not in the list.

Using the Schema

When you apply column formatting the JSON is validated, but the actual schema isn’t really restricted like you might expect (this is why you could previously specify an iconName property without issue even though it was technically invalid). This also means that using the Verbose schema won’t cause any problems for you (I’ve actually tested it against every sample available to me) and is actually much more likely to prevent you from getting multiple console error messages about unsupported style attributes, etc.

For now, you can just save the file to your machine and use a local reference (as shown in the image above) or, even better, you can reference it directly from the gist (raw) like this:

{
    "$schema": "https://gist.githubusercontent.com/thechriskent/2e09be14a4b491cfae256220cfca6310/raw/eb9f675bf523208eb840c462d4f716fa92ce14c2/columnFormattingSchema.json"
}

Now, as long as you save that file with a .json extension, VS Code will automatically add the intellisense and extra validation!

You don’t even need to remove the $schema property (you can even leave it out, it is not currently used by SharePoint at all).

Also, for anyone that is wondering what the column formatter shown in the animation above looks like, here it is for a Person field:

Final
My name is red since I’m the logged in user

Top Link Bar Navigation From XML

Applies To: SharePoint

In my last post, Top Link Bar Navigation To XML, I provided you with a script to serialize a site collection’s Global Navigation Nodes to XML. In the post before that, Multi-Level Top Link Bar Navigation (Sub Menus), I showed you how to enable additional sub menus using the native control in SharePoint by editing a simple attribute in the Master Page. However, it quickly became clear that the native navigation editor (Site Actions > Site Settings > Navigation) won’t allow you to edit anything greater than 2 levels despite the control’s support for it. In this final post I’ll show you how to edit the exported XML to add multiple levels and then to import it.

The Script

Copy and paste the code below into notepad and save it as NavigationFromXml.ps1

$xmlFile = "d:\scripts\navigation\ExportedNavigation.xml"
$sourcewebApp = "http://somesite.com"
$destweb = "http://somesite.com"
$keepAudiences = $true #set to false to have it totally ignore audiences (good for testing)

Function CreateNode($xNode,$destCollection){
    Write-Host $xNode.Title

    #make URLs relative to destination
    $hnUrl = $xNode.Url
    #Write-Host "O URL: $hnUrl"
    $hnUrl = SwapUrl $hnUrl $sourceWebApp $destWeb
    #Write-Host "N URL: $hnUrl"

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

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

    if($xNode.Children.Count -gt 0) {
        foreach($child in $xNode.Children) {
            CreateNode $child $hNode.Children
        }
    } elseif($xNode.Children.IsNode -eq "yes") {
        #single child
        CreateNode $xNode.Children $hNode.Children
    }
}

Function SwapUrl([string]$currentUrl,[string]$sourceRoot,[string]$destRoot) {
	if ($currentUrl -ne "/") {
		if ($currentUrl.StartsWith("/")) {
            #Relative URL
			$currentUrl = $sourceRoot + $currentUrl
		} elseif ($currentUrl.StartsWith($destRoot)) {
            #May be a problem for non root sites
			$currentUrl = $currentUrl.Replace($destRoot,"")
		}
	} else {
		$currentUrl = [System.String]::Empty
	}
	$currentUrl
}

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

$gnn = Import-Clixml $xmlFile

$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()
}

if($gnn.IsNode -Eq "yes"){
    #not an array (just 1 top level nav item)
    #Write-Host "1 Only!"
    [void](CreateNode $gnn $pdw.Navigation.GlobalNavigationNodes)
} else {
    #array of nodes, so add each one
    foreach($n in $gnn){
        [void](CreateNode $n $pdw.Navigation.GlobalNavigationNodes)
    }
}

#cleanup
$dw.dispose()

What it Does and How to Use It

There are 4 parameters at the top of the script for you to adjust:

  • $xmlFile: The path to use for the XML input
  • $sourcewebApp: The URL of the web application (Needed to ensure relative links are imported correctly)
  • $destweb: The URL of the site collection you are importing the navigation nodes to
  • $keepAudiences: When $true audience values are used, when $false they are ignored (helpful for testing)

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 run it by typing: .\NavigationToXml.ps1

RunNavigationFromXMLScript

The script will delete all global navigation nodes from the $destweb. It will then use the Import-Clixml command to hydrate a series of custom objects that it will use to build the new navigation nodes. It will build the nodes recursively allowing any number of levels of child nodes (You will have to adjust your Master Page as outlined in my post, Multi-Level Top Link Bar Navigation (Sub Menus), to see any more than the default 2 levels).

How it Works

The main code begins at line 53 where we retrieve the $destweb and then hydrate the $gnn object from the $xmlFile. One of the custom properties used in our NavigationToXml.ps1 script we output was IsNode. In PowerShell an array of one object does not serialize to an array. Rather, it serializes directly to the single object. Using IsNode allows us to know if the object we are working with is an actual node or an array of nodes so that we can avoid exceptions when accessing other properties.

For every node we hydrated we call the function CreateNode (lines 6-36) which creates a node using the custom properties in the passed collection. URLs are made relative to the web application using the function SwapUrl (lines 38-51). This will process every node in the collection along with all of their children.

Editing the XML

So why go through this at all? If you just want to copy global navigation from one site collection to another then just use the simpler NavigationPropagation.ps1 script provided in this article: SharePoint Top Link Bar Synchronization.

However, doing it this way allows you a chance to tweak the XML using notepad (I recommend notepad++). This is the easiest way to add Multiple Levels. For now I’ll explain the basics of the XML document and how to edit it. We’ll go over more of the whys in the Putting it All Together section below.

Structure

To get your base structure it’s best to use the native navigation editor (Site Actions > Site SettingsNavigation) and setup as many of your nodes as you can. Then you can use the NavigationToXml.ps1 script provided in the previous article, Top Link Bar Navigation To XML, as your base document. Trust me, trying to write it all from scratch is dumb. Here’s a quick summary of the results of running that script against navigation that looks like this:

IMG_0347

Skipping the automatic link for the site (Navigation Propagation) here is the current structure of those nodes:

  • Some Sites (Heading)
    • theChrisKent (AuthoredLinkPlain)
    • Microsoft (AuthoredLinkPlain)
  • Google (AuthoredLinkPlain)

Here’s what the exported XML looks like for that same set of nodes:

<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
  <Obj RefId="0">
    <TN RefId="0">
      <T>System.Object</T>
    </TN>
    <ToString>System.Object</ToString>
    <MS>
      <S N="IsNode">yes</S>
      <S N="Title">Some Sites</S>
      <S N="Url">/personal/ckent/navtest</S>
      <S N="NodeType">Heading</S>
      <S N="Description"></S>
      <S N="Audience"></S>
      <Nil N="Target" />
      <Obj N="Children" RefId="1">
        <TN RefId="1">
          <T>System.Object[]</T>
          <T>System.Array</T>
          <T>System.Object</T>
        </TN>
        <LST>
          <Obj RefId="2">
            <TNRef RefId="0" />
            <ToString>System.Object</ToString>
            <MS>
              <S N="IsNode">yes</S>
              <S N="Title">theChrisKent</S>
              <S N="Url">http://thechriskent.com</S>
              <S N="NodeType">AuthoredLinkPlain</S>
              <S N="Description"></S>
              <S N="Audience"></S>
              <Nil N="Target" />
              <Obj N="Children" RefId="3">
                <TNRef RefId="1" />
                <LST />
              </Obj>
            </MS>
          </Obj>
          <Obj RefId="4">
            <TNRef RefId="0" />
            <ToString>System.Object</ToString>
            <MS>
              <S N="IsNode">yes</S>
              <S N="Title">Microsoft</S>
              <S N="Url">http://microsoft.com</S>
              <S N="NodeType">AuthoredLinkPlain</S>
              <S N="Description"></S>
              <S N="Audience"></S>
              <Nil N="Target" />
              <Ref N="Children" RefId="3" />
            </MS>
          </Obj>
        </LST>
      </Obj>
    </MS>
  </Obj>
  <Obj RefId="5">
    <TNRef RefId="0" />
    <ToString>System.Object</ToString>
    <MS>
      <S N="IsNode">yes</S>
      <S N="Title">Google</S>
      <S N="Url">http://google.com</S>
      <S N="NodeType">AuthoredLinkPlain</S>
      <S N="Description"></S>
      <S N="Audience"></S>
      <Nil N="Target" />
      <Ref N="Children" RefId="3" />
    </MS>
  </Obj>
</Objs>

Each node can be found in an Obj element where you can easily find the list of custom properties in the MS section (IsNode, Title, Url, NodeType, Description, Audience, Target and Children). Each of these are simple strings. The only one that can be a little tricky is Audience which we’ll cover in depth in the Adding Nodes section below.

Each Obj element has a unique (to this document) value for it’s RefId. This is just an integer. The only thing to really note here is that each one needs to be unique. They will be the case in any export since this is part of the Export-Clixml command, but you’ll need to pay close attention to this when adding additional nodes. These are also used in Ref elements which you’ll see in various spots. If an object is the exact same as an object previously defined in the document it won’t be defined. Rather it will just get a Ref element instead of an Obj element. The Ref element will have a RefId that is equal to the previously defined Obj‘s RefId.

This mostly comes up with blank children. A good example is the first node with no children above is the node for theChrisKent (lines 22-38). You can see that the Children Obj is defined in lines 33-36. Whereas, the very next node without children (Microsoft lines 39-52) doesn’t have an Obj for Children but rather a Ref (line 50) with a RefId of 3 which you’ll recognize as the RefId specified in line 33. This can seem very confusing at first but it will get easier using the examples below.

Adding Nodes

Adding a single node with no children is pretty easy. Just cut and paste a similar node (Obj) and switch up the RefId to something unique for the document. For instance if I wanted to create another link under Some Sites right after the Microsoft one, I could just copy the Microsoft node (lines 39-52) and paste it directly below its closing tag (</Obj>). It would look something like this:

<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
  <Obj RefId="0">
    <TN RefId="0">
      <T>System.Object</T>
    </TN>
    <ToString>System.Object</ToString>
    <MS>
      <S N="IsNode">yes</S>
      <S N="Title">Some Sites</S>
      <S N="Url">/personal/ckent/navtest</S>
      <S N="NodeType">Heading</S>
      <S N="Description"></S>
      <S N="Audience"></S>
      <Nil N="Target" />
      <Obj N="Children" RefId="1">
        <TN RefId="1">
          <T>System.Object[]</T>
          <T>System.Array</T>
          <T>System.Object</T>
        </TN>
        <LST>
          <Obj RefId="2">
            <TNRef RefId="0" />
            <ToString>System.Object</ToString>
            <MS>
              <S N="IsNode">yes</S>
              <S N="Title">theChrisKent</S>
              <S N="Url">http://thechriskent.com</S>
              <S N="NodeType">AuthoredLinkPlain</S>
              <S N="Description"></S>
              <S N="Audience"></S>
              <Nil N="Target" />
              <Obj N="Children" RefId="3">
                <TNRef RefId="1" />
                <LST />
              </Obj>
            </MS>
          </Obj>
          <Obj RefId="4">
            <TNRef RefId="0" />
            <ToString>System.Object</ToString>
            <MS>
              <S N="IsNode">yes</S>
              <S N="Title">Microsoft</S>
              <S N="Url">http://microsoft.com</S>
              <S N="NodeType">AuthoredLinkPlain</S>
              <S N="Description"></S>
              <S N="Audience"></S>
              <Nil N="Target" />
              <Ref N="Children" RefId="3" />
            </MS>
          </Obj>
          <Obj RefId="10">
            <TNRef RefId="0" />
            <ToString>System.Object</ToString>
            <MS>
              <S N="IsNode">yes</S>
              <S N="Title">Apple</S>
              <S N="Url">http://apple.com</S>
              <S N="NodeType">AuthoredLinkPlain</S>
              <S N="Description"></S>
              <S N="Audience"></S>
              <Nil N="Target" />
              <Ref N="Children" RefId="3" />
            </MS>
          </Obj>
        </LST>
      </Obj>
    </MS>
  </Obj>
  <Obj RefId="5">
    <TNRef RefId="0" />
    <ToString>System.Object</ToString>
    <MS>
      <S N="IsNode">yes</S>
      <S N="Title">Google</S>
      <S N="Url">http://google.com</S>
      <S N="NodeType">AuthoredLinkPlain</S>
      <S N="Description"></S>
      <S N="Audience"></S>
      <Nil N="Target" />
      <Ref N="Children" RefId="3" />
    </MS>
  </Obj>
</Objs>

Pretty straightforward overall. Notice that in line 53 I’ve set the RefId to 10. This is because the only requirement is for it to be unique – It does not have to be in sequence. If you run the NavigationFromXml.ps1 script on the above the site now looks like this:

TopLinkBarWithAppleLink

What about an additional level? For this example we’ll be adding a new sub menu under Some Sites called Pizza with 2 links. Our structure should look like this:

  • Some Sites (Heading)
    • theChrisKent (AuthoredLinkPlain)
    • Microsoft (AuthoredLinkPlain)
    • Apple (AuthoredLinkPlain)
    • Pizza (Heading)
      • Pizza Hut (AuthoredLinkPlain)
      • Little Caesars (AuthoredLinkPlain)
  • Google (AuthoredLinkPlain)

Here’s what our modified XML looks like now:

<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
  <Obj RefId="0">
    <TN RefId="0">
      <T>System.Object</T>
    </TN>
    <ToString>System.Object</ToString>
    <MS>
      <S N="IsNode">yes</S>
      <S N="Title">Some Sites</S>
      <S N="Url">/personal/ckent/navtest</S>
      <S N="NodeType">Heading</S>
      <S N="Description"></S>
      <S N="Audience"></S>
      <Nil N="Target" />
      <Obj N="Children" RefId="1">
        <TN RefId="1">
          <T>System.Object[]</T>
          <T>System.Array</T>
          <T>System.Object</T>
        </TN>
        <LST>
          <Obj RefId="2">
            <TNRef RefId="0" />
            <ToString>System.Object</ToString>
            <MS>
              <S N="IsNode">yes</S>
              <S N="Title">theChrisKent</S>
              <S N="Url">http://thechriskent.com</S>
              <S N="NodeType">AuthoredLinkPlain</S>
              <S N="Description"></S>
              <S N="Audience"></S>
              <Nil N="Target" />
              <Obj N="Children" RefId="3">
                <TNRef RefId="1" />
                <LST />
              </Obj>
            </MS>
          </Obj>
          <Obj RefId="4">
            <TNRef RefId="0" />
            <ToString>System.Object</ToString>
            <MS>
              <S N="IsNode">yes</S>
              <S N="Title">Microsoft</S>
              <S N="Url">http://microsoft.com</S>
              <S N="NodeType">AuthoredLinkPlain</S>
              <S N="Description"></S>
              <S N="Audience"></S>
              <Nil N="Target" />
              <Ref N="Children" RefId="3" />
            </MS>
          </Obj>
          <Obj RefId="10">
            <TNRef RefId="0" />
            <ToString>System.Object</ToString>
            <MS>
              <S N="IsNode">yes</S>
              <S N="Title">Apple</S>
              <S N="Url">http://apple.com</S>
              <S N="NodeType">AuthoredLinkPlain</S>
              <S N="Description"></S>
              <S N="Audience"></S>
              <Nil N="Target" />
              <Ref N="Children" RefId="3" />
            </MS>
          </Obj>
          <Obj RefId="11">
            <TNRef RefId="0" />
            <ToString>System.Object</ToString>
            <MS>
              <S N="IsNode">yes</S>
              <S N="Title">Pizza</S>
              <S N="Url">http://apple.com</S>
              <S N="NodeType">Heading</S>
              <S N="Description"></S>
              <S N="Audience"></S>
              <Nil N="Target" />
              <Obj N="Children" RefId="12">
                <TNRef RefId="1" />
                <LST>
                  <Obj RefId="13">
                    <TNRef RefId="0" />
                    <ToString>System.Object</ToString>
                    <MS>
                      <S N="IsNode">yes</S>
                      <S N="Title">Pizza Hut</S>
                      <S N="Url">http://www.pizzahut.com</S>
                      <S N="NodeType">AuthoredLinkPlain</S>
                      <S N="Description"></S>
                      <S N="Audience"></S>
                      <Nil N="Target" />
                      <Ref N="Children" RefId="3" />
                    </MS>
                  </Obj>
                  <Obj RefId="14">
                    <TNRef RefId="0" />
                    <ToString>System.Object</ToString>
                    <MS>
                      <S N="IsNode">yes</S>
                      <S N="Title">Little Caesars</S>
                      <S N="Url">http://www.littlecaesars.com</S>
                      <S N="NodeType">AuthoredLinkPlain</S>
                      <S N="Description"></S>
                      <S N="Audience"></S>
                      <Nil N="Target" />
                      <Ref N="Children" RefId="3" />
                    </MS>
                  </Obj>
                </LST>
              </Obj>
            </MS>
          </Obj>
        </LST>
      </Obj>
    </MS>
  </Obj>
  <Obj RefId="5">
    <TNRef RefId="0" />
    <ToString>System.Object</ToString>
    <MS>
      <S N="IsNode">yes</S>
      <S N="Title">Google</S>
      <S N="Url">http://google.com</S>
      <S N="NodeType">AuthoredLinkPlain</S>
      <S N="Description"></S>
      <S N="Audience"></S>
      <Nil N="Target" />
      <Ref N="Children" RefId="3" />
    </MS>
  </Obj>
</Objs>

We’ve now added an object with NodeType set to Heading since this is required in order to support having children. We’ve also created a Children Obj that is not a Ref. It has a TNRef and a LST. Inside the LST we just add more of those AuthoredLinkPlain nodes like we did before. You can repeat this same trick for infinite levels down. Running NavigationFromXml.ps1 may result in something like this:

TopLinkBarWithPizzaNoSubMenu

What happened? We can see Pizza but it’s not a sub menu like we expected! These nodes are all there but by default SharePoint doesn’t show them. You’ll need to adjust your Master Page using the techniques in this article: Multi-Level Top Link Bar Navigation (Sub Menus). Once you’ve done that you’ll see something like this:

TopLinkBarWithPizzaAndSubMenu

What about Audiences? This one was a little trickier to get right. The easiest thing to do is apply an audience to a node using the built in navigation editor and then export it using NavigationToXml.ps1 to see what the value should be. But what about when you’ve already done that and you want to manually edit it? An actual audience (Not a SharePoint Group but a compiled audience) is just specified as the GUID followed by 4 semi-colons. If you wish to do more than one then just put a comma between the GUIDs and then add on 4 semi-colons on the end. Here’s what that looks like:

Single Audience:

<S N="Audience">45175fed-9cc8-45ad-9cd6-dda031bf7577;;;;</S>

Multiple Audiences:

<S N="Audience">0a6e46d0-8f45-487d-a249-141fe4a8c429,8b722c7b-a1b4-4f4f-8035-2bac6294b713;;;;</S>

Putting it All Together

This has been a 4 part series on how to improve Top Link Bar navigation:

I’ve provided you with 3 PowerShell scripts (NavigationPropagation.ps1, NavigationToXml.ps1 and NavigationFromXml.ps1) and told you how to make necessary changes to your Master Page. So how do we use all of these?

Because we want multiple sub menus we made the change to our Master Page(s) to set the MaximumDynamicDisplayLevels to 2 (2 is all we wanted, but feel free to go higher as needed). Then we setup our top level links and ran the NavigationToXml.ps1 script just to get our starter structure (We haven’t really needed it since). We made all the adjustments to add our sub menus and nodes then ran the NavigationFromXml.ps1 script to get all that populated.

For changes to our navigation we just update the XML file, run NavigationFromXml.ps1 and then run NavigationPropagation.ps1 to synchronize our changes across our site collections. It works really well. Hopefully you’ll find this system or some parts of it to be helpful too!

Top Link Bar Navigation To XML

Applies To: SharePoint

Continuing in my series on SharePoint’s Top Link Bar (sometimes called the Tab Bar) I want to show you how to serialize a site collection’s Global Navigation Nodes to XML using PowerShell. This isn’t quite the same as just using the Export-Clixml command since we are interested in very specific properties and their format.

In my previous post, Multi-Level Top Link Bar Navigation (Sub Menus), I showed you how to enable additional sub menus using the native control in SharePoint by editing a simple attribute in the Master Page. However, it quickly became clear that the native navigation editor (Site Actions > Site Settings > Navigation) won’t allow you to edit anything greater than 2 levels despite the control’s support for it. In this post I’ll show you how to output a site’s navigation to XML. In my next post I’ll show how to edit it and then import it to create multiple levels.

The Script

Copy and paste the code below into notepad and save it as NavigationToXML.ps1

$xmlFile = "d:\scripts\navigation\ExportedNavigation.xml"
$sourceweb = "http://somesite.com"
$keepAudiences = $true

Function ProcessNode($nodeCollection) {
    $parentCollection = @()
    foreach($node in $nodeCollection) {
       if ($node.Properties.NodeType -ne [Microsoft.SharePoint.Publishing.NodeTypes]::Area -and $node.Properties.NodeType -ne [Microsoft.SharePoint.Publishing.NodeTypes]::Page) {
	   $nHash = New-Object System.Object
           $nHash | Add-Member -type NoteProperty -name IsNode -value "yes"
           $nHash | Add-Member -type NoteProperty -name Title -value $node.Title
           $nHash | Add-Member -type NoteProperty -name Url -value $node.Url
           $nHash | Add-Member -type NoteProperty -name NodeType -value $node.Properties.NodeType
           $nHash | Add-Member -type NoteProperty -name Description -value $node.Properties.Description
           if($keepAudiences){
                $nHash | Add-Member -type NoteProperty -name Audience -value $node.Properties.Audience
           } else {
                $nHash | Add-Member -type NoteProperty -name Audience -value ""
           }
           $nHash | Add-Member -type NoteProperty -name Target -value $node.Target
           if($node.Children.Count -gt 0){
                $nHashChildren = ProcessNode($node.Children)
                $nHash | Add-Member -type NoteProperty -name Children -value $nHashChildren
           } else {
                $nHash | Add-Member -type NoteProperty -name Children -value @()
           }
           $parentCollection += $nHash
       }
    }
    $parentCollection
}

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

ProcessNode($psw.Navigation.GlobalNavigationNodes) | Export-Clixml $xmlFile

#cleanup
$sw.dispose()

What it Does and How to Use It

There are 3 parameters at the top of the script you will need to change:

  • $xmlFile: The path to use for the XML output
  • $sourceweb: The URL of the site whose navigation you are serializing
  • $keepAudiences: When $true audience values are serialized, when $false they are left blank

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 run it by typing: .\NavigationToXml.ps1

RunNavigationToXMLScript

The script will create an array of custom objects that correspond to the $sourceweb‘s navigation nodes and then use standard PowerShell serialization to create a simplified XML document at the location specified by $xmlFile. For instance, given the navigation shown here:

IMG_0347

The XML output will look similar to this:

<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
  <Obj RefId="0">
    <TN RefId="0">
      <T>System.Object</T>
    </TN>
    <ToString>System.Object</ToString>
    <MS>
      <S N="IsNode">yes</S>
      <S N="Title">Some Sites</S>
      <S N="Url">/personal/ckent/navtest</S>
      <S N="NodeType">Heading</S>
      <S N="Description"></S>
      <S N="Audience"></S>
      <Nil N="Target" />
      <Obj N="Children" RefId="1">
        <TN RefId="1">
          <T>System.Object[]</T>
          <T>System.Array</T>
          <T>System.Object</T>
        </TN>
        <LST>
          <Obj RefId="2">
            <TNRef RefId="0" />
            <ToString>System.Object</ToString>
            <MS>
              <S N="IsNode">yes</S>
              <S N="Title">theChrisKent</S>
              <S N="Url">http://thechriskent.com</S>
              <S N="NodeType">AuthoredLinkPlain</S>
              <S N="Description"></S>
              <S N="Audience"></S>
              <Nil N="Target" />
              <Obj N="Children" RefId="3">
                <TNRef RefId="1" />
                <LST />
              </Obj>
            </MS>
          </Obj>
          <Obj RefId="4">
            <TNRef RefId="0" />
            <ToString>System.Object</ToString>
            <MS>
              <S N="IsNode">yes</S>
              <S N="Title">Microsoft</S>
              <S N="Url">http://microsoft.com</S>
              <S N="NodeType">AuthoredLinkPlain</S>
              <S N="Description"></S>
              <S N="Audience"></S>
              <Nil N="Target" />
              <Ref N="Children" RefId="3" />
            </MS>
          </Obj>
        </LST>
      </Obj>
    </MS>
  </Obj>
  <Obj RefId="5">
    <TNRef RefId="0" />
    <ToString>System.Object</ToString>
    <MS>
      <S N="IsNode">yes</S>
      <S N="Title">Google</S>
      <S N="Url">http://google.com</S>
      <S N="NodeType">AuthoredLinkPlain</S>
      <S N="Description"></S>
      <S N="Audience"></S>
      <Nil N="Target" />
      <Ref N="Children" RefId="3" />
    </MS>
  </Obj>
</Objs>

For those familiar with the Export-Clixml PowerShell command this shouldn’t look too crazy. For the rest of us, I will go into detail about this in my next post. Regardless of how complicated that may look to you, it is much simpler than if we had just called Export-Clixml on the Global Navigation Nodes object for the site.

How it Works

The main code begins at line 33 where the $sourceweb is retrieved and then the ProcessNode function (Lines 5-31) is called on the GlobalNavigationNodes collection. This generates an array of custom objects that are then serialized to the $xmlFile using Export-Clixml.

The ProcessNode function creates a custom object for every node and captures the Title, Url, NodeType, Description, Target, and when $keepAudiences is $true the audience information. Every custom object is also given a Children property (lines 21-26) and this is set to an array recursively for all child nodes that exist. This has been written to capture any number of levels of children. (This script does, however, skip all Area and Page node types since these are automatic and shouldn’t be edited manually)

 

So what do we do with this XML document? In my next post I’ll show you how to read, edit and ultimately import these nodes back into the site collection. This can be helpful to copy nodes from one site colleciton to another (although my post on SharePoint Top Link Bar Synchronization provided a much cleaner approach for this). More importantly this will provide you an easy way to have multiple sub menus in the SharePoint Top Link Bar.

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.

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.

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!

Showing Icons in a List View

Applies To: SharePoint 2010

Displaying icons in a list view is a great way to make things immediately more understandable, look awesome and make things oh so pretty. It’s a pretty common request and there are some interesting methods out there to get it done. There’s everything from deployed solutions to give you specialized columns to throwing some magical jQuery on the page. I personally prefer to keep things simple with some quick use of conditional formatting in SharePoint Designer.

Technically this solution uses some XSL which I’ll show you at the end, but you don’t need to know anything about that to get it to work. A good example of a list this works really well for is a Task list. I most often show icons based on Choice columns (since there’s a nice one-to-one mapping between icon and choice value), but you can easily adapt this solution to apply icons based off of other calculations or combination of columns (for instance, showing a frowny face when a due date has been missed and the status is not completed).

Here’s the standard Tasks list that we’re going to iconize:

BasicTaskList

Right away you’ll notice there’s at least 2 easy targets for icons. Both the Status and the Priority columns would really get a big upgrade if turned into icons.

You can put your icons wherever you want, but the easiest place is going to be a picture library right on the site. So create a new picture library called Icons (Site Actions > More Options > Library > Picture Library):

PictureLibrary

Head to the Icons library you just added and upload some icons. We’re going to upload 5 status icons and 3 priority icons. They should all be the same size (16×16 works well, but I’ll leave that up to you). There’s plenty of great icon sets out there (famfamfam and all it’s varients work very well). I’ll be using icons from the Fugue Icons collection since they look nice, there’s tons of them and they’re free:

Status Icons Priority Icons
  • StatusNotStarted  StatusNotStarted.png
  • StatusInProgress  StatusInProgress.png
  • StatusDeferred  StatusDeferred.png
  • StatusWaiting  StatusWaiting.png
  • StatusCompleted  StatusCompleted.png
  • PriorityLow  PriorityLow.png
  • PriorityNormal  PriorityNormal.png
  • PriorityHigh  PriorityHigh.png

Now that the icons are uploaded, it’ll be easy to select them in Designer (You can also have designer upload them directly from your computer while you’re working but there is a bug that sometimes keeps the path relative to your machine rather than the picture library).

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

The basic steps we are going to perform 8 times (one for each image):

  1. In Design view click in one of the cells for the column we are iconizing (Status or Priority)
  2. On the Insert tab in the ribbon, choose Picture:

    InsertPicture
  3. Choose the Icons library (double-click), and pick the appropriate icon image:
    OpenPicture
  4. Fill out the Accessibility Properties dialog with the appropriate information:
    AccessibilityProperties
  5. With the new icon selected, type the value in the title field of the Tag Properties window (This will be the tooltip):TitleProperty
  6. With the new image still selected, choose Hide Content in the Conditional Formatting dropdown in the Options tab on the ribbon:
    HideContent
  7. In the Condition Criteria dialog, select the Field Name as the column, the Comparison as Not Equal and the Value to the value the icon should represent. This basically says when the value of this field isn’t the value this icon is meant for, then don’t show this icon:
    ConditionCriteria
  8. Repeat for all remaining icons

Once you save in SharePoint Designer you should see something like this on the page (after a refresh of course):

TasksWithIcons

That’s it, so super pretty! I’d recommend taking the actual text values away (you’ve got them in the tooltip) or at least adding some spacing.

For those that are interested, what designer’s really doing is generating some XSL templates for you. It’s the equivalent of choosing Customize Item in the Customize XSLT dropdown on the Design tab and adding some extra XSL. The XSL we’re talking about is a simple <xsl:if> element with the <img> tag inside. For instance the Completed Status icon looks like this in XSL:

<xsl:if test="not(normalize-space($thisNode/@Status) != 'Completed')"
  ddwrt:cf_explicit="1">
  	<img alt="Complete" longdesc="Complete"
  	  src="../../Icons/StatusCompleted.png" width="16" height="16"
  	  title="Complete" />
</xsl:if>

XSL isn’t nearly as scary as it seems, but Designer does a pretty good job of wrapping up a lot of basic formatting and conditional checks with some nice wizards – so why not use them?