Alright, so here is one solution I came up with.
To recap, this isn't as simple as adding an "active" CSS class to an item if it is selected (as per the default Bootstrap MVC. In this solution we need to identify the parent of and a child and identify both.
Default page is Dashboard. The user then clicks on "Configuration" to expand the menu, then selects the "Websites" link which opens the page.
Here is the solution:
Model:
public class NavigationMenu
{
public string Text { get; set; }
public string Action { get; set; }
public string Controller { get; set; }
public string Icon { get; set; }
public bool Selected { get; set; }
public List<NavigationMenu> MenuChildren;
}
Controller:
public class NavigationController : Controller
{
[ChildActionOnly]
public ActionResult GenerateMenu()
{
List<NavigationMenu> menuItems = new List<NavigationMenu>();
// build the menu
menuItems.Add(new NavigationMenu
{
Text = "Dashboard",
Action = "",
Controller = "Dashboard",
Icon = "icon-home",
Selected = true, // default selected menu item
MenuChildren = null
});
menuItems.Add(new NavigationMenu
{
Text = "Configuration",
Action = null,
Controller = null,
Icon = "icon-cog",
MenuChildren = new List<NavigationMenu>{
new NavigationMenu{
Text = "Websites",
Action = "",
Controller = "Websites",
Icon = null,
MenuChildren = null
},
new NavigationMenu{
Text = "Child 2",
Action = null,
Controller = null,
Icon = null,
MenuChildren = null
}
}
});
menuItems.Add(new NavigationMenu
{
Text = "Item 2",
Action = "",
Controller = "Item2",
Icon = "icon-random",
MenuChildren = null
});
menuItems.Add(new NavigationMenu
{
Text = "Item 3",
Action = "",
Controller = "Item3",
Icon = "icon-certificate",
MenuChildren = null
});
string action = ControllerContext.ParentActionViewContext.RouteData.Values["action"].ToString() == "Index" ? "" : ControllerContext.ParentActionViewContext.RouteData.Values["action"].ToString();
string controller = ControllerContext.ParentActionViewContext.RouteData.Values["controller"].ToString();
foreach (var item in menuItems)
{
if (item.MenuChildren != null)
{
foreach (var cItem in item.MenuChildren)
{
if (cItem.Controller == controller && cItem.Action == action)
{
cItem.Selected = true;
break;
}
else
{
cItem.Selected = false;
}
}
}
if (item.Controller == controller && item.Action == action)
{
item.Selected = true;
break;
}
else
{
item.Selected = false;
}
}
return PartialView("~/Views/Shared/_Navigation.cshtml", menuItems);
}
}
Shared View:
@model IEnumerable<AdminWebsite.Models.NavigationMenu>
@{
var basePath = HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority);
}
<div id="sidebar">
@Html.Raw("<ul>")
@foreach (var item in Model)
{
// if the menu item does not have children then it should be clickable
if (item.MenuChildren == null & item.Selected)
{
<li class="active"><a href="@String.Format("{0}/{1}/{2}", basePath, item.Controller, item.Action)"><i class="@item.Icon"></i> <span>@item.Text</span></a></li>
}
else if (item.MenuChildren == null & !item.Selected)
{
<li><a href="@String.Format("{0}/{1}/{2}", basePath, item.Controller, item.Action)"><i class="@item.Icon"></i> <span>@item.Text</span></a></li>
}
// has children and one of its children is selected
if (item.MenuChildren != null)
{
if (item.MenuChildren.Any(c => c.Selected) == true)
{
<text><li class="submenu active open"></text>
}
else
{
<text><li class="submenu"></text>
}
// sub-menu parent
if (item.MenuChildren != null & item.Selected)
{
<a href="@String.Format("{0}/{1}/{2}", basePath, item.Action, item.Controller)"><i class="@item.Icon"></i> <span>@item.Text</span></a>
}
else if (item.MenuChildren != null & !item.Selected)
{
<a href="@String.Format("{0}/{1}/{2}", basePath, item.Action, item.Controller)"><i class="@item.Icon"></i> <span>@item.Text</span></a>
}
// children
<text><ul></text>
// iterate through children
foreach(var cItem in item.MenuChildren)
{
if (cItem.MenuChildren == null & cItem.Selected)
{
<li class="active"><a href="@String.Format("{0}/{1}/{2}", basePath, cItem.Controller, cItem.Action)"><i class="@cItem.Icon"></i> <span>@cItem.Text</span></a></li>
}
else if (cItem.MenuChildren == null & !cItem.Selected)
{
<li><a href="@String.Format("{0}/{1}/{2}", basePath, cItem.Controller, cItem.Action)"><i class="@cItem.Icon"></i> <span>@cItem.Text</span></a></li>
}
}
@Html.Raw("</ul>");
@Html.Raw("</li>");
}
}
@Html.Raw("</ul>")
</div>
Implementation in the view:
@{Html.RenderAction("GenerateMenu", "Navigation");}
The controller checks if the current action/controller names match one on the menu and if so, set selected = true
. In the partial view, there is some logic to determine the display structure, based on the parent/child relationships, and if a child is selected, so is the parent.
In brief, that's it. I'd like to hear some comments/other examples.