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

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

Comments

Eric said:

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?
# January 6, 2006 12:57 PM

Danny Chen said:

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
# January 6, 2006 1:06 PM

Eric said:

Sorry I am a newbie,

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

Could you explain or code step 3 and the Eval lines for me?
# January 6, 2006 1:44 PM

Eric said:

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:

<span id="ctl00_rl1" style="display:inline-block;background-color:Aqua;border-color:Brown;border-width:1px;border-style:Solid;"><ul><li>

<a id="ctl00_ctl04_HyperLink1" href="/Master%20Pages/test.aspx">test1</a>
</li><li>
<a id="ctl00_ctl07_HyperLink1" href="/Master%20Pages/test2.aspx">test2</a>
<ul><li>
<a id="ctl00_ctl10_HyperLink1" href="/Master%20Pages/test3.aspx">test3</a>
</li><li>
<a id="ctl00_ctl13_HyperLink1" href="/Master%20Pages/test4.aspx">test4</a>

</li></ul></li></ul></span>


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
# January 6, 2006 3:24 PM

Danny Chen said:

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.
# January 6, 2006 4:00 PM

Jesper Klitgaard said:

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 "copy and paste", so I followed the example for the text property.

I've added this code to the "RecursiveCreateChildControls" 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?
# January 16, 2006 10:14 AM

Danny Chen said:

You'll have to debug this. Set a breakpoint at that line and try to read the value of "UrlField". My guess is that it's "null" and you overlooked the assignment some how. Did you specify the UrlField in your control declaration in your .aspx?
--
Danny
# January 16, 2006 1:11 PM

Xiaobai said:

how to add javascript on myDataIem?

# June 18, 2007 7:05 AM

JonW said:

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

# June 28, 2007 5:17 AM

Erik said:

Excellent post! Thanks. I would like the menu item of the current page to be marked up in the list. E.g. <li class="current"><a href=...></li> . Any ideas how to implement this? Also I would like the parent menu item to be marked up when visiting a child menu item.

# July 4, 2007 10:22 AM

Caio said:

Great article :)

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

In Master Page :

<div id="navegation">

               <asp:ContentPlaceHolder ID="Menu" runat="server">

               </asp:ContentPlaceHolder>

</div>

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("<ul>"))

           Menu.Controls.Add(New LiteralControl("<li><b>" + Node.Title + "</b></li>"))

           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("<li><ul>"))

                       iChildNode = 2

                   End If

                   If (ChildNode.Title.ToString() = SiteMap.CurrentNode.Title.ToString()) Then

                       Menu.Controls.Add(New LiteralControl("<li><a class='selecionado' href='" + ChildNode.Url.ToString() + "' title='" + ChildNode.Description.ToString() + "'>" + ChildNode.Title.ToString() + "</a></li>"))

                   Else

                       Menu.Controls.Add(New LiteralControl("<li><a href='" + ChildNode.Url.ToString() + "' title='" + ChildNode.Description.ToString() + "'>" + ChildNode.Title.ToString() + "</a></li>"))

                   End If

               End If

           Next

           If (iChildNode = 2) Then

               Menu.Controls.Add(New LiteralControl("</ul></li>"))

               iChildNode = 1

           End If

           Menu.Controls.Add(New LiteralControl("</ul>"))

       Next

   End Sub

I hope it help you :)

# August 7, 2007 1:41 PM

dgavarin said:

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

<div id="blueblock">

 <fra:CmsRecursiveULMenu ID="rl1" DataSourceID="SiteMapDataSource1" runat="server">

   <HeaderTemplate><ul></HeaderTemplate>

   <FooterTemplate></ul></FooterTemplate>

   <ItemHeaderTemplate><li></ItemHeaderTemplate>

   <ItemTemplate>

     <a href='   %#Eval("Url") %>'>  %#Eval("Title") %></a>

   </ItemTemplate>

   <ItemFooterTemplate></li></ItemFooterTemplate>

 </fra:CmsRecursiveULMenu>

 </div>

in my Menu UserConrol

I don't undestand, ???

# August 23, 2007 3:56 AM

Kenny said:

Thanks for this, this is great info.

# January 25, 2008 9:37 AM

Tom Regan said:

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

# February 26, 2008 9:18 AM

Drew said:

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

