Question

I have seen several examples on creating a HTML helper method for active menu items.

**Summary:** Simply put, in an MVC project, using the Twitter Bootstrap, I am trying to preserve the open state of a collapsible menu when a child is selected.

I am using a collapsible menu, where the parent's css (the selected item) needs to include active open if a child is selected. This will ensure that the menu is open at the right location. With the use of another HTML helper, the active item is already set to active.

HTML for the menu:

    <div id="sidebar">
        <ul>
            <li class="active"><a href="dashboard.html"><i class="icon-home"></i> <span>Dashboard</span></a></li>
            <li class="submenu">
                <a href="#"><i class="icon-beaker"></i> <span>UI Lab</span> <i class="arrow icon-chevron-right"></i></a>
                <ul>
                    <li><a href="interface.html">Interface Elements</a></li>
                    <li><a href="jquery-ui.html">jQuery UI</a></li>
                    <li><a href="buttons.html">Buttons &amp; icons</a></li>
                </ul>
            </li>
            <li class="submenu">
                <a href="#"><i class="icon-th-list"></i> <span>Form elements</span> <i class="arrow icon-chevron-right"></i></a>
                <ul>
                    <li><a href="form-common.html">Common elements</a></li>
                    <li><a href="form-validation.html">Validation</a></li>
                    <li><a href="form-wizard.html">Wizard</a></li>
                </ul>
            </li>
            <li><a href="tables.html"><i class="icon-th"></i> <span>Tables</span></a></li>
            <li><a href="grid.html"><i class="icon-th-list"></i> <span>Grid Layout</span></a></li>
            <li class="submenu">
                <a href="#"><i class="icon-file"></i> <span>Sample pages</span> <i class="arrow icon-chevron-right"></i></a>
                <ul>
                    <li><a href="invoice.html">Invoice</a></li>
                    <li><a href="chat.html">Support chat</a></li>
                    <li><a href="calendar.html">Calendar</a></li>
                    <li><a href="gallery.html">Gallery</a></li>
                    <li><a href="messages.html">Messages</a></li>
                </ul>
            </li>
            <li>
                <a href="charts.html"><i class="icon-signal"></i> <span>Charts &amp; graphs</span></a>
            </li>
            <li>
                <a href="widgets.html"><i class="icon-inbox"></i> <span>Widgets</span></a>
            </li>
        </ul>

    </div>

Here is the helper method for items:

    public static MvcHtmlString MenuItem(this HtmlHelper htmlHelper,
        string text,
        string action,
        string controller,
        string iconClass)
    {
        var li = new TagBuilder("li");
        var routeData = htmlHelper.ViewContext.RouteData;
        var currentAction = routeData.GetRequiredString("action");
        var currentController = routeData.GetRequiredString("controller");
        if (string.Equals(currentAction, action, StringComparison.OrdinalIgnoreCase) &&
            string.Equals(currentController, controller, StringComparison.OrdinalIgnoreCase))
        {
            li.AddCssClass("active");
        }

        var i = new TagBuilder("i");
        i.AddCssClass(iconClass);

        var basePath = HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority);

        //li.InnerHtml = htmlHelper.ActionLink("<i>something</i>" + text, action, controller).ToHtmlString();
        li.InnerHtml = htmlHelper.Raw(string.Format("<a href=\"{0}/{1}/{2}\"><i class=\"{4}\"></i>{3}</a>", basePath, controller, action, text, iconClass)).ToString();

        return MvcHtmlString.Create(li.ToString());
    }

And implemented like this:

        <div id="sidebar">
            <ul>
                @Html.MenuItem("Dashboard", "Index", "Dashboard", "icon-home")
@*              <li class="active"><a href="dashboard.html"><i class="icon-home"></i> <span>Dashboard</span></a></li>*@
                <li class="submenu">
                    <a href="#"><i class="icon-beaker"></i> <span>UI Lab</span> <i class="arrow icon-chevron-right"></i></a>

                    <ul>
                        <li>@Html.MenuItem("Websites", "Index", "Websites", null)</li>
                        <li><a href="jquery-ui.html">jQuery UI</a></li>
                        <li><a href="buttons.html">Buttons &amp; icons</a></li>
                    </ul>
                </li>
                <li class="submenu">
                    <a href="#"><i class="icon-th-list"></i> <span>Form elements</span> <i class="arrow icon-chevron-right"></i></a>
                    <ul>
                        <li><a href="form-common.html">Common elements</a></li>
                        <li><a href="form-validation.html">Validation</a></li>
                        <li><a href="form-wizard.html">Wizard</a></li>
                    </ul>
                </li>

So what I don't have is something for the submenu items.

Is there a simpler way of trying to accomplish this?

--UPDATE--

