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);
}
}
}