Sitemap based ASP.NET menu styling

One problem with the use of a sitemap to feed a bulleted list is that you have no way of modifying individual styles of menu items, such as the current page link, which you may want to have a different style, or even be removed. This mini-turorial looks at how to solve this problem in a quick an easy way, and at providing better control of the generated XHTML code.

The problem

The <asp:BulletedList> control can be linked to an <asp:SiteMap> and 'out of the box' gives you a clean XHTML unnumbered list of anchor elements. However, the code is all on a single line and you have no opportunity to style the list other than the CssClass property on the list as a whole. You can handle the DataBound event on the list, but this does not give you fine control over individual items in the list.

Possible solutions

One solution is to write a custom server control which gives you total flexibility in generating the page code linked to the SiteMap. However, this is probably too complex a solution for a single web application, and even more complex should you want to write a general purpose control suitable for use in other applications.

The other alternative is to use an alternative control to generate the list - one which gives you the opportuntiy to handle generation of the XHTML code at a finer level than the BulletedList. This is the solution I will present in this mini-tutorial

Binding a Repeater to a SiteMap

The beauty of the <asp:Repeater> control is it's ability to bind to a range of data sources and the way in which the developer has total control of the code generated by the control. The first stage in creating the menu is to prototype the desired code for your menu in XHTML, but avoiding special cases, like variable class or id attributes on the elements. For example:

<ul>
  <li><a href="url" title="title">text</a></li>
  <li><a href="url2" title="title2">text2</a></li>
  ...
  <li><a href="urln" title="titlen">textn</a></li>
</ul>

Of course, you are free to put together more complex elements like classes on the elements, or extra XHTML, to support styling such as image replacement techniques in CSS.

The Sitemap to match this menu is a simple single level one, e.g.

<?xml version="1.0" encoding="utf-8" ?>
<siteMap xmlns="http://sche ... Map-File-1.0" >
 <siteMapNode url="~/" title="Dummy" description="Dummy desc">
  <siteMapNode url="~/url.aspx" title="title"
               description="text" />
  <siteMapNode url="~/url2.aspx" title="title2"
               description="text2" />
  ...
  <siteMapNode url="~/urln.aspx" title="titlen"
               description="textn" />
 </siteMapNode>
</siteMap> 

We can add a Repeater into our MasterPage and bind it to the Sitemap. All that is left is to build the templates in the Repeater to generate our XHTML. The first pass is to build a dummy menu, as follows:

<asp:Repeater ID="Repeater1" runat="server"
              DataSourceID="SiteMapDataSource1"
              onitemdatabound="Repeater1_ItemDataBound">
 <HeaderTemplate>
  <ul></HeaderTemplate>
 <ItemTemplate>
    <li><a runat="server"
           href='url'
           title='title'>text</a></li></ItemTemplate>
 <FooterTemplate>
  </ul></FooterTemplate>
</asp:Repeater>
<asp:SiteMapDataSource ID="SiteMapDataSource1"
                       runat="server"
                       ShowStartingNode="False" /> 

Notice how the above is careful about position of the XHTML elements, and also the use of the runat="server" attribute in the anchor so that the server can patch the URLs from the sitemap. This way you can generate nicely structured, readable code. The next step is to replace the dummy data with the values from the site map data source, in this case we use the three fields: url, title and description, coded as <%# Eval("url") %>, etc.

<asp:Repeater ID="Repeater1" runat="server"
              DataSourceID="SiteMapDataSource1"
              onitemdatabound="Repeater1_ItemDataBound">
 <HeaderTemplate>
  <ul></HeaderTemplate>
 <ItemTemplate>
    <li><a runat="server"
           href='<%# Eval("url") %>'
           title='<%# Eval("description") %>'>
          <%# Eval("title") %></a></li></ItemTemplate>
 <FooterTemplate>
  </ul></FooterTemplate>
</asp:Repeater>
<asp:SiteMapDataSource ID="SiteMapDataSource1"
                       runat="server"
                       ShowStartingNode="False" /> 

We could have used an <asp:HyperLink> instead of a plain XHTML anchor, but that would give us less control of the generated code.

