Click here to Skip to main content
Click here to Skip to main content
Go to top

ASP.NET Horizontal Menu Control

, 27 Feb 2010
Rate this:
Please Sign up or sign in to vote.
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
<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
<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
<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
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
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
<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
//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
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
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
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
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
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)

Share

About the Author

Bryian Tan
Software Developer
United States United States
I have over three years of experience working with Microsoft technologies. I have earned my Microsoft Certified Technology Specialist (MCTS) certification. I'm a highly motivated self-starter with an aptitude for learning new skills quickly.

Comments and Discussions

 
QuestionREG:Hover Menu Control PinmemberMember 1067595917-Mar-14 0:04 
QuestionLeading Separator Before Root Node PinmemberMember 81920506-Dec-13 3:59 
Questionhelp me fix this error tks, Pinmemberitlinh11-May-13 0:11 
I created new project completely and add yor code to , I got this errors in master page file when run My prj,
"the name navigation mennu does not exists in the current context "
"the name navigation treeview does not exists in the current context "
"the name sitemappath1 does not exists in the current context "
even though i have search for this error but now i coud not fix it ( i haven't chane anything in your code yet,and I don't declare anything out of namespace)
can u help me
tks
LL
AnswerRe: help me fix this error tks, PinmemberBryian Tan12-May-13 12:22 
GeneralRe: help me fix this error tks, Pinmemberitlinh16-May-13 8:03 
QuestionMouseover Tabs Menu PinmemberMS Babu10-May-13 20:12 
QuestionContent for page: Default.aspx PinmemberPowerplanner13-Feb-13 9:09 
AnswerRe: Content for page: Default.aspx PinmemberBryian Tan19-Feb-13 14:50 
Questionabout this article Pinmembershenbagam126-Jan-13 22:28 
AnswerRe: about this article PinmemberBryian Tan19-Feb-13 14:52 
QuestionNice Article... PinmemberTripati Balaji16-Nov-12 0:17 
GeneralRe: Nice Article... PinmemberBryian Tan19-Feb-13 14:53 
Questionthanks PinmemberabiFarrah15-Oct-12 16:07 
GeneralRe: thanks PinmemberBryian Tan19-Feb-13 14:54 
GeneralMy vote of 5 PinmemberMember 93931171-Oct-12 20:34 
GeneralRe: My vote of 5 PinmemberBryian Tan11-Dec-12 4:06 
GeneralHow do you hide submenu items in MaterPage from Content Page? [modified] Pinmemberavt2k617-Jul-12 8:11 
GeneralRe: How do you hide submenu items in MaterPage from Content Page? PinmemberBryian Tan17-Jul-12 14:29 
GeneralRe: How do you hide submenu items in MaterPage from Content Page? Pinmemberavt2k617-Jul-12 15:29 
GeneralRe: How do you hide submenu items in MaterPage from Content Page? PinmemberBryian Tan17-Jul-12 16:36 
SuggestionRe: How do you hide submenu items in MaterPage from Content Page? PinmemberBryian Tan17-Jul-12 16:43 
GeneralRe: How do you hide submenu items in MaterPage from Content Page? [modified] Pinmemberavt2k618-Jul-12 3:40 
GeneralRe: How do you hide submenu items in MaterPage from Content Page? PinmemberBryian Tan19-Jul-12 5:29 
GeneralRe: How do you hide submenu items in MaterPage from Content Page? [modified] Pinmemberavt2k619-Jul-12 8:29 
QuestionObject reference not set to an instance of an object after integrate with authentication mode Pinmemberwong0630214-May-12 21:58 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140921.1 | Last Updated 27 Feb 2010
Article Copyright 2009 by Bryian Tan
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid