Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / CSS

ASP.NET Horizontal Menu Control

4.72/5 (41 votes)
27 Feb 2010CPOL5 min read 556.6K   10.6K  
An article on how to put together an ASP.NET horizontal menu control with access key and target window support.

Introduction

A few weeks ago, I was working on an ASP.NET web application and needed a simple horizontal menu with a submenu. I decided to use the ASP.NET Menu control, and just drag and drop the control on to the page. Simple enough, but the control does not provide access key and target window support on menu items. I have put together a tutorial on how to:

  1. Include an accesskey attribute
  2. Include a target attribute
  3. Include a Site Map Path
Figure 1

Sample results

Getting started

Here is the structure of my project. You are welcome to download the demo.

Figure 2

Project Structure

Putting everything together

First, add a Site Map to the website project. Open the web.sitemap file and populate it with your navigation data and structures. To underline a certain character of the menu title, we can use the HTML underline tag (<u></u>). In order to parse the XML flawlessly, we must replace the less than sign (<) with & lt; (no spaces). Then, include accesskey and target attributes with a value to each siteMapNode. See below for an example.

Site Map

Listing 1
XML
<siteMapNode>
      <siteMapNode url="Default.aspx" 
         title="& lt;u>H& lt;/u>ome" 
         description="Home" 
         accesskey="H" />
      <siteMapNode url="~/Views/Menu1.aspx" 
                   title="& lt;u>M& lt;/u>enu1"  
                   description="Menu1" accesskey="M" />
        <siteMapNode url="~/Views/Menu2.aspx" 
                     title="M<u>e</u>nu2" 
                     description="Menu2" accesskey="E" />
    
    <siteMapNode url="~/Views/Menu3.aspx" 
                 title="Me<u>n</u>u3" 
                 description="Menu3" 
                 accesskey="N" target="_blank" />
        
    <siteMapNode url="~/Views/Menu4.aspx" 
                 title="Men<u>u</u>4" 
                 description="Menu4" accesskey="U">
      <siteMapNode url="~/Views/Menu4Sub1.aspx" 
                   title="Menu4<u>S</u>ub1" 
                   description="Menu4Sub1" 
                   accesskey="S" />
      <siteMapNode url="~/Views/Menu4Sub2.aspx" 
                   title="Menu4Su<u>b</u>2" 
                   description="Menu4Sub2" 
                   target="_blank" accesskey="B" />
    </siteMapNode>
……
….
  </siteMapNode>
</siteMap>

Master Page

Add a Master Page to the website project. Drag a SiteMapDataSource control on to the page and then the Menu control, and wrap the Menu control inside a div tag. A detailed description of each menu property can be found here. Set staticdisplaylevels ="2" and orientation="Horizontal" to display the menu control in horizontal mode. We can use an inline style sheet, or place the CSS style in an external file. In this tutorial, the CSS style is located in the style.css file. See listing 2.

Listing 2
XML
<asp:SiteMapDataSource id="MenuSource" runat="server" />
<div class="background">
  <asp:menu id="NavigationMenu" CssClass="NavigationMenu"  
        staticdisplaylevels="2" DynamicHorizontalOffset="1"
        staticsubmenuindent="1px" MaximumDynamicDisplayLevels="4"
        orientation="Horizontal"   
        DynamicPopOutImageUrl="~/Images/right-arrow.gif" 
        StaticPopOutImageUrl="~/Images/drop-arrow.gif"
        datasourceid="MenuSource"    
        runat="server" Height="30px">

        <staticmenuitemstyle ItemSpacing="10" 
                    CssClass="staticMenuItemStyle"/>
        <statichoverstyle CssClass="staticHoverStyle" />
       <StaticSelectedStyle CssClass="staticMenuItemSelectedStyle"/> 
        <DynamicMenuItemStyle CssClass="dynamicMenuItemStyle" />      
        <dynamichoverstyle CssClass="menuItemMouseOver" />
        <DynamicMenuStyle CssClass="menuItem" />
       <DynamicSelectedStyle CssClass="menuItemSelected" />
     
       <DataBindings>        
             <asp:MenuItemBinding DataMember="siteMapNode" 
                    NavigateUrlField="url" TextField="title"  
                    ToolTipField="description" />
        </DataBindings>

      </asp:menu>
</div>

Drag a SiteMapPath control on to the page. The purpose of this control is to display a navigation path that shows the user the current page location. See listing 3.

