I am just diving into the MVC SiteMapProvider and fiddling around with it, so this question is more about design than not being able to achieve what i want. I set up a simple MVC4 Web Application that uses MvcSiteMapProvider 3.3.4.
What i want to achieve with it is to have a 2 level site navigation; one for the top level that is displayed horizontally and one for the level below that to be displayed vertically. The submenu must only display the nodes that are hierarchically below the currently selected node. Additionally I want to have the active node highlighted in both levels, so the node that is actually selected and if it is a subnode I also want to highlight the parent node.
I was actually able to set things up so the result looks like what I wanted, but I do not really like how it looks like in code, so what I am asking here is whether this is a legit way to do this or if someone could enlighten me about a best practice that I am currently missing.
The hurdles I face to realize this are:
1) I only want to use one sitemap file and display different levels of it in different calls to the MvcSiteMap.Menu function(as said above level 1 is the horizontal one, level 2 is the vertical one, level 0 the sitemap's root node[which i do not want to see])
2) My second problem is that I have the Home-Controller's Index-Action as my root node but also as a subnode of the root node, because I want that extra level of hierarchy to put the other Home-Controller's actions into. Surely this needs some additional configuration to display a proper breadcrumb later on, as it turned out this also makes highlighting the "Home"-Node if it is selected more difficult.
This in mind I set up the following sitemap:
(web.config section)
<siteMap defaultProvider="MvcSiteMapProvider" enabled="true">
<providers>
<clear />
<add name="MvcSiteMapProvider" type="MvcSiteMapProvider.DefaultSiteMapProvider, MvcSiteMapProvider" siteMapFile="~/Mvc.Sitemap" securityTrimmingEnabled="true" cacheDuration="5" enableLocalization="true" scanAssembliesForSiteMapNodes="true" includeAssembliesForScan="" excludeAssembliesForScan="" attributesToIgnore="visibility" nodeKeyGenerator="MvcSiteMapProvider.DefaultNodeKeyGenerator, MvcSiteMapProvider" controllerTypeResolver="MvcSiteMapProvider.DefaultControllerTypeResolver, MvcSiteMapProvider" actionMethodParameterResolver="MvcSiteMapProvider.DefaultActionMethodParameterResolver, MvcSiteMapProvider" aclModule="MvcSiteMapProvider.DefaultAclModule, MvcSiteMapProvider" siteMapNodeUrlResolver="MvcSiteMapProvider.DefaultSiteMapNodeUrlResolver, MvcSiteMapProvider" siteMapNodeVisibilityProvider="MvcSiteMapProvider.FilteredSiteMapNodeVisibilityProvider, MvcSiteMapProvider" siteMapProviderEventHandler="MvcSiteMapProvider.DefaultSiteMapProviderEventHandler, MvcSiteMapProvider" />
</providers>
</siteMap>
(Mvc.Sitemap)
<?xml version="1.0" encoding="utf-8" ?>
<mvcSiteMap xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://mvcsitemap.codeplex.com/schemas/MvcSiteMap-File-3.0"
xsi:schemaLocation="http://mvcsitemap.codeplex.com/schemas/MvcSiteMap-File-3.0 MvcSiteMapSchema.xsd"
enableLocalization="true">
<mvcSiteMapNode title="Home" controller="Home" action="Index">
<mvcSiteMapNode title="Home" controller="Home" action="Index" visibility="!SiteMapPathHelper,*">
<mvcSiteMapNode title="HomeSub" controller="Home" action="Sub">
</mvcSiteMapNode>
</mvcSiteMapNode>
<mvcSiteMapNode title="Menu1" controller="Menu1" action="Index">
<mvcSiteMapNode title="Menu1Sub" controller="Menu1" action="Sub">
</mvcSiteMapNode>
</mvcSiteMapNode>
<mvcSiteMapNode title="Menu2" controller="Menu2" action="Index">
<mvcSiteMapNode title="Menu2Sub" controller="Menu2" action="Sub">
</mvcSiteMapNode>
</mvcSiteMapNode>
</mvcSiteMapNode>
</mvcSiteMap>
(Views\Shared\DisplayTemplates\MenuHelperModel.cshtml)
@model MvcSiteMapProvider.Web.Html.Models.MenuHelperModel
@using System.Web.Mvc.Html
@using MvcSiteMapProvider.Web.Html.Models
<ul id="menu">
@{
string firstUrl = null;
}
@foreach (var node in Model.Nodes) {
string classes = node.IsCurrentNode | node.IsInCurrentPath | ( ((MvcSiteMapProvider.MvcSiteMapNode)SiteMap.CurrentNode).Action == node.Action && ((MvcSiteMapProvider.MvcSiteMapNode)SiteMap.CurrentNode).Controller == node.Controller )
? "current" : "";
if (firstUrl == null)
{
firstUrl = node.Url;
}
else if (node.Url.Contains(firstUrl))
{
classes += " child";
}
<li class="@classes">@Html.DisplayFor(m => node)
@if (node.Children.Any()) {
@Html.DisplayFor(m => node.Children)
}
</li>
}
</ul>
I render the main site menu with this
@Html.MvcSiteMap("MvcSiteMapProvider").Menu(0, true, false, 1)
I read this like "get me the Menu starting from the root node down to the level below that; put the root node into the same level as its children, but do not show me the root node". I don't get this call, because I would expect the same result is returned if I set startingNodeInChildLevel to false and leave showStartingNode as false, but it is not? How can this be written so it is easier to understand?
And my sub menu like this
@Html.MvcSiteMap("MvcSiteMapProvider").Menu(2, 1, true)
To be honest I do not really understand this call either, I just stumbled upon it when I was trying out the different overloads of MvcSiteMap.Menu and it seems to do exactly what I need (reducing the shown nodes to only those under the selected parent node). Someone got a clarification about this what it does exactly?
A breadcrumb is added with
@Html.MvcSiteMap("MvcSiteMapProvider").SiteMapPath()
Now since this seems to work for me, why do I even bother posting this?
Well it's the calls to MvcSiteMap.Menu that surely look confusing to anyone who reads this the first (or maybe the first dozen :-)) times.
The second thing I do not like about this current solution is the visibility setting in the Home-Controller's Index-Action-Node; I put it there so the Home-Node does not appear twice in the breadcrump if I selected the "HomeSub"-Link.
Third thing: is there a better way to determine whether the current node is selected (in the very specific case, that "Home" was selected, since it is also a subnode of "Home"? I added a check there for controller and action equality, because IsCurrentNode seems to compare the keys that the nodes have and those are different unfortunately.
I know this reads like a vague question at first, but I don't want to start a discussion here I want to know whether this is an okay way to do this (or what is the better/easier to read way) and whether someone could explain me those Menu-Calls and why I have to put the visibility-setting the way I did to make this work.
If you want to fiddle around with this, you can download this "complete" sample project here (Visual Studio 2012).