-[Danny Chen]- Blog of an ASP.NET QA tester

Tips and info about Site Navigation, ImageMap, Menu and other cool ASP.NET v2.0 features.

December 2005 - Posts

Rendering a databound UL menu

One very common request I get is how to render the Menu with <UL>'s and <LI>'s instead of the heavy table rendering.  Even though the Menu allows for templating, most users quickly find that this cannot be solved with just templating.  Perhaps we didn't expect this or we didn't think it was important enough at the time; I'm not sure, the decision was made long before me.  However, whatever the reason, the Menu doesn't support this kind of rendering.  But, of course, since you're reading this, there are several solutions...   

A simple UL menu

The simplest solution for a single level of hierarchy is to simply use a Repeater control bound to your SiteMapDataSource:

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

<asp:Repeater ID="Repeater1" runat="server" DataSourceID="SiteMapDataSource1">
    <HeaderTemplate><ul></HeaderTemplate>
    <ItemTemplate><li><%# Eval("title") %></li></ItemTemplate>
    <FooterTemplate></ul></FooterTemplate>
</asp:Repeater>

It's a pretty straight forward solution.  It's fairly flexible as well.  Templating and Databinding are availible so custom styling of individual items should be easily possible. 

The next obvious improvement is to allow some hierarchy information.  This can be done with nesting the Repeaters.

<asp:SiteMapDataSource ID="SiteMapDataSource2" runat="server" ShowStartingNode="False" />
<asp:Repeater ID="Repeater2" runat="server" DataSourceID="SiteMapDataSource2">
    <HeaderTemplate><ul></HeaderTemplate>
    <ItemTemplate><li><%# Eval("title") %>
        
<asp:SiteMapDataSource ID="SiteMapDataSource2_1" runat="server"
                               ShowStartingNode="False" StartingNodeUrl='<%# Eval("url") %>' />
        <asp:Repeater ID="Repeater3" runat="server" DataSourceID="SiteMapDataSource2_1">
            <HeaderTemplate><ul></HeaderTemplate>
            <ItemTemplate><li><%# Eval("title") %></li></ItemTemplate>
            <FooterTemplate></ul></FooterTemplate>
        </asp:Repeater>                
        
</li>
    </ItemTemplate>
    <FooterTemplate></ul></FooterTemplate>
</asp:Repeater>

Again this works fairly well and doesn't use any code.  But there are a couple obvious issues with this approach.  First, although it provides for hierarchy, it's still limited to a fixed depth.  Second, if a node has no child nodes, the code will still render and empty <ul></ul> tag.

A custom, templated HierarchicalDataBoundControl

Ok, so writing this blog entry was just an excuse for me to have a little fun and come up with this sample.  But, the way to get the most flexible rendering is a custom control.  This is also a good example of creating a templated control and implementing a HierarchicalDataBound control. 

Implementing Templates and Databinding

Actually templates aren't new, they've been around for a while.  Using them is pretty straight forward.  Declare a template property using the ITemplate interface and instantiate it with InstantiateIn(container).

Private _HeaderTemplate As ITemplate

<PersistenceMode(PersistenceMode.InnerProperty), _
 TemplateContainer(GetType(MyDataItem))> _
Public Property HeaderTemplate() As ITemplate
    
Get
        Return _HeaderTemplate
    
End Get
    Set(ByVal value As ITemplate)
        _HeaderTemplate = value
    
End Set
End Property

Protected Overrides Sub CreateChildControls()
    Dim header As New MyDataItem(String.Empty)
    HeaderTemplate.InstantiateIn(header)
    Controls.Add(header)
End Sub


 

What is new is the IDataItemContainer interface.  This new interface is designed to make it easier to databind using Eval(<prop>).  The interface has 3 members:

Public ReadOnly Property DataItem() As Object Implements IDataItemContainer.DataItem
Public ReadOnly Property DataItemIndex() As Integer Implements IDataItemContainer.DataItemIndex
Public ReadOnly Property DisplayIndex() As Integer Implements IDataItemContainer.DisplayIndex

The DataItemIndex and DisplayIndex properties are used to keep track of the index of the current item and it's index in the rendering.  This can be useful for controls that do paging.  However, if you aren't paging, you can cheat (like I did) and return a dummy value.  The DataItem property is the most interesting one.  It's the object that is returned from an operation like:  <%# Container.DataItem %>  The way this ties into templates is that the template container should implement the IDataItemContainer interface. 

    Public Class MyDataItem
        