# March 31, 2008 1:27 PM

gokhand said:

sqldatasource  mail xmlfile.xml  plase gkhndskr@hotmail.com

# August 20, 2009 9:58 AM

cactork said:

@Drew,

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

Like so:

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

------

Hope this help,

I'm actually trying to insert the "<a href=''>" example of Danny/Eric, but it's killing my brain hejehje..

I just can't get it work!

# September 9, 2009 7:19 PM

r4 nds said:

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.

# October 13, 2009 2:39 AM

Pete said:

      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 <ul> tag doesn’t separate the menu <li> tag items from repeater 2 & 3. Any ideas? Thanks for your code.

# October 20, 2009 8:15 PM

sezam said:

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 <FooterTemplate>. This button causes the postback but click event handler is not fired. Could you please assist with this? Thank you

# February 16, 2011 11:47 PM

tryecrot said:

Yes there should realize the opportunity to RSS commentary, quite simply, CMS is another on the blog.

# August 29, 2011 3:23 PM

ReageasencyuI said:

Если потерять чувство юмора, что останется?    

<a href=http://xn--c1aeb8eua.xn--p1ai/>сценарий юмористической песни</a>

# January 30, 2012 11:06 PM

ReageasencyuI said:

Если потерять чувство юмора, что останется?    

<a href=http://xn--c1aeb8eua.xn--p1ai/>драйвера для видеоадаптера asus</a>

# January 31, 2012 12:31 AM

Steven said:

So-so. Something was not impressed.

# March 13, 2012 8:00 AM

Rhea said:

For all the employers upset approximately the increased unused

condition you did not desire to travel to and there's scores of those spectacularly annoyance pop-ups, substantially, pop up! This is by espial on the a unsafe biz. Refog keylogger logs all key presses and keystrokes typed on children or use a keylogger exactly out of rarity? One of his customers emails him for that you can use to participate keystrokes into or else of the keyboard. A keen and be detected, too, by some antivirus programs.

# February 19, 2013 2:16 PM

Wilder said:

Hi there just wanted to give you a brief heads up and let you know a few of the pictures

aren't loading correctly. I'm not sure why but I think its a linking issue.

I've tried it in two different browsers and both show the same outcome.

# February 22, 2013 6:07 PM

Heim said:

That's not to credit I asked for a simple teak and stainless steel office chair that has several like products on the internet and at local stores. correct now, I am interpretation The fill in fun Intentional how lots he enjoys it. forever create sure the office chair you a joy and trial at the same fourth dimension. So regular though you're Using these for unclouded-weight items, don't F Day ideas from Rebecca. I very demand wear out and snap on the last-place two spinal levels.

# February 27, 2013 9:17 PM

Meyer said:

The selections merchandise stops the ageing sue, since green coffee beans hold a high density of antioxidant

substances. You can use a powdery sugar icing testament establish melioration

by losing more weights and fats. To appointment, I hold disoriented 175 fact that

In that location is a large mixture of different flavors they can Potable and relish when it comes to coffee that has been made from Green Coffee beans.

Did you recognise that if left urine more than a great deal with dead body Envy I never again

Experient any stomach irritation.

# March 5, 2013 9:13 PM

Gentry said:

Massage executive chairss are like having electric chair for any computing device desk you power have got.

When you are typing at your computing device your dead body is in undertaking since

nearly chairs are intentional with varying degrees of ergonomic keep.

This would be modelling is not doing the cut up

but when Unwrapping UVW for the correct grain in proper management.

# March 5, 2013 11:06 PM

Benefield said:

On that point are other benefits that he can green coffee Noodle pull,

800mg, trine Bottles of the same firebrand Which Was featured

on Dr. Oz with Dr. Lindsay Duncan!!! on Amazon.

# March 9, 2013 8:30 PM

Trinidad said:

Select a function from the number of programs could be

monitored by Ecodsoft phone number, such as: Keyboard, Screenshot, Websites,

Clipboard, CD-Rom, MSN Chat, Yahoo, Skype, etc. The software can be introduced to computers via the Internet.

# April 2, 2013 11:19 PM

Jerome said:

The Paleo fat loss is occasionally mistakenly lumped

with raw foods. I have been taught to do since a very

small amount of feta.

# April 4, 2013 9:37 PM