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

20 Comments

  • Danny,



    Where are the links and URLs at?



    I got your code to work, but all thats outputted is a unordered list with text grabbed from the sitemap. To make this into a menu you need the text to be inside a hyperlink with the url from the sitemap.



    Could you please update your code to make this into a usable menu?

  • Eric,

    Adding another field such as Url, Description, Key, etc is not a difficult process. You should look at how the TextField is implemented and basically copy and paste that code. It should be very straight forward.



    1) Add an extra property for the Url to the Data Item object and a way to set it (like in the constructor).



    2) Add a property for UrlField to the control



    3) Add a couple lines of code that retrieves the property and sets the appropriate field in the Data Item object like it is done for Text in RecursiveCreateChildControls.



    --

    Danny

  • Sorry I am a newbie,



    I get everything up to step #3, and also what the correct Eval(&quot;&quot;) code to use.



    Could you explain or code step 3 and the Eval lines for me?

  • Ok, I finally got it to work, but ran into an output issue. For some reason the control is getting wrapped by a SPAN making the XHTML invalid causing rendering issues in FireFox, but in IE it seems to work.



    Here is my output:



    &lt;span id=&quot;ctl00_rl1&quot; style=&quot;display:inline-block;background-color:Aqua;border-color:Brown;border-width:1px;border-style:Solid;&quot;&gt;&lt;ul&gt;&lt;li&gt;



    &lt;a id=&quot;ctl00_ctl04_HyperLink1&quot; href=&quot;/Master%20Pages/test.aspx&quot;&gt;test1&lt;/a&gt;

    &lt;/li&gt;&lt;li&gt;

    &lt;a id=&quot;ctl00_ctl07_HyperLink1&quot; href=&quot;/Master%20Pages/test2.aspx&quot;&gt;test2&lt;/a&gt;

    &lt;ul&gt;&lt;li&gt;

    &lt;a id=&quot;ctl00_ctl10_HyperLink1&quot; href=&quot;/Master%20Pages/test3.aspx&quot;&gt;test3&lt;/a&gt;

    &lt;/li&gt;&lt;li&gt;

    &lt;a id=&quot;ctl00_ctl13_HyperLink1&quot; href=&quot;/Master%20Pages/test4.aspx&quot;&gt;test4&lt;/a&gt;



    &lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/span&gt;





    Do you have any idea what's causing this to be wrapped in a SPAN? If this can be changed to a DIV instead it would work and be valid XHTML

  • The span was caused by the default begin tag for the HierarchicalDataBoundControl. I added an update to the end of the article for how you can workaround it. Since it's extra code for the sample and not ALWAYS necessary (I realize it IS for the example I'm giving... but I'm just being lazy about updating the entire code sample), I'll leave it up to the readers to add it as needed.

  • Hi,



    I also want to add the url functionality.



    I've added the extra property for the url in my DataItem class and updated the constructor to take another string as argument. Then I added the property for the UrlField in my RecursiveULMenu class. Like you said it's &quot;copy and paste&quot;, so I followed the example for the text property.



    I've added this code to the &quot;RecursiveCreateChildControls&quot; function:



    string url = string.Empty;

    url = (string)DataBinder.GetPropertyValue(data, UrlField);



    I get the following error when I run:

    Value cannot be null.

    Parameter name: propName

    source error:

    Line 157:

    url = (string)DataBinder.GetPropertyValue(data, UrlField);





    Do you have any suggestions to what might be wrong?

  • You'll have to debug this. Set a breakpoint at that line and try to read the value of &quot;UrlField&quot;. My guess is that it's &quot;null&quot; and you overlooked the assignment some how. Did you specify the UrlField in your control declaration in your .aspx?

    --

    Danny

  • how to add javascript on myDataIem?

  • Excelent article!
    been looking for this kind of solution :)

    Alternate solution to UPDATE 1/6/06:
    Protected Overrides ReadOnly Property TagKey() As System.Web.UI.HtmlTextWriterTag
    Get
    Return HtmlTextWriterTag.Div
    End Get
    End Property

  • Excellent post! Thanks. I would like the menu item of the current page to be marked up in the list. E.g. . Any ideas how to implement this? Also I would like the parent menu item to be marked up when visiting a child menu item.

  • Great article :)

    Erik, i made like this : (sorry for my english =/ )

    In Master Page :






    In MasterPage.master.vb (Code-behind) :

    Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Me.Load

    Dim iChildNode As Integer = 1

    For Each Node As SiteMapNode In SiteMap.RootNode.ChildNodes

    Menu.Controls.Add(New LiteralControl(""))
    Menu.Controls.Add(New LiteralControl("" + Node.Title + ""))

    For Each ChildNode As SiteMapNode In Node.ChildNodes
    If ((ChildNode.Url.Length > 0) And (ChildNode.Title.Length > 0)) Then

    If (iChildNode = 1) Then
    Menu.Controls.Add(New LiteralControl(""))
    iChildNode = 2
    End If


    If (ChildNode.Title.ToString() = SiteMap.CurrentNode.Title.ToString()) Then
    Menu.Controls.Add(New LiteralControl("" + ChildNode.Title.ToString() + ""))
    Else
    Menu.Controls.Add(New LiteralControl("" + ChildNode.Title.ToString() + ""))
    End If

    End If

    Next

    If (iChildNode = 2) Then
    Menu.Controls.Add(New LiteralControl(""))
    iChildNode = 1
    End If

    Menu.Controls.Add(New LiteralControl(""))

    Next

    End Sub

    I hope it help you :)

  • hi,

    I have a probleme, with this control in the VS.NET ToolBox, my control is grey;

    but it work well, when i type this







    '> %#Eval("Title") %>





    in my Menu UserConrol

    I don't undestand, ???

  • Thanks for this, this is great info.

  • Great stuff, exactly what I needed, saved me loads of time. Thanks for posting it and for leaving it up.

  • is there a way we can filter out the root node?

  • sqldatasource mail xmlfile.xml plase gkhndskr@hotmail.com

  • @Drew,
    In your SiteMapDataSource, you can specify the property "ShowStartingNode" to "false".

    Like so:

    ------
    Hope this help,

    I'm actually trying to insert the "" example of Danny/Eric, but it's killing my brain hejehje..
    I just can't get it work!

  • thanx for the code......helped me a lot about SiteMaps........This was just the thing I was looking for, keep posting. Will be visiting back soon.

  • I am using your “A simple UL menu” sample codes, it works in the IE7. However it doesn’t work in IE8 & Firefox. It looks like the tag doesn’t separate the menu tag items from repeater 2 & 3. Any ideas? Thanks for your code.

  • Hi Danny!

    I am trying to use this post to create a control i need. Everything works fine expept one thing. I added a button inside . This button causes the postback but click event handler is not fired. Could you please assist with this? Thank you

Comments have been disabled for this content.