Inherits Control
        
Implements INamingContainer
        
Implements IDataItemContainer

        
Private _data As MyDataObject

        Public ReadOnly Property DataItem() As Object Implements UI.IDataItemContainer.DataItem
            
Get
                Return _data
            End Get
        End Property

        Public ReadOnly Property DataItemIndex() As Integer Implements IDataItemContainer.DataItemIndex
            
Get
                Return 0
            
End Get
        End Property

        Public ReadOnly Property DisplayIndex() As Integer Implements IDataItemContainer.DisplayIndex
            
Get
                Return 0
            
End Get
        End Property
    End Class

 

Implementing a HierarchicalDataBoundControl

Implementing a HierarchicalDataBoundControl is a lot like any other custom control.  The best place to override is CreateChildControls.  A common design pattern is to create a private recursive CreateChildControls and call into it from CreateChildControls.  The biggest feature that the HierarchicalDataBoundControl base class gives us is GetData(string) and GetDataSource().  These two provide access to a HierarchicalDataSourceView which in turn provides access to the hierarchy data through IHierarchicalEnumerable and IHierarchyData objects.  You can see more information about those interfaces in this previous post and at the links at the end of the article. 

 

        Protected Overrides Sub CreateChildControls()
            RecursiveCreateChildControls(GetData(
"").Select())
        
End Sub

        Private Sub RecursiveCreateChildControls(ByVal dataItems As IHierarchicalEnumerable)
            For Each e As Object In dataItems

                Dim data As IHierarchyData = dataItems.GetHierarchyData(e)

                
Dim text As String = String.Empty
                text = DataBinder.GetPropertyValue(data, TextField)
 
                ... add controls ...
 
                RecursiveCreateChildControls(data.GetChildren())

            Next
        End Sub

One other extremely useful class is the DataBinder class.  Recall from the IDataItemContainer interface that the data is just an object reference.  It may be an xmlElement or a SiteMapNode or a DataRowView or some other object.  In order to extract property values from an arbitrary object, the DataBinder.GetPropertyValue(object, string) function can be used.  Putting together all these concepts and we have a HierchicalDataBoundControl that supports templates and databinding.  The complete code listing for this custom control is availible at the end of this post.  Here's how it can be used for a <UL> <LI> menu.  The exact same code can also be bound to a XmlDataSource.

<asp:SiteMapDataSource ID="SiteMapDataSource3" runat="server" />

<My:RecursiveULMenu ID="rl1" DataSourceID="SiteMapDataSource3"
                    
runat="server" TextField="title">
    <HeaderTemplate><ul></HeaderTemplate>
    <FooterTemplate></ul></FooterTemplate>
    <ItemHeaderTemplate><LI></ItemHeaderTemplate>
    <ItemTemplate>
        <%# Eval("Text") %>    
    
</ItemTemplate>
    <ItemFooterTemplate></LI></ItemFooterTemplate>
</My:RecursiveULMenu>
                  

<asp:XmlDataSource ID="XmlDataSource1" runat="server" DataFile="XMLFile.xml" />

<My:RecursiveULMenu ID="RecursiveList1" DataSourceID="XmlDataSource1"
                    
runat="server" TextField="name">
    <HeaderTemplate><ol></HeaderTemplate>
    <FooterTemplate></ol></FooterTemplate>            
    
<ItemHeaderTemplate><LI></ItemHeaderTemplate>
    <ItemTemplate>
        <%# Eval("Text") %>    
    
</ItemTemplate>
    <ItemFooterTemplate></LI></ItemFooterTemplate>
</My:RecursiveULMenu>

References:

http://msdn2.microsoft.com/library/system.web.ui.idataitemcontainer.aspx
http://msdn2.microsoft.com/library/system.web.ui.webcontrols.hierarchicaldataboundcontrol.aspx
http://msdn2.microsoft.com/en-us/library/system.web.ui.itemplate.aspx
http://msdn2.microsoft.com/library/system.web.ui.ihierarchicalenumerable.aspx
http://msdn2.microsoft.com/library/system.web.ui.ihierarchydata.aspx

Code Listings:

Link to VB code listing
Link to C# code listing

 

UPDATE 1/6/06:

  As found by Eric in the comments below, the code gets wrapped in a SPAN.  It's possible that you might want to wrap the control with a DIV instead.  In that case you can override RenderBeginTag like so:

Public Overrides Sub RenderBeginTag(ByVal writer As System.Web.UI.HtmlTextWriter)
    writer.AddAttribute(HtmlTextWriterAttribute.Id,
Me.ClientID)
    writer.RenderBeginTag(HtmlTextWriterTag.Div)
End Sub
 
 or
 
public override void RenderBeginTag(System.Web.UI.HtmlTextWriter writer) {
    writer.AddAttribute(
HtmlTextWriterAttribute.Id, this.ClientID);
    writer.RenderBeginTag(
HtmlTextWriterTag.Div);
}
Menu Control doesn't work after migrating from VS2003 and ASP.NET 1.1

I noticed this issue quite a bit on the forums.  A lot of users are finding that after moving their ASP.NET 1.1 site to ASP.NET 2.0 and then adding a Menu Control to their app, the Menu doesn't work. 

The reason this doesn't work is that an extra tag was added to web.config <xhtmlConformance mode="Legacy" />  This flag was meant for 2.0 to act a lot more like 1.1 and one of the major differences is the naming convention of controls.  This naming change causes the quirky behavior with Menu in IE.

There are 2 fixes for this issue:

1) Remove the xhtmlConformance tag from web.config
Personally if you can do it, I recommend this option.  If you're using ASP.NET 2.0 features, you should try and stick with the new rendering modes.

2) If you can't do #1, then give your masterpage an ID
To do this, add a line to your Page_Load for your Masterpage:

public partial class MasterPage : System.Web.UI.MasterPage
{
    protected void Page_Load(object sender, EventArgs e)
    {
        this.ID = "Master1";
    }
}

Hacking the SiteMapPath, changing it's data source.

The SiteMapPath control is designed to work directly against a SiteMapProvider.  However, there is sometimes that rare
occasion where a developer might want to take advantage of the formatting of a SiteMapPath but take the data from somewhere
other than a SiteMapProvider (such as another control).  In that case, obviously, the developer would be inclined to build a
custom control that inherits from SiteMapPath.  However, since the SiteMapPath works directly with a provider and not with a
datasource, the process seems trickier.  This MSDN help article helps describes some of the process:

http://msdn2.microsoft.com/en-us/library/system.web.ui.webcontrols.sitemappath.createcontrolhierarchy.aspx

As mentioned in the article, there are two primary places a developer is likely to override.  InitializeItem where completely
custom rendering can be injected and CreateControlHierarchy where the controls are generated.  This article will focus on
CreateControlHierarchy because that is what needs to be overridden to change the data feed.

The purpose of CreateControlHierarchy is to take the data from a source (normally a SiteMapProvider) and convert it into a
series of SiteMapNodeItem controls which are added to the Controls collection of the SiteMapPath.  In the process, InitializeItem
is called on each item which populates it with any templating/style information that has been set.  It also is responsible for firing any
Databinding and Creation events. 

Here's an example of a custom CreateControlHierarchy function which takes it's data from a TreeView.  The idea in this case is to
take a populated TreeView with a selected node and render out a path from the root of the tree to that node.  There's a link to a
working control listing at the end of the article.

        Protected Overrides Sub CreateControlHierarchy()
            
Dim item As SiteMapNodeItem = Nothing

            ' Get the current selected tree node
            If Tree Is Nothing Then
                Throw New Exception("Tree or TreeViewID is not valid")
            
End If
            Dim node As TreeNode = Tree.SelectedNode()
            
If node Is Nothing Then
                Return
            Else
                ' Create the current node
                AddItemFromTreeNode(item, node, SiteMapNodeItemType.Current)
                node = node.Parent

                
' Create the parent nodes and separators
                While node IsNot Nothing AndAlso node.Parent IsNot Nothing
                    AddItemFromTreeNode(item, Nothing, SiteMapNodeItemType.PathSeparator)
                    AddItemFromTreeNode(item, node, SiteMapNodeItemType.Parent)
                    node = node.Parent
                
End While

                ' Create the root node (unless there is only one node)
                If node IsNot Nothing Then
                    AddItemFromTreeNode(item, Nothing, SiteMapNodeItemType.PathSeparator)
                    AddItemFromTreeNode(item, node, SiteMapNodeItemType.Root)
                
End If
            End If
        End Sub

The function AddItemFromTreeNode will add SiteMapNodeItems to the Controls collection that reflect the passed in
TreeNode.  The code itself is very straight forward, add the current node, any number of parent nodes, and then the root node, all
the while looping through the parent node hierarchy of the TreeView.

 Link to VB Code Listing

