To read the article online, visit http://www.4GuysFromRolla.com/articles/030806-1.aspx

Examining ASP.NET 2.0's Site Navigation - Part 5

By Scott Mitchell


Introduction


The site navigation features in ASP.NET 2.0 make it easy to define a site map and implement common navigation UI elements, such as a breadcrumb, treeview, and menu. Due to its use of the provide model, you can dictate how to serialize the site map. ASP.NET 2.0 ships with a default implementation that serializes site map information to an XML-formatted file (Web.sitemap, by default), but as we saw in Part 4 this logic can be customized to garner site map information directly from the file system or through a SQL Server database table. Site navigation can even be configured to use security trimming, which will remove those nodes in the site map for which the currently logged on user does not have authorization to view.

The site map provider model and security trimming features are used to customize the set of site map nodes used by the navigation Web controls, and afford a great deal of customization. However, there are times where we may want to customize the rendered output of the navigation control based on the site map data. For example, maybe in our Menu control we want to display an icon next to each menu item depending on some classification defined for the menu item's corresponding site map node. Alternatively, the markup rendered by ASP.NET's built-in navigation controls may not suit our needs. Rather than displaying a TreeView or Menu, we may want to show the site navigation information in a bulleted list. Such functionality is possible by directly working with the SiteMap class.

In this article we'll look at how to accomplish a hodgepodge of customizations when rendering the navigation UI controls. Read on to learn more!

(This article does not explore setting up the site map or the basics of the SiteMap class or the site navigation system. Refer to the earlier parts of this article series for such information.)

Adding Custom Attributes to the Site Map's Nodes


In earlier installments of this article series we discussed how a website's logical navigational structure is composed as a site map, with the site map being a hierarchical collection of site map nodes. Each site map node is represented in code as an instance of the SiteMapNode class, which has properties like ChildNodes, ParentNode, Title, Url, and others. The following diagram provides a pictoral representation of a site map; each block in the diagram is a node in the site map.

A representation of a site map...

The properties of the SiteMapNode class indicate the information that can be stored about a node in the site map. Rather than limit the site map nodes to just the predefined properties in the SiteMapNode class, Microsoft added a generic collection to the class. With this generic collection, you can stuff any values into a SiteMapNode instance and associate it with a string key. This generic collection can be accessed using the following syntax:

// C#
siteMapNodeInstance["key"] = value;
object value = siteMapNodeInstance["key"];

' VB
siteMapNodeInstance("key") = value
Dim value as Object = siteMapNodeInstance("key")

These custom values can then be programmatically accessed when working with the site map nodes (such as when displaying them in a Menu), and decisions can be made based on these values.

Of course, before we can work with the custom values we must first assign the custom values to the SiteMapNode instances that makeup the site map. How you accomplish this depends on the site map provider used. If you use the default site map provider (XmlSiteMapProvider, the one that stores the XML site map in an XML-formatted file), the custom values can be added as additional attributes to the <siteMapNode> elements. For example, imagine that we wanted to include an image in each node in a TreeView control, with the image for each node defined in the site map. We could add an imageUrl attribute to the various <siteMapNode> elements like so:

<?xml version="1.0" encoding="utf-8" ?>
<siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >
 <siteMapNode url="~/Default.aspx" title="Home">
   <siteMapNode url="~/Books/Default.aspx" title="Books"
                imageUrl="books.jpg">
    <siteMapNode url="~/Books/Novels.aspx" title="Novels"
                 imageUrl="books.jpg" />
    <siteMapNode url="~/Books/History.aspx" title="History"
                 imageUrl="books.jpg" />
    <siteMapNode url="~/Books/Romance.aspx" title="Romance"
                 imageUrl="Heart.gif" />
   </siteMapNode>
  
   <siteMapNode url="~/Electronics/Default.aspx" title="Electronics"
                imageUrl="electronics.jpg" />
   <siteMapNode url="~/DVDs/Default.aspx" title="DVDs"
                imageUrl="dvd.png" />
   <siteMapNode url="~/Computers/Default.aspx" title="Computers"
                imageUrl="computer.png" />
 </siteMapNode>
</siteMap>

With the imageUrl properties defined, we now need to customize the navigation UI control used to display this information to include the image URL. Let's look at an example using the TreeView control (see Betrand Le Roy's blog entry Site Map Menu with Icons for an example of this code using the Menu control instead). Start by adding a SiteMapDataSource to the page and then a TreeView, binding the TreeView to the SiteMapDataSource. Next, create an event handler for the TreeView's TreeNodeDataBound event. This event fires once for each node in the TreeView after the item has been bound to the TreeView node.

This event handler passes in as its second parameter a TreeNodeEventArgs instance, which has a Node property which returns the TreeView node being databound. The TreeView node has a DataItem property which returns the object that's been bound to the TreeView. When using the TreeView to show site map data, the DataItem is the particular SiteMapNode instance in the site map that was bound to the particular TreeView node.