I'm guessing that putting this into a partial view may be best. I need to find some way to preserve the selected item on click to reference it on every menu item, rather than check if the controller/action matches in order to set the current item to "active". A controller method that activates on click, checks if the currently selected item is a parent or child, and if the parent matches a child, then format differently? I'm sure there has to be an easier way.

Was it helpful?

Solution

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.

enter image description here

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.

OTHER TIPS

Here is a solution using most of the code from the accepted answer, refactored to use HtmlHelpers and TagBuilders with a little renaming to fit my project.

Model:

public class MenuViewModel
{
    public IList<MenuItemDto> MenuItems; 
}

public class MenuItemDto
{
    public string Text { get; set; }
    public string Action { get; set; }
    public string Controller { get; set; }
    public string IconCssClass { get; set; }
    public bool Active { get; set; }

    public List<MenuItemDto> MenuChildren;
}

Controller:

public ActionResult GenerateMenu()
{
    var viewModel = new MenuViewModel();
    viewModel.MenuItems = //code to build menu model like ElHaix provided in his Controller;

    return PartialView("~/Views/Shared/_Menu.cshtml", viewModel);
}

Shared View:

@using Extensions

<div id="sidebar">
    @Html.Raw("<ul>")

    @foreach (var item in Model.MenuItems)
    {
        // if the menu item does not have     children then it should be clickable
        if (item.MenuChildren == null)
        {
            @Html.LiForMenuItem(item)
        }

        // has children and one of its children     is selected
        if (item.MenuChildren != null)
        {
            if (item.MenuChildren.Any(c => c.    Active) == true)
            {
                <text><li class="submenu active     open">
                </text>
            }
            else
            {
                <text>
                <li class="submenu">
                </text>
            }

            // sub-menu parent
            if (item.MenuChildren != null)
            {
                @Html.HrefForSubMenuItemRoot(    item)
            }

            // children
            <text><ul>
            </text>
            // iterate through children
            foreach (var cItem in item.    MenuChildren)
            {
                if (cItem.MenuChildren == null)
                {
                    @Html.LiForMenuItem(cItem)
                }
            }
            @Html.Raw("</ul>");

        @Html.Raw("</li>");
        }

    }
    @Html.Raw("</ul>")
</div>

Html Helpers:

namespace Extensions
{
    public static class MenuExtensions
    {
        public static MvcHtmlString LiForMenuItem(this HtmlHelper htmlHelper, MenuItemDto menuItem)
        {
            var li = new TagBuilder("li");
            AddActiveCssClassToTag(menuItem, li);
            var contentUrl = GenerateContentUrlFromHttpContext(htmlHelper);
            li.InnerHtml = GenerateLinkForMenuItem(menuItem, contentUrl);
            return MvcHtmlString.Create(li.ToString());
        }

        public static MvcHtmlString HrefForSubMenuItemRoot(this HtmlHelper htmlHelper, MenuItemDto menuItem)
        {
            var a = new TagBuilder("a");
            AddActiveCssClassToTag(menuItem, a);
            var contentUrl = GenerateContentUrlFromHttpContext(htmlHelper);
            a.Attributes.Add("href", GenerateUrlForMenuItem(menuItem, contentUrl));
            a.InnerHtml = GenerateInnerHtmlForMenuItem(menuItem);
            return MvcHtmlString.Create(a.ToString());
        }

        private static void AddActiveCssClassToTag(MenuItemDto menuItem, TagBuilder tag)
        {
            if (menuItem.Active)
            {
                tag.AddCssClass("active");
            }
        }

        private static string GenerateContentUrlFromHttpContext(HtmlHelper htmlHelper)
        {
            return UrlHelper.GenerateContentUrl("~/", htmlHelper.ViewContext.HttpContext);
        }

        private static string GenerateLinkForMenuItem(MenuItemDto menuItem, string contentUrl)
        {
            var a = new TagBuilder("a");
            a.Attributes.Add("href", GenerateUrlForMenuItem(menuItem, contentUrl));
            a.InnerHtml = GenerateInnerHtmlForMenuItem(menuItem);
            return a.ToString();
        }

        private static string GenerateInnerHtmlForMenuItem(MenuItemDto menuItem)
        {
            var html = string.Empty;

            //Add <i></i> if there is an IconCssClass present
            var i = new TagBuilder("i");
            if (!String.IsNullOrEmpty(menuItem.IconCssClass))
            {
                i.AddCssClass(menuItem.IconCssClass);
                html += i.ToString();
            }

            //add a span for the text of the menuItem
            var span = new TagBuilder("span");
            span.InnerHtml = menuItem.Text;

            html += span.ToString();

            return html;
        }

        private static string GenerateUrlForMenuItem(MenuItemDto menuItem, string contentUrl)
        {
            var url = contentUrl + menuItem.Controller;
            if (!String.IsNullOrEmpty(menuItem.Action)) url += "/" + menuItem.Action;
            return url;
        }
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top