Listing 3
XML
<div id="e">
       <asp:SiteMapPath ID="SiteMapPath1" runat="server" 
                RenderCurrentNodeAsLink="true" 
                CssClass="currentNodeStyle"
            PathSeparator=" >> ">
            <PathSeparatorStyle ForeColor="#5D7B9D" CssClass="currentNodeStyle" />
            <CurrentNodeStyle ForeColor="#333333" CssClass="currentNodeStyle" />
            <NodeStyle ForeColor="#7C6F57"  CssClass="currentNodeStyle"  />
            <RootNodeStyle  ForeColor="#5D7B9D" CssClass="currentNodeStyle"  />
    </asp:SiteMapPath> 
</div>

Master Page code-behind

Include the MenuItemDataBound and SiteMapResolve event handlers on the Page_Load event. The purpose of the former event is to insert the target attribute value and create an access key for the menu item before it is rendered or displayed in a Menu control. The latter event is to modify the text displayed by the SiteMapPath control.

Listing 4
C#
NavigationMenu.MenuItemDataBound += 
     new MenuEventHandler(NavigationMenu_MenuItemDataBound);
SiteMap.SiteMapResolve += 
     new SiteMapResolveEventHandler(SiteMap_SiteMapResolve);

Shown below is the implementation of the NavigationMenu_MenuItemDataBound method. The MenuItemDataBound event occurs when a menu item in a Menu control is bound to data. That being said, it will loop through each siteMapNode and look for the accesskey and target attributes. There is a target property associated with the menu item, and we can set its target window with the target attribute value. See listing 5.

Listing 5
C#
void NavigationMenu_MenuItemDataBound(object sender, MenuEventArgs e)
{
    SiteMapNode node = (SiteMapNode)e.Item.DataItem;
   
    //set the target of the navigation menu item (blank, self, etc...)
    if (node["target"] != null)
    {
        e.Item.Target = node["target"];
    }
    //create access key button
    if (node["accesskey"] != null)
    {
        CreateAccessKeyButton(node["accesskey"] as string, node.Url);
    }
}

To get the access key to work, add a Panel control to the master page and a JavaScript function to redirect the webpage to the one that is specified. See below.

Listing 6
HTML
<asp:Panel ID="AccessKeyPanel" runat="server" />
<script type="text/javascript">
 function navigateTo(url) {
    window.location = url;
 }
</script>

Below is the implementation of the CreateAccessKeyButton method. Create an HtmlButton control dynamically and attach an onclick event to it. Set the style.left property to -2555px to hide the control. A complete list of access keys in different browsers is available here.

Listing 7
C#
//create access key button
void CreateAccessKeyButton(string ak, string url)
{
    HtmlButton inputBtn = new HtmlButton();
    inputBtn.Style.Add("width", "1px");
    inputBtn.Style.Add("height", "1px");
    inputBtn.Style.Add("position", "absolute");
    inputBtn.Style.Add("left", "-2555px");
    inputBtn.Style.Add("z-index", "-1");
    inputBtn.Attributes.Add("type", "button");
    inputBtn.Attributes.Add("value", "");
    inputBtn.Attributes.Add("accesskey", ak);
    inputBtn.Attributes.Add("onclick", "navigateTo('" + url + "');");

    AccessKeyPanel.Controls.Add(inputBtn);
}

The SiteMap.SiteMapResolve event gets triggered when the CurrentNode property is accessed. It will call the ReplaceNodeText method recursively and replace the HTML underline tag. See listing 8.

Listing 8
C#
SiteMapNode SiteMap_SiteMapResolve(object sender, SiteMapResolveEventArgs e)
{
   if (SiteMap.CurrentNode != null)
        {
            SiteMapNode currentNode = SiteMap.CurrentNode.Clone(true);
            SiteMapNode tempNode = currentNode;
            tempNode = ReplaceNodeText(tempNode);

            return currentNode;
        }

        return null;
}

//remove <u></u> tag recursively
internal SiteMapNode ReplaceNodeText(SiteMapNode smn)
{
    //current node
    if (smn != null && smn.Title.Contains("<u>"))
    {
        smn.Title = smn.Title.Replace("<u>", 
                       "").Replace("</u>", "");
    }

    //parent node
    if (smn.ParentNode != null)
    {
        if (smn.ParentNode.Title.Contains("<u>"))
        {
            SiteMapNode gpn = smn.ParentNode;
            smn.ParentNode.Title = smn.ParentNode.Title.Replace(
              "<u>", "").Replace("</u>", "");
            smn = ReplaceNodeText(gpn);
        }
    }
    return smn;
}

Using the code

Since the menu is in the Master Page, right click the website project, Add New Item, Web Form, and check the Select Master Page checkbox.

Points of interest