Modifying the list at run-time

All that remains is to get access to the menu when the page loads, so that it can be modified, based on the current page. For example, if we are viewing page 2, we may want the Repeater to generate the following code:

<ul>
  <li><a href="url" title="title">text</a></li>
  <li><a class="current" href="url2"
                         title="title2">text2</a></li>
  ...
  <li><a href="urln" title="titlen">textn</a></li>
</ul>

which will allow your CSS to alter the presentation of the current pages menu item.

Alternatively you may want to remove the anchor completely, e.g.

<ul>
  <li><a href="url" title="title">text</a></li>
  <li><span>text2</span></li>
  ...
  <li><a href="urln" title="titlen">textn</a></li>
</ul>

The secret to this technique lies in handling the ItemDataBound event on the Repeater. This event fires each time a template is about to be displayed, including the Header and Footer templates. We can write code to check when an Item is a menu list item and make modification to it, because the event passes the Item in its event parameters. If we add an event handler to our Repeater called 'displayItem' our basic code will look something like this:

protected void displayItem
                     (object sender, RepeaterItemEventArgs e)
{
   if ( item in e is a list item)
   {
     //code here to check what to do
   }
} 
How do we find out whether the item is one of our menu list items?

The e.Item.Controls property contains the content of the item. In our case, the header and footer items only contain Literal text (i.e 1 control each), but the menu items contain 3 controls (The preliminary <li> literal, the HTML anchor and the final </li> literal. We can check for a menu item by checking how many controls are in e.Item. e.g.

if (e.Item.Controls.Count > 1)
{
  // it must be our menu item
}

Of course, if you have a more complex set of templates for your menu you may have to modify this code.

How do we find out if the item is for our current page?

In our case e.Item.Controls[1] is an HtmlAnchor control and so we can get its Href property. We can also get the URL of the current page from the Page.Request property. We must first get the URLs into a common format and convert them to lower case so that we can compare them. e.g.

HtmlAnchor anchor = (HtmlAnchor)e.Item.Controls[1];
string menuItemUrl = Page.ResolveUrl(anchor.HRef).ToLower();
string pageUrl = Page.Request.Url.AbsolutePath.ToLower();
if (menuItemUrl == pageUrl)
  {
    // must be for this page
  }

This will only work if the SiteMap contains the name of the page in the URL. There may also be problems if your pages postback using query strings. However, the code can be modified to handle such situations should they arise for a particular project.

How to add a class attribute to the anchor

This is simplicity itself. We can access the Attributes property of the anchor directly and use the Add method to add our details. e.g.

(e.Item.Controls[1] as HtmlAnchor).Attributes.Add
                                          ("class", "current"); 
How to replace the anchor with a span

The Controls property has methods RemoveAt and AddAt so we can remove the anchor and add a replacement control. e.g.

e.Item.Controls.RemoveAt(1);
Literal lit = new Literal();
lit.Text = "<span>" + anchor.InnerText + "</span>";
e.Item.Controls.AddAt(1, lit);

Finishing off

All that is left to do now is produce the CSS to handle the presentation of the menu. We have a menu which has identifiably different content for the current page, so the CSS can target this. You may have noticed that the Developer Notes pages are using this technique - there is no longer a link on the current page, and the menu item is highlighted. Below is the actual C# code for the Developer Notes master page:

protected void menuItemDataBound
   (object sender, RepeaterItemEventArgs e)
{
 if (e.Item.Controls.Count == 3)
 {
  HtmlAnchor anchor = (HtmlAnchor)e.Item.Controls[1];
  string thismenuitemurl =
                   Page.ResolveUrl(anchor.HRef).ToLower();
  string thispageurl = Page.Request.Url.AbsolutePath.ToLower();
  if (thismenuitemurl == thispageurl)
  {
   e.Item.Controls.RemoveAt(1);
   Literal lit = new Literal();
   lit.Text = "<span>" + anchor.InnerText + "</span>";
   e.Item.Controls.AddAt(1, lit);
  }
 }
} 
Valid XHTML 1.0! | Valid CSS! | WCAG Approved AA
Page design by: John P Scott - Hosting with: Netcetera