The following code checks to make sure that the current SiteMapNode instance has a imageUrl custom value defined. If it does, it sets the TreeView node's ImageUrl property to ~/Images/value, where value is the value of the SiteMapNode's imageUrl attribute.

'VB
Protected Sub TreeViewID_TreeNodeDataBound(ByVal sender As Object, 
           ByVal e As TreeNodeEventArgs) Handles TreeViewID.TreeNodeDataBound

  'Reference the underlying SiteMapNode object...
  Dim nodeFromSiteMap As SiteMapNode = CType(e.Node.DataItem, SiteMapNode)
    
  'If we have an imageUrl value, assign it to the TreeView node's ImageUrl property
  If nodeFromSiteMap("imageUrl") IsNot Nothing Then
    e.Node.ImageUrl = System.IO.Path.Combine("~/Images/", nodeFromSiteMap("imageUrl"))
  End If
End Sub

// C#
protected void TreeViewID_TreeNodeDataBound(object sender,
                                                   TreeNodeEventArgs e)
{
  // Reference the underlying SiteMapNode object...
  SiteMapNode nodeFromSiteMap = (SiteMapNode) e.Node.DataItem;
   
  // If we have an imageUrl value, assign it to the TreeView node's ImageUrl property
  if (nodeFromSiteMap("imageUrl") != null)
    e.Node.ImageUrl = System.IO.Path.Combine("~/Images/", nodeFromSiteMap["imageUrl"]);
}

A TreeView with icons
Please forgive my lack of any semblance of artistic skill...

Creating a Custom Navigation User Interface


ASP.NET 2.0 ships with three controls that are commonly used to display information from the site map:
  • The SiteMapPath, which displays a "breadcrumb" showing the user their position in the site map hierarchy (This control was discussed in detail in Part 2 of this article series.)
  • The TreeView, which displays all of the nodes in the site map in a collapsible treeview
  • The Menu, which displays all of the nodes in the site map in a vertically- or horizontally-aligned menu
To display site map data, both the TreeView and Menu controls should be bound to a SiteMapDataSource. The SiteMapDataSource is a control that returns the site map data to the TreeView or Menu control as a SiteMapNodeCollection object, which contains the hierarchy of nodes. The TreeView and Menu control then programmatically recurse this collection and build up their menu items and tree nodes appropriately.

We can work with the SiteMapNodeCollection returned by the SiteMapDataSource on our own, either declaratively or programmatically. Scott Guthrie has an example of doing just this in his blog entry Data Tutorial #2: Building our Master Page and Site Navigation Structure. He uses a Repeater to declaratively, using something similar to the following markup:

<asp:Repeater runat="server" ID="siteMapAsBulletedList" DataSourceID="SiteMapDataSource1">
    <HeaderTemplate>
        <ul>
            <li><asp:HyperLink runat="server" ID="lnkHome" NavigateUrl='<%# SiteMap.RootNode.Url %>' Text='<%# SiteMap.RootNode.Title %>'></asp:HyperLink></li>
    </HeaderTemplate>

    <ItemTemplate>
        <li>
            <asp:HyperLink runat="server" NavigateUrl='<%# Eval("Url") %>' Text='<%# Eval("Title") %>'></asp:HyperLink>
        </li>
    </ItemTemplate>

    <FooterTemplate>
        </ul>
    </FooterTemplate>
</asp:Repeater>

<asp:SiteMapDataSource ShowStartingNode="false" ID="SiteMapDataSource1" runat="server" />

Here we've bound the Repeater siteMapAsBulletedList to the SiteMapDataSource control SiteMapDataSource1. Keep in mind that the SiteMapDataSource returns a hierarchical SiteMapNodeCollection object. The Repeater, not being a hierarchical data Web control, only enumerates the first "level" of site map nodes returns. That is, it doesn't drill down into each SiteMapNode's children nodes. In this example the SiteMapDataSource's ShowStartingNode is set to False, which causes the SiteMapNodeCollection to begin with the second "level" (Books, DVDs, Electroincs, and Computers). That means for each SiteMapNode in this second level, the ItemTemplate will be instantiated. (The root site map node (Home) is displayed with a bit of markup in the HeaderTemplate.)

With the above declarative markup, the following output will be rendered:

The first level of the site map is displayed in a bulleted list...

This is great for displaying the root site map node and the first level, but what if we want to show additional levels? We can just add another Repeater in the first Repeater's ItemTemplate, setting that second Repeater's DataSource to the ChildNodes property of the current node being used, like:

<asp:Repeater runat="server" ID="siteMapAsBulletedList" DataSourceID="SiteMapDataSource1">
    <HeaderTemplate>...</HeaderTemplate>
    
    <ItemTemplate>
        <li>
            <asp:HyperLink runat="server" NavigateUrl='<%# Eval("Url") %>' Text='<%# Eval("Title") %>'></asp:HyperLink>

            <asp:Repeater runat="server" id="SecondLevel" DataSource='<%# CType(Container.DataItem, SiteMapNode).ChildNodes %>'>
                <HeaderTemplate><ul></HeaderTemplate>
                <ItemTemplate>
                    <li>
                        <asp:HyperLink ID="HyperLink1" runat="server" NavigateUrl='<%# Eval("Url") %>' Text='<%# Eval("Title") %>'></asp:HyperLink>
                    </li>
                </ItemTemplate>
                <FooterTemplate></ul></FooterTemplate>
            </asp:Repeater>
        </li>
    </ItemTemplate>
    
    <FooterTemplate>...</FooterTemplate>
</asp:Repeater>

<asp:SiteMapDataSource ShowStartingNode="false" ID="SiteMapDataSource1" runat="server" />

For C#, the DataSource property would be set like DataSource='<%# ((SiteMapNode) Container.DataItem).ChildNodes %>'

The first and second levels of the site map are displayed in a bulleted list...
The appearance of the bulleted list could be enhanced with a bit of CSS. See Scott Guthrie's blog entry for an example of a much more attractive rendering of the bulleted list example from above.

The declarative approach is limited to only showing as many site map levels as there are embedded Repeaters. That is, if the Romance node had additional sub-nodes, the above declarative markup wouldn't include them. In order to see them using declarative markup we'd need to add a third Repeater to the second Repeater's ItemTemplate. However, that would not display any site nodes that happened to be four levels deep....

To display an arbitrary depth of nodes in a bulleted list we'll need to programmatically invoke the SiteMapDataSource and recursively iterate through the SiteMapNodeCollection object returned. The following code (in VB only, sorry!) shows how to accomplish this. On the page there's a Label Web control with ID bulletedList, and a SiteMapDataSource with ID siteMapData and its ShowStartingNode property set to False. (This code, as well as the earlier examples, are all avaiable for download at the end of this article...)

Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs) _
                                                      Handles Me.Load
  'Create the bulleted list
  bulletedList.Text = _
      String.Format("<ul><li><a href=""{0}"">{1}</a></li>{2}</ul>", _
                    SiteMap.RootNode.Url, SiteMap.RootNode.Title, _
                    DisplaySiteMapLevelAsBulletedList())
End Sub

Private Function DisplaySiteMapLevelAsBulletedList() As String
  'Get the SiteMapDataSourceView from the siteMapData SiteMapDataSource
  Dim siteMapView As SiteMapDataSourceView = _
          CType(siteMapData.GetView(String.Empty), SiteMapDataSourceView)
  
  'Get the SiteMapNodeCollection from the SiteMapDataSourceView
  Dim nodes As SiteMapNodeCollection = _
          CType(siteMapView.Select(DataSourceSelectArguments.Empty), _
          SiteMapNodeCollection)

  'Recurse through the SiteMapNodeCollection...
  Return GetSiteMapLevelAsBulletedList(nodes)
End Function

Private Function GetSiteMapLevelAsBulletedList(ByVal nodes As _
                                 SiteMapNodeCollection) As String
    Dim output As String = String.Empty
    For Each node As SiteMapNode In nodes
        output &= String.Format("<li><a href=""{0}"">{1}</a>", _
                                node.Url, node.Title)

        'Add any children levels, if needed (recursively)
        If node.HasChildNodes Then
            output &= String.Format("<ul>{0}</ul>", _
                        GetSiteMapLevelAsBulletedList(node.ChildNodes))
        End If

        output &= "</li>"
    Next

    Return output
End Function

Conclusion


In this article we saw how to further customize the site navigation system in ASP.NET. The nodes that comprise the site map can have an arbitrary number of custom values associated with them, which can then be examined when working with the site map data. A common example of this is using this custom information to make formatting or appearance-related decisions when rendering a TreeView or Menu. We also saw how to create our own navigation UI elements for greater control over the rendered markup. In this article we saw how to have the site map rendered as a nested, bulleted list, both using declarative and programmatic techniques.

Happy Programming!

  • By Scott Mitchell


    Attachments


  • Download the code used in this article

  • Article Information
    Article Title: ASP.NET.Examining ASP.NET 2.0's Site Navigation - Part 5
    Article Author: Scott Mitchell
    Published Date: March 8, 2006
    Article URL: http://www.4GuysFromRolla.com/articles/030806-1.aspx


    Copyright 2017 QuinStreet Inc. All Rights Reserved.
    Legal Notices, Licensing, Permissions, Privacy Policy.
    Advertise | Newsletters | E-mail Offers