Setting the DataSource property of a Menu or TreeView to an array

Here is a neat "trick" you can do with the Data controls.  If you have an array of data you want to format, you can just
directly assign it to the Datasource property kind of like this:

 <%@ Page Language="VB" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<
script runat="server">
   Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs)
      DataList1.DataSource =
New String() {"one", "two", "three"}
      DataList1.DataBind()
   End Sub
</
script>
<
html xmlns="http://www.w3.org/1999/xhtml" >
   <
head runat="server">
      <title>Untitled Page</title>
   </
head>
<
body>
<form id="form1" runat="server">
<div>
   <asp:DataList ID="DataList1" runat="server">
      <ItemTemplate>
         <%# Container.DataItem %>
      </ItemTemplate>
   </asp:DataList>
</div>
</form>
</
body>
</
html>

However, if you've ever tried this with the Menu or Treeview, you'd quickly figure out it doesn't work.  This is
because the HierarchicalDataSourceView requires a new type called a IHierarchicalEnumerable which enumerates
a type called IHierarchyData.  The solution would be to implement these two interfaces as minimally as possible.

 

 Public Class HierarchicalData
   Implements IHierarchicalEnumerable



   
Private data As ArrayList
   Public Sub New(ByVal values() As String)
      data =
New ArrayList()
      For Each value As String In values
         data.Add(
New HierarchicalDataElement(value))
      Next
   End Sub

   Public
Function GetEnumerator() As System.Collections.IEnumerator Implements System.Collections.IEnumerable.GetEnumerator
      Return data.GetEnumerator()
   End Function
   Public Function GetHierarchyData(ByVal enumeratedItem As Object) As System.Web.UI.IHierarchyData Implements System.Web.UI.IHierarchicalEnumerable.GetHierarchyData
      Return CType(enumeratedItem, HierarchicalDataElement)
   End Function
End
Class


Public
Class HierarchicalDataElement
   Implements IHierarchyData    
   
Public _data As String
   Public Sub New(ByVal data As String)
      _data = data
   End Sub
   Public Function GetChildren() As System.Web.UI.IHierarchicalEnumerable Implements System.Web.UI.IHierarchyData.GetChildren
      Return Nothing
   End Function
   Public Function GetParent() As System.Web.UI.IHierarchyData Implements System.Web.UI.IHierarchyData.GetParent
      Return Nothing
   End Function
   Public ReadOnly Property HasChildren() As Boolean Implements System.Web.UI.IHierarchyData.HasChildren
   Get
      Return False
   End Get
   End Property
   Public ReadOnly Property Item() As Object Implements System.Web.UI.IHierarchyData.Item
   Get
      Return _data
   End Get
   End Property
   Public ReadOnly Property Path() As String Implements System.Web.UI.IHierarchyData.Path
   Get
      Return _data
   End Get
   End Property
   Public ReadOnly Property Type() As String Implements System.Web.UI.IHierarchyData.Type
   Get
      Return _data.GetType().ToString()
   End Get
   End Property
   
    Public
Overrides Function ToString() As String
       Return _data
    End Function
End
Class
 

And to use it, declare a Menu on your form and just add the following code.  There is no need to bind to
Container.DataItem in a template because the MenuItems get their text value from the IHierarchyData.ToString() overload. 

 

<script runat="server">
   Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs)
      Menu1.DataSource = New HierarchicalData(New String() {"one", "two", "three"})
      Menu1.DataBind()
   End
Sub
</
script
>
Customizing Individual Menu Items

One question I see very frequently is how to customize the styles on an individual menu item. Declaratively, this isn't possible, although it is on the list of features for a future release. However, that doesn't mean it can't be done at all. It does take a little more work.  So, here are a couple variations on a theme to help customize various items. The ideas here can be extended on easily and I expect we might see some pretty interesting Menu's out there in the future.

Here's the most straight forward brute force way of setting an individual item style:

 <script runat="server">
Shared colorWheel As System.Collections.Generic.List(Of System.Drawing.Color)
Shared currentColor As System.Collections.Generic.List(Of System.Drawing.Color).Enumerator
Public Function GetBackColor() As System.Drawing.Color
   If colorWheel Is Nothing Then
      populatecolors()
   End If
   currentColor.MoveNext()
   Return currentColor.Current
End Function
Private Sub populatecolors()
   colorWheel =
