Building A Truly Dynamic Menu - ASP.NET

Table of Contents [Hide/Show]

{outline||<1> - }

Overview

To build a simple hierarchical menu from a database structure, the article found here is good. However, I encountered a problem with the approach for my particular implementation. In my implementation, a sales region was to be determined from the query string, and the navigation menu adjusted accordingly. However, using an XmlDataSource as shown in the above article didn't work. Apparently the XmlDataSource caches its data, because my navigation menu never changed from the first entry to the website in a particular session. This happened even though the underlying data for the XmlDataSource was changed, and the DataBind method was called. Therefore, the solution was adapted as follows. This article shows the complete solution in case the above link ever becomes broken.

Database Code

The database code (Called AppDatabase.Methods.GetMenuItems() below) should return the following columns.

Name Data Type
MenuIDint
Textvarchar
Descriptionvarchar
ParentIDint
Urlvarchar

Application Code

The LoadMenu() method is called typically in the Page_Load() event handler of a master page.

'--------------------------------------------------------------------------------------------------
Private Sub LoadMenu()

    Dim ds As DataSet = AppDatabase.Methods.GetMenuItems(Request.QueryString("region"))

    ds.DataSetName = "Menus"

    ds.Tables(0).TableName = "Menu"

    Dim t As DataTable = ds.Tables("Menu")

    Dim rel As DataRelation = New DataRelation("ParentChild", t.Columns("MenuID"), _
                t.Columns("ParentID"), True)

    rel.Nested = True

    ds.Relations.Add(rel)

    Dim xml As String = ds.GetXml()
    Dim xslFile As String = Server.MapPath("~/Menu.xsl")

    xml = TransformXml(xml, xslFile)

    BuildXmlMenu(uxSiteMenu, xml)

End Sub
'--------------------------------------------------------------------------------------------------
Private Sub BuildXmlMenu(ByVal menuControl As Menu, ByVal xmlData As String)

    Dim xmlDoc As XmlDocument = New XmlDocument()
    xmlDoc.LoadXml(xmlData)

    For Each child As XmlNode In xmlDoc.DocumentElement.ChildNodes

        Dim text As String = GetAttribute(child, "Text")
        Dim url As String = GetAttribute(child, "NavigateUrl")
        Dim childMenu As MenuItem = New MenuItem(text)
        Dim d As Short = childMenu.Depth

        If url.Length > 0 Then
            childMenu.NavigateUrl = url
        End If

        If child.HasChildNodes Then
            BuildXmlMenu(childMenu, child.ChildNodes)
        End If

        menuControl.Items.Add(childMenu)

    Next

End Sub
'--------------------------------------------------------------------------------------------------
Private Sub BuildXmlMenu(ByVal parent As MenuItem, ByVal children As XmlNodeList)

    Dim imax As Integer = children.Count - 1

    For i As Integer = 0 To imax

        Dim child As XmlNode = children(i)
        Dim childMenu As MenuItem = New MenuItem(GetAttribute(child, "Text"))
        Dim url As String = GetAttribute(child, "NavigateUrl")
        If url.Length > 0 Then
            childMenu.NavigateUrl = url
        End If

        If child.HasChildNodes Then
            BuildXmlMenu(childMenu, child.ChildNodes)
        End If

        parent.ChildItems.Add(childMenu)

    Next

End Sub
'--------------------------------------------------------------------------------------------------
Private Function GetAttribute(ByVal node As XmlNode, ByVal child As String) As String

    Dim result As String = ""

    Dim nodes As XmlNodeList = node.SelectNodes("@" & child)

    If nodes.Count = 1 Then
        result = nodes(0).InnerText
    End If

    Return result

End Function
'--------------------------------------------------------------------------------------------------
Public Shared Function TransformXml(ByVal xmlData As String, ByVal xslFile As String) As String

    Dim result As String = ""

    '-- Load XSL Transformation ---------------------------------------------------------------
    Dim xslDoc As XslCompiledTransform = New XslCompiledTransform()
    xslDoc.Load(xslFile)

    '-- Load XML Data -------------------------------------------------------------------------
    Dim sr As StringReader = New StringReader(xmlData)
    Dim xr As XmlTextReader = New XmlTextReader(sr)
    Dim sw As StringWriter = New StringWriter()
    Dim xw As XmlTextWriter = New XmlTextWriter(sw)

    '-- Transform data and return result ------------------------------------------------------
    xslDoc.Transform(xr, xw)
    result = sw.ToString()
    Return result

End Function

ASPX Code

Of course, a minimal amount of ASPX code is required to establish the menu control.

<asp:Menu ID="uxSiteMenu" runat="server" Orientation="Vertical" DisappearAfter="10"
    CssClass="menu" StaticPopOutImageUrl="./images/redarrow.gif" Width="100%" 
    >
    <LevelSubMenuStyles>
        <asp:SubMenuStyle CssClass="menu-submenu" />
    </LevelSubMenuStyles>
    <LevelMenuItemStyles>
        <asp:MenuItemStyle CssClass="menuitem-level1" />
        <asp:MenuItemStyle CssClass="menuitem-level1" />
    </LevelMenuItemStyles>
    <StaticHoverStyle CssClass="menu-hover" />
    <DynamicHoverStyle CssClass="menu-hover" />
</asp:Menu>

Menu.XSL

This XSL is used to transform the XML data into the form required. I find it curious that the original author didn't just generate the data in the form required. However, out of pure laziness, I followed the same approach.

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" indent="yes" encoding="utf-8"/>


    <!-- Replace root node name Menus with MenuItems
       and call MenuListing for its children-->
    <xsl:template match="/Menus">
        <MenuItem>
            <xsl:call-template name="MenuListing" />
        </MenuItem>
    </xsl:template>

    <!-- Allow for recursive child nodeprocessing -->
    <xsl:template name="MenuListing">
        <xsl:apply-templates select="Menu" />
    </xsl:template>

    <xsl:template match="Menu">
        <MenuItem>
            <!-- Convert Menu child elements to MenuItem attributes -->
            <xsl:attribute name="Text">
                <xsl:value-of select="Text"/>
            </xsl:attribute>
            <xsl:attribute name="ToolTip">
                <xsl:value-of select="Description"/>
            </xsl:attribute>
            <xsl:attribute name="NavigateUrl">
                <xsl:value-of select="Url"/>
            </xsl:attribute>

            <!-- Recursively call MenuListing forchild menu nodes -->
            <xsl:if test="count(Menu) >0">
                <xsl:call-template name="MenuListing" />
            </xsl:if>

        </MenuItem>
    </xsl:template>
</xsl:stylesheet>