Introduction
Breadcrumbs
are often an important aid in user interface, allowing users to know their
position in the site they are navigating.
One of the most common ways of creating a breadcrumb in ASP.NET sites is based on an XML file named web.sitemap, that is located in the site root directory.
The web.sitemap collects site pages in hierarchical order, where each page is referenced by a siteMapNode and siteMapNodes are nested to reproduce the site structure.
In Web Forms there’s a siteMapPath control that lets insert a breadcrumb easily into any page of the site, based on the contents of web.sitemap.
Controls are not allowed in ASP.NET Web Pages, therefore a different solution must be implemented to achieve the same goal.
The Sitemap Helper
Carlos Aguilar Mares, in his blog, suggests a simple solution based on an helper that I have just improved a little adding three optional parameters that set the separators before, after and between the links.
@helper
Sitemap(string prev = "", string sep = " > ", string post = "")
{
@* Helper Syntax:
Sitemap(prev, sep, post)
Sitemap()
Creates and displays a bredcrumb as unordered list based on web.sitemap.
The breadcrumb is preceded by "prev", followed by "post" and with items
divided by "sep".
The default values for parameters are an empty string for "prev" and
"post" and the greater-than sign for "sep".
Parameters
prev: string added before the breadcrumb (optional)
sep: string added between items (optional)
post: string added after the breadcrumb (optional)
*@
@* Retrive current node *@
SiteMapNode currentNode = SiteMap.CurrentNode;
@* Build breadcrumb *@
<nav>
<ul class="siteMap">
<li><span id="prev">@Html.Raw(prev)</span></li>
@* If current node exist in web.sitemap *@
@if (currentNode != null) {
@* Push parent nodes into a stack to reverse them *@
var node = currentNode;
var nodes = new Stack<SiteMapNode>();
while (node.ParentNode != null) {
nodes.Push(node.ParentNode);
node = node.ParentNode;
}
@* Pop parent nodes from the stack *@
while (nodes.Count != 0) {
SiteMapNode n = nodes.Pop();
@* Add a parent node's link to the breadcrumb *@
<li><a href="@n.Url" title="@n.Description">@n.Title</a><span id="sep">@Html.Raw(sep)</span></li>
}
@* Add current node title to the breadcrumb *@
<li><span>@currentNode.Title</span></li>
}
@* If current node don't exist in web.sitemap *@
else {
@* Replace breadcrumb with page title *@
<li><span>@Page.Title</span></li>
}
<li id="post">@Html.Raw(post)</li>
</ul>
</nav>
}
The simple helper proposed by Carlos Aguilar Mares retrieves the current node through the SiteMap.CurrentNode property and reverses its parent nodes pushing them into a stack.
Then it builds the breadcrumb popping each parent node and adding its link to an unordered list.
Another addition that I have made to the original helper it’s the possibility of displaying the title of the page instead of the breadcrumb in case the helper isn’t able of identify the current node.
The breadcrumb look can be defined adding the right styles to the css file, as the example in the following:
.siteMap {
font-size:14px;
color:#888;
display:inline;
}
.siteMap li {
float:left;
list-style-type:none;
padding-left:0px;
border-width:0px;
}
.siteMap span {
font-weight:bold;
}
.siteMap #prev {
color:blue;
font-weight:normal;
}
.siteMap #post {
color:blue;
font-weight:normal;
}
.siteMap #sep {
color:blue;
font-weight:normal;
}
.siteMap a,a.Visited {
color:#888;
text-decoration:none;
}
The appropriate position for the helper call is from the site’s Layout page.
The extension-less problem
ASP.NET 4 applications can handle requests for extensionless URLs in IIS 7.0 and IIS 7.5, so you can access default.cshtml
just typing default
. However, if in web.sitemap the files are reported without the extension, typing their name with the extension doesn’t let recognize the siteMapNode.
This problem can be solved implementing URL rewriting with the Application_BeginRequest event in Global.asax:
void Application_BeginRequest(object sender, EventArgs e)
{
string currentUrl = Request.FilePath.ToLower();
if(currentUrl.Length > 6) {
if ((currentUrl.Substring(currentUrl.Length - 7)) == ".cshtml") {
var originalUrl = currentUrl.Substring(0, currentUrl.Length - 7);
Context.RewritePath(originalUrl);
}
}
}
The code I’ve used is pretty simple: if the actual URL contains the .cshtml extension, the Application_BeginRequest event redirects to the same URL without extension, permitting the identification of the right siteMapNode.