When you think ASP, think...
Recent Articles
All Articles
ASP.NET Articles
ASPFAQs.com
Message Board
Related Web Technologies
User Tips!
Coding Tips
Search

Sections:
Book Reviews
Sample Chapters
Commonly Asked Message Board Questions
JavaScript Tutorials
MSDN Communities Hub
Official Docs
Security
Stump the SQL Guru!
Web Hosts
XML
Information:
Advertise
Feedback
Author an Article
Jobs

ASP ASP.NET ASP FAQs Message Board Feedback ASP Jobs
 
Print this Page!
Published: Wednesday, March 8, 2006

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

By Scott Mitchell


A Multipart Series on ASP.NET 2.0's Site Navigation
This article is one in a series of articles on ASP.NET 2.0's site navigation functionality.

  • Part 1 - shows how to create a simple site map using the default XML-based site map provider and how to display a TreeView and SiteMapPath (breadcrumb) based on the site map data.
  • Part 2 - explores programmatically accessing site map data through the SiteMap class; includes a thorough discussion of the SiteMapPath (breadcrumb) control.
  • Part 3 - examines how to use base the site map's contents on the currently logged in user and the authorization rules defined for the pages in the site map.
  • Part 4 - delves into creating a custom site map provider, specifically one that bases the site map on the website's physical, file system structure.
  • Part 5 - see how to customize the markup displayed by the navigation controls, and how to create your own custom navigation UI.
  • (Subscribe to this Article Series! )

    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.)

    - continued -

    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

    A Multipart Series on ASP.NET 2.0's Site Navigation
    This article is one in a series of articles on ASP.NET 2.0's site navigation functionality.

  • Part 1 - shows how to create a simple site map using the default XML-based site map provider and how to display a TreeView and SiteMapPath (breadcrumb) based on the site map data.
  • Part 2 - explores programmatically accessing site map data through the SiteMap class; includes a thorough discussion of the SiteMapPath (breadcrumb) control.
  • Part 3 - examines how to use base the site map's contents on the currently logged in user and the authorization rules defined for the pages in the site map.
  • Part 4 - delves into creating a custom site map provider, specifically one that bases the site map on the website's physical, file system structure.
  • Part 5 - see how to customize the markup displayed by the navigation controls, and how to create your own custom navigation UI.
  • (Subscribe to this Article Series! )



  • ASP.NET [1.x] [2.0] | ASPMessageboard.com | ASPFAQs.com | Advertise | Feedback | Author an Article