New System.Collections.Generic.List(Of System.Drawing.Color)
   colorWheel.Add(Drawing.Color.Purple)
   colorWheel.Add(Drawing.Color.Green)
   colorWheel.Add(Drawing.Color.Brown)
   colorWheel.Add(Drawing.Color.Chartreuse)
   currentColor = colorWheel.GetEnumerator()
End Sub
</
script>

<asp:Menu ID="Menu1" runat="server" Orientation="Horizontal">

   <Items>

      <asp:MenuItem Text="one" Value="one"></asp:MenuItem>

      <asp:MenuItem Text="two" Value="two"></asp:MenuItem>

      <asp:MenuItem Text="three" Value="three"></asp:MenuItem>

      <asp:MenuItem Text="four" Value="four"></asp:MenuItem>

   </Items>

   <StaticItemTemplate>

      <asp:Label runat="server" ID="Label1" Text='<%# Eval("Text") %>'

         BackColor='<%# GetBackColor() %>' />

   </StaticItemTemplate>

</asp:Menu>

 

Now, this isn't the most ideal code for a number of reasons, but it gets the job done. So lets make 2 improvements. First, lets use the Style property so that we're not limited to a single style. Second, lets get away from relying on matching the order in our style list and the order of declared items.

<script runat="server">
Shared
styles As System.Collections.Generic.Dictionary(Of String, String)
Public Function GetStyle(ByVal value As String) As String
   If styles Is Nothing Then
      populateStyles()
   End If
   Return styles(value)
End Function
Private Sub populateStyles()
   styles =
New System.Collections.Generic.Dictionary(Of String, String)
   styles(
"one") = "background-color:Blue; color:White;"
   styles("two") = "background-color:Black; color:Yellow;"
   styles("three") = "background-color:Purple; color:Black;"
   styles("four") = "background-color:Green; color:Red;"
End Sub
</script>

<asp:Menu ID="Menu1" runat="server" Orientation="Horizontal">

   <Items>

      <asp:MenuItem Text="one" Value="one"></asp:MenuItem>

      <asp:MenuItem Text="two" Value="two">

         <asp:MenuItem Text="three" Value="three"></asp:MenuItem>

         <asp:MenuItem Text="four" Value="four"></asp:MenuItem>

      </asp:MenuItem>

   </Items>

   <StaticItemTemplate>

      <asp:Label runat="server" ID="Label1" Text='<%# Eval("Text") %>'

         Style='<%# GetStyle( Eval("Value") ) %>' />

   </StaticItemTemplate>

   <DynamicItemTemplate>

      <asp:Label runat="server" ID="Label1" Text='<%# Eval("Text") %>'

         Style='<%# GetStyle( Eval("Value") ) %>' />

   </DynamicItemTemplate>

</asp:Menu>

 

With this improvement, we have a lot control. Dynamic items also aren't a problem (although technically they weren't with the first method). With a bit of a variation, we can also make this work with a databound Menu like this:

 <script runat="server">
Shared styles As New System.Collections.Generic.Dictionary(Of String, String)
Protected Sub Menu1_MenuItemDataBound(ByVal sender As Object, _
                                      
ByVal e As System.Web.UI.WebControls.MenuEventArgs)
   styles(e.Item.ValuePath) =
CType(e.Item.DataItem, SiteMapNode)("style")
End Sub </script>
<asp:Menu ID="Menu1" runat="server" DataSourceID="SiteMapDataSource1"
   OnMenuItemDataBound="Menu1_MenuItemDataBound">
   <StaticItemTemplate>
      <asp:Label runat="server" ID="Label1" Text='<%# Eval("Text") %>'
         
Style='<%# styles( Eval("ValuePath") ) %>' />
   </StaticItemTemplate>
   <DynamicItemTemplate>
      <asp:Label runat="server" ID="Label1" Text='<%# Eval("Text") %>'
         
Style='<%# styles( Eval("ValuePath") ) %>' />
   </DynamicItemTemplate>
</asp:Menu>



[web.sitemap]
<siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >
   <
siteMapNode url="root.aspx" title="root" style="">
      <
siteMapNode url="one.aspx" title="one" style="background-color:Blue; color:White;" />
      <
siteMapNode url="two.aspx" title="two" style="background-color:Black; color:Yellow;">
         <
siteMapNode url="three.aspx" title="three" style="background-color:Purple; color:Black;" />
         <
siteMapNode url="four.aspx" title="four" style="background-color:Green; color:Red;" />
      </
siteMapNode>
   </
siteMapNode>
</
siteMap>
More Posts