Wednesday, 11 November 2009

I bet you didn't know that adding custom attributes to your web.sitemap was this easy!

The site map file is a great place to store extra snippets of page-specific information. In this article we will explore how easy it is to add custom attributes to a site map by creating a user control to display a sitemap and adding custom attributes to filter out specific pages.

The web.sitemap is one of the reasons that I love asp.net and .net development in general. The library has this awesome habit of providing tons of functionality out-of-the-box and allowing it to be extended when you need to take the next step.

This means that as a developer you can have a big advantage when you move from project to project. You have already taken the time to learn how a component works and you can reuse your skills.

This lesson is going to help us take advantage of the custom attributes that you can add into your sitemap files.

Displaying a site map

We are going to need a simple control to demonstrate a real-world use of the custom attributes technique. I am going to base the control on a snippet of VB code that Scott Mitchell wrote in his article about examining ASP.NET 2.0's Site Navigation - Part 5.

Converted to a C# user control it looks like this:

DisplaySiteMap.ascx

<%@ Control Language="C#" AutoEventWireup="true" CodeFile="DisplaySiteMap.ascx.cs" Inherits="UserControls_DisplaySiteMap" %>
<asp:Literal ID="SiteMapPlaceHolder" runat="server"></asp:Literal>

DisplaySiteMap.ascx.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Text;

public partial class UserControls_DisplaySiteMap : System.Web.UI.UserControl
{
    protected void Page_Load(object sender, EventArgs e)
    {
        // Create the bulleted list
        SiteMapPlaceHolder.Text = GenerateSiteMapHtmlList();
    }

    private string GenerateSiteMapHtmlList()
    {
        return string.Format("<ul><li><a href=\"{0}\">{1}</a></li>{2}</ul>",
            SiteMap.RootNode.Url,
            SiteMap.RootNode.Title,
            GetSiteMapLevelAsBulletedList(SiteMap.RootNode.ChildNodes));
    }

    private string GetSiteMapLevelAsBulletedList(SiteMapNodeCollection nodes)
    {
        if (nodes.Count == 0)
        {
            return string.Empty;
        }

        StringBuilder output = new StringBuilder();
        output.AppendLine("<ul>");

        foreach (SiteMapNode node in nodes)
        {
            output.AppendFormat("<li><a href=\"{0}\">{1}</a>", node.Url, node.Title);

            // Add any children levels, if needed (recursively)
            if (node.HasChildNodes)
            {
                output.AppendLine(GetSiteMapLevelAsBulletedList(node.ChildNodes));
            }

            output.AppendLine("</li>");
        }

        output.AppendLine("</ul>");

        return output.ToString();
    }

}

This sample won't work without a sitemap file so add a new web.sitemap to your project and put this sample content in it:

<?xml version="1.0" encoding="utf-8" ?>
<siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >
  <siteMapNode url="~/Home.aspx" title="Home">
    <siteMapNode url="~/About-Us.aspx" title="About Us" />
    <siteMapNode url="~/Contact-Us.aspx" title="Contact Us" />
    
    <siteMapNode url="~/Toys/Default.aspx" title="Products">
      <siteMapNode url="~/Toys/Toy-Soldier.aspx" title="Toy Solder" />
      <siteMapNode url="~/Toys/Tin-Car.aspx" title="Tin Car" />
      <siteMapNode url="~/Toys/Drum.aspx" title="Drum" />
      <siteMapNode url="~/Toys/Spinning-Top.aspx" title="Spinning Top">
        <siteMapNode url="~/Toys/Spinning-Top-Images.aspx" title="Spinning Top Image Gallery" />
      </siteMapNode>
      <siteMapNode url="~/Toys/Marbles.aspx" title="Marbles" />
    </siteMapNode>

    <siteMapNode url="~/Accessibility.aspx" title="Accessibility Statement" />
    <siteMapNode url="~/Site-Map.aspx" title="Site Map" />
  </siteMapNode>
</siteMap>

I have created a sample page which is included in the download package. It is a page which has the site map control on it to demonstrate the output. I have also created a second, filtered site map user control which we will modify in the next stage to use our custom sitemap attributes. At the moment both of the lists look the same:

SiteMapCustomAttributes-1-SiteMaps

The DisplayInSiteMap Custom Attribute

Now that we have a working demo its time to start modifying the sitemap so that it features some custom attributes. The first attribute that we are going to implement is a flag to say whether we want the particular site map node to be displayed in our site map control. To exclude a node we will add a DisplayInSiteMap="false" attribute like so:

<siteMapNode url="~/Site-Map.aspx" title="Site Map" DisplayInSiteMap="false" />

Because of the flexible way that the sitemap file supports custom attributes your code will still compile when you add this.

To access this data in code you use this notation:

if(SiteMap.Current["DisplayInSiteMap"] == "false")
{
  // don't show the node
}

Its really that simple to add an extra attribute and access the data in your code!

So to lets add that feature to our site map user control. We just have to find the loop that iterates through each of the nodes and add a check for our custom attribute in to it.

The main loop that controls the site map user control is the foreach loop in GetSiteMapLevelAsBulletedList().