The hover menu appears to not work on mobile devices. To remedy this problem, I included a TreeView control and set its Visible property to false. This control expands its entire node by default. That will take care of the above mentioned problem. In the code-behind, hide the Menu control and show the TreeView control if the requesting browser is a mobile device. See listing 9.

Listing 9
C#
protected void Page_Load(object sender, EventArgs e)
{
    if (Request.Browser.IsMobileDevice)
    {
        NavigationMenu.Visible = false;
        NavigationTreeView.Visible = true;
    }
}

When I tested the menu on IE 8, the hover menu did not render correctly. To overcome this problem, I set the DynamicMenuStyle z-index to 200, see style.css. The submenu does not work with Google Chrome. After some research, I found the solution for it. See listing 10.

Listing 10
C#
protected void Page_Load(object sender, EventArgs e)
{
    if (Request.UserAgent.IndexOf("AppleWebKit") > 0)
    {
        Request.Browser.Adapters.Clear();
        NavigationMenu.DynamicMenuStyle.Width = Unit.Pixel(120);
    }
}

New update

I have received several complaints from readers concerning the menu control not displaying correctly on Safari and Google Chrome browsers. Somehow, the menu items are stacked on each other and the submenu widths are gapped apart. After doing some research, I found the answer here, see listing 11. To fix the submenu width, remove display:block from dynamicMenuItemStyle in the CSS file.

Listing 11
C#
protected override void AddedControl(Control control, int index)
{
    if (Request.ServerVariables["http_user_agent"].IndexOf("Safari",
        StringComparison.CurrentCultureIgnoreCase) != -1)
        this.Page.ClientTarget = "uplevel";

    base.AddedControl(control, index);
}

I also rewrote the logic to detect mobile browsers with the code from Vincent Van Zyl. See listing 12.

Listing 12
C#
public static readonly string[] mobiles =
      new[]
            {
                "midp", "j2me", "avant", "docomo", 
                "novarra", "palmos", "palmsource", 
                "240x320", "opwv", "chtml",
                "pda", "windows ce", "mmp/", 
                "blackberry", "mib/", "symbian", 
                "wireless", "nokia", "hand", "mobi",
                "phone", "cdm", "up.b", "audio", 
                "SIE-", "SEC-", "samsung", "HTC", 
                "mot-", "mitsu", "sagem", "sony"
                , "alcatel", "lg", "eric", "vx", 
                "NEC", "philips", "mmm", "xx", 
                "panasonic", "sharp", "wap", "sch",
                "rover", "pocket", "benq", "java", 
                "pt", "pg", "vox", "amoi", 
                "bird", "compal", "kg", "voda",
                "sany", "kdd", "dbt", "sendo", 
                "sgh", "gradi", "jb", "dddi", 
                "moto", "iphone"
            };

public static bool isMobileBrowser()
{
    //GETS THE CURRENT USER CONTEXT
    HttpContext context = HttpContext.Current;

    //FIRST TRY BUILT IN ASP.NT CHECK
    if (context.Request.Browser.IsMobileDevice)
    {
        return true;
    }
    //THEN TRY CHECKING FOR THE HTTP_X_WAP_PROFILE HEADER
    if (context.Request.ServerVariables["HTTP_X_WAP_PROFILE"] != null)
    {
        return true;
    }
    //THEN TRY CHECKING THAT HTTP_ACCEPT EXISTS AND CONTAINS WAP
    if (context.Request.ServerVariables["HTTP_ACCEPT"] != null &&
        context.Request.ServerVariables["HTTP_ACCEPT"].ToLower().Contains("wap"))
    {
        return true;
    }
    //AND FINALLY CHECK THE HTTP_USER_AGENT 
    //HEADER VARIABLE FOR ANY ONE OF THE FOLLOWING
    if (context.Request.ServerVariables["HTTP_USER_AGENT"] != null)
    {
        for (int i = 0; i < mobiles.Length; i++)
        {
            if (context.Request.ServerVariables["HTTP_USER_AGENT"].
                                                ToLower().Contains(
                                                    mobiles[i].ToLower()))
            {
                return true;
            }
        }
    }

    return false;
}

Conclusion

If you find any bugs or disagree with the contents, please drop me a line and I'll work with you to correct it.

Tested on IE 6.0/7.0/8.0, Google Chrome, Safari, and Firefox.

History

  • 02/25/2010 - Removed PathSeparator of SiteMapPath before the root node, as suggested by The Code Project member, kentex2000. Added SiteMap.CurrentNode != null to the SiteMap_SiteMapResolve method.
  • 02/03/2010 - Fixed the menu display problem in Safari and Google Chrome browsers, added new logic to detect mobile browsers.

Resources

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)