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:
- Include an
accesskey
attribute - Include a
target
attribute - Include a Site Map Path
Figure 1
Getting started
Here is the structure of my project. You are welcome to download the demo.
Figure 2
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;
if (node["target"] != null)
{
e.Item.Target = node["target"];
}
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
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;
}
internal SiteMapNode ReplaceNodeText(SiteMapNode smn)
{
if (smn != null && smn.Title.Contains("<u>"))
{
smn.Title = smn.Title.Replace("<u>",
"").Replace("</u>", "");
}
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()
{
HttpContext context = HttpContext.Current;
if (context.Request.Browser.IsMobileDevice)
{
return true;
}
if (context.Request.ServerVariables["HTTP_X_WAP_PROFILE"] != null)
{
return true;
}
if (context.Request.ServerVariables["HTTP_ACCEPT"] != null &&
context.Request.ServerVariables["HTTP_ACCEPT"].ToLower().Contains("wap"))
{
return true;
}
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