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