Here is the updated method which will go in DisplayFilteredSiteMap.ascx.cs; all other lines in the user control are identical to our first site map user control:

    private string GetSiteMapLevelAsBulletedList(SiteMapNodeCollection nodes)
    {
        if (nodes.Count == 0)
        {
            return string.Empty;
        }

        StringBuilder output = new StringBuilder();
        output.AppendLine("<ul>");

        foreach (SiteMapNode node in nodes)
        {
            // check if node should be displayed
            if (node["DisplayInSiteMap"] != null
                && node["DisplayInSiteMap"].Equals("false", StringComparison.CurrentCultureIgnoreCase))
            {
                continue;
            }

            output.AppendFormat("<li><a href=\"{0}\">{1}</a>", node.Url, node.Title);

            // Add any children levels, if needed (recursively)
            if (node.HasChildNodes)
            {
                output.AppendLine(GetSiteMapLevelAsBulletedList(node.ChildNodes));
            }

            output.AppendLine("</li>");
        }

        output.AppendLine("</ul>");

        return output.ToString();
    }

You should have noticed that the new code differs slightly from my "look how simple it is" code sample I posted further up. In a real world example you would want to check if the attribute is actually set on that node (if it is null or not). I have also used the .Equals() comparison for two reasons:

  1. I find it better to use Equals() because it prevents you making that age old mistake of using the = assignment operator instead of the == comparison operator.
  2. It gives you the option to throw in the option to make the comparison case insensitive - I didn't see any value in being strict in this situation.

To demonstrate this new piece of code in action I am going to sprinkle a few of the custom attributes into our sitemap - on the Site-Map.aspx, Accessibility.aspx and /Toys/Spinning-Top.aspx. This will leave us with the following updated sitemap:

<?xml version="1.0" encoding="utf-8" ?>
<siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >
  <siteMapNode url="~/Home.aspx" title="Home">
    <siteMapNode url="~/About-Us.aspx" title="About Us" />
    <siteMapNode url="~/Contact-Us.aspx" title="Contact Us" />
    
    <siteMapNode url="~/Toys/Default.aspx" title="Products">
      <siteMapNode url="~/Toys/Toy-Soldier.aspx" title="Toy Solder" />
      <siteMapNode url="~/Toys/Tin-Car.aspx" title="Tin Car" />
      <siteMapNode url="~/Toys/Drum.aspx" title="Drum" />
      <siteMapNode url="~/Toys/Spinning-Top.aspx" title="Spinning Top" DisplayInSiteMap="false">
        <siteMapNode url="~/Toys/Spinning-Top-Images.aspx" title="Spinning Top Image Gallery" />
      </siteMapNode>
      <siteMapNode url="~/Toys/Marbles.aspx" title="Marbles" />
    </siteMapNode>

    <siteMapNode url="~/Accessibility.aspx" title="Accessibility Statement" DisplayInSiteMap="false" />
    <siteMapNode url="~/Site-Map.aspx" title="Site Map" DisplayInSiteMap="false" />
  </siteMapNode>
</siteMap>

If we run the sample page again we will see that the two controls are no longer showing identical output:

SiteMapCustomAttributes-2-FilteredSiteMaps

Looking at the graphic above you will see the three pages we marked with the DisplayInSiteMap="false" attribute have disappeared.

You will also spot that the Spinning Top Image Gallery page has also disappeared! This is because if you disable the parent node then it disables all of the nodes underneath it. Kind of obvious when I point it out but I just wanted to make sure that it "clicked" inside your head.

Serving Suggestions

Just like on the front packet of your favourite food which has a serving suggestion that looks great but isn't explained I am going to leave you with a few more serving suggestions for site map related custom attributes you could implement:

  • DisplayInMenu="true/false"
  • CssClass
  • PageIcon
  • Target="_blank/_bottom/_top/etc"
  • TabIndex
  • AccessKey
  • MetaDescription / MetaKeywords

Download Demo Application

A demo application containing a working example of the code described in this article can be downloaded here:

kick it Shout it vote it on WebDevVote.com

9 comments:

Anonymous said...

Very interesting article, and almost perfectly timed in a sense. A few months ago when I started a project to build an entirely new, from the ground up Intranet, I initially constructed my web.sitemap file to first, articulate the logical structure I would follow, then later to construct the actual links, at which time I added some "custom" attributes that I intended to explore later. And today, I open up www.aspnet.com and I see this article. Nice!

Anonymous said...

Thank you ver mucy, really apperciate it.
Please do an article on those attributes (serving suggestion).

Anonymous said...

Very nice article, this help me to kbow more about Custom Atribute in SiteMap. Good article.

Thanks.

Anonymous said...

Hi there..how can we use sql sitemap provider (database)instead of xml file

عمر العمودي said...

Thank You Good

Anonymous said...

Good post! But just to nitpick:

"false".Equals(node["DisplayInSiteMap"], StringComparison.CurrentCultureIgnoreCase)
or
string.Equals("false", node["DisplayInSiteMap"], StringComparison.CurrentCultureIgnoreCase)

doesn't require the null check ;).

rob said...

Excellent

Gerry Creighton said...

I just found this post and wanted to learn from the source code but the download link is no longer working. I'm sure it is due to it being so old. Is the file still available elsewhere?

rtpHarry said...

Gerry, I'm a bit confused as I checked the download link on my article and the 4guysfromrolla article and they both downloaded ok? Made sure I wasn't logged in as well and it still worked. Can you give it another whirl see if it works for you now? visit this then click on the CustomSiteMapAttributes option. If you're still having problems let me know and I'll put it up on GitHub for you.