in

ASP.NET Weblogs

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

 

zakoops said:

Straight cascading style sheets with xhtml would not be simpler?

But that would disrupt the sitemap and breadcrumb gizmos... And that's the problem with ASP.NET (1.0, and more so in 2.0) as everything is so coupled that changing one thing will cascade on other ones.

I'm still debating with myself as to which way is the better one: going simpler in php with xhtm/css/w3c or forcing ASP.NET to adhere correctly to that trio, or yet, to force master pages into that, or what?

When well done, xhtml and css are not rocket science but do deserve good understanding. Yet, I'm not quite at ease having to add the complexities of ASP.NET/C#, *AND* having first to understand the .net class hierarchy just to know exactly where and when I can alter the normal behaviour of some controls like you do in your post.

Making my site more compliant to W3C standards (http://www.webstandards.org/about/) is the main reason for a big redesign in view. And while doing so, I intend updating it from ASP.NET 1.1 to 2.0. Even if VS 2005 is much more apt to deliver and conform to these standards, I can see that deep inside those APS.NET controls, we still have old tricks full of <TABLE>, <TR> and <TD>, meaning that I will have to clearly understand code snippets like the ones you posted and, doing so, struggle against ASP.NET.

But I want to reflect on that before doing so...


December 20, 2005 10:59 AM
 

Danny Chen said:

zakoops,
You raise some very valid points. Let me see if I can address them.

Firstly, there are certainly many ways to produce the same outcome. However, there is a minimal level of complexity required to develop a proper databound control. This example was definitely an exercise in balancing complexity with flexibility. In general, I hope that our customers find the DataSource and Provider infrastructure as solving more problems than they create. For example, in this case, it's very easy to reuse the same control for a variety of datasources.

Secondly, I agree that many of the samples I've posted touch on fairly difficult topics. I hope this is most useful use of my time for the readers because most of the answers to more straight forward challenges can be found in a lot of places (such as the <a href="http://forums.aps.net">asp.net forums</a>.

Lastly, as part of our testing, we've tried to make sure that ASP.NET is W3C standards compliant and where it isn't is documented or configurable. If you find somewhere this isn't true, it's certainly a bug and we want to know about it.
December 20, 2005 11:45 AM
 

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
 

Danny Chen said:

Sure thing Eric,

The tricky part starts here at this line of code:

Dim text As String = String.Empty
text = DataBinder.GetPropertyValue(data, TextField)

The reason is simple: We don't know what type of data we're bound to but yet we want to be able to read fields from that data. So what this code does under the hood is use reflection to enumerate the fields on the data coming in and tries to find the property on the data which whose name matches TextField.

The next tricky part is the simplified DataBinding, the Eval part. Essentially it's a similar kind of trick except it operates on the DataItem from the container. So: Eval("Text") gets the value of the field called "Text" from the DataItem which is one of the implemented interfaces of MyDataItem.

So, baby steps of part 3:

Add some DataBinder.GetProperties code to get the property value for the UrlField. Assign this value to the instance of MyDataItem in the constructor in the same way that "text" is being assigned.
Then, in your .aspx you can put something like this in the ItemTemplate

<asp:HyperLink runat="server" id="hl1" NavigateUrl='<%# Eval("Url") %>' Text='<%# Eval("Text") %>' />
January 6, 2006 1:58 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
 

Rick.Stavanja.com said:

Another month. Another collection of links... Sports/Packers/Racing Turiaf will play for Yakama Sun Kings

September 14, 2006 4:01 AM
 

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
 

information about real estate said:

Finding the best property web sites is not always simple.

September 22, 2007 6:51 PM
 

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
 

Nguyen's Blog said:

Render the Menu with ul tags

March 4, 2008 8:32 PM
 

SoopahMan’s Dev WordPress » Blog Archive » Rendering a databound UL menu said:

Pingback from  SoopahMan&#8217;s Dev WordPress  &raquo; Blog Archive   &raquo; Rendering a databound UL menu

March 7, 2008 8:27 PM
 

Drew said:

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

March 31, 2008 1:27 PM
 

ASP.NET Menu renderen met UL tags at Superdopey’s Techblog said:

Pingback from  ASP.NET Menu renderen met UL tags at  Superdopey&#8217;s Techblog

August 5, 2008 4:42 AM
 

Tao Designs Weblog » Hierarchical asp:repeater said:

Pingback from  Tao Designs Weblog &raquo; Hierarchical asp:repeater

October 13, 2008 6:22 PM

Leave a Comment

(required)  
(optional)
(required)  
Add