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