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 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 an accesskey and target attribute with a value to each siteMapNode. See below for example.
Site Map
Listing 1
<siteMapNode>
<siteMapNode url="Default.aspx" title="<u>H</u>ome" description="Home" accesskey="H" />
<siteMapNode url="~/Views/Menu1.aspx" title="<u>M</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 onto the page and then the menu control and wrap the menu control inside a div tag. The details description of each menu property can be found here. Set the staticdisplaylevels ="2" and orientation="Horizontal" to display the menu control in horizontal mode. We can use an inline style sheets or place the CSS style in an external file. In this tutorial, the CSS style is located in 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 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
In the Master Page code behind, include both a MenuItemDataBound and a SiteMapResolve event handler 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);
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 attribute. 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 on 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 key 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 get trigger 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 nd
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, choose add new item --> Web Form and check the Select Master Page checkbox.
Points of Interest
The hover menu does not appears to work on mobile devices. To remedy this problem, I include a TreeView control and set its visible property to false. This control expands its entire node by default. That will take care of the hover menu problem. In the code behind, hide the Menu control and show the TreeView control if requesting browser is a mobile device. See listing 9.
Listing 9
protected void Page_Load(object sender, EventArgs e)
{
if (isMobileBrowser())
{
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 is also not working 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();
}
}
New Update
I have received several complaints from the reader 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 the display:block from the 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 aslo rewrote the logic to detect mobiles browser 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",
"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;
}
Add the code shown below in the Page_Load event.
Listing 13
if (isMobileBrowser())
{
NavigationMenu.Visible = false;
NavigationTreeView.Visible = true;
}
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
Resources
Watch this script in action