Load and render a dynamic site menu in an ASP.NET MVC 3 application

Posted on November 7th, 2011

Building a web site or application with support for a main menu of links that can be dynamically edited over time presents a few challenges on both the programming and the design fronts. If we are working on a site in which a user can modify links, maybe via a content management system that allows the addition of content pages or external links, code is required for building out the link structure and rendering the markup. Within the context of an ASP.NET MVC 3 web application we can write some model code, an HtmlHelper method, and some base controller logic to handle a dynamic menu build out and rendering.

Our sample solution will focus on working with menu link data after it has been pulled from some persistent data source. To build support for a dynamic menu of links we need to think about how to handle sorted links, nested sub menus, and semantic markup. We will utilize an interface to define the "requirements" of a menu link entry that our sample code is able to work with, allowing the implementation of the menu link data pull to be built out separate as needed. A HtmlHelper static method will be used to render the semantic markup and will leverage some class code to handle recursion for nested sub menus. Finally, a base controller that can be inherited by all controllers responsible for serving up content containing the menu in the layout will be used to wire up the menu loading and delivery to the layout View for rendering with the HtmlHelper.

Providing menu entry structure rules

Let's begin with modeling the data needed for a site menu link. A typical site menu consists of top level links with nested child links. Most links navigate to a relative location on the site, but some may link out to an external site. Those external links may need to open in a new browser window or tab. All links at all levels will need a sort order. Armed with this information, an interface can be written to define what is expected of a site link object.

namespace Mvc3DynamicMenu.Models
{
    public interface ISiteLink
    {
        int Id { get; }
        int ParentId { get; }
        string Text { get; }
        string Url { get; }
        bool OpenInNewWindow { get; }
        int SortOrder { get; }
    }
}

The Id will allow for a unique identifier for the link and the ParentId will be used to reference where to nest the link. The Text will be used for the readable text of the link. The OpenInNewWindow will indicate whether or not the click event should trigger the link to open in a new window/tab. The interface uses a boolean here to keep the UX implementation separate from the model (instead of storing a string with a HTML anchor tag target attribute value like _blank). This way the code that builds the markup can identify if the link should OpenInNewWindow and handle the markup accordingly.

This interface will allow the rest of our code to work off of a single list of all menu link entries. We do not need the link objects to keep track of their child links within the object graph. We also have the flexibility of working with different data storage structures. Maybe we want to handle the rendering of a site menu from a back end that is already written. All we need to do is write some code to query that link data and load it into class objects that implement our interface and add them to a List.

Support logic for rendering the site menu

When engineering the render code for the dynamic menu we are going to need to handle some data checks/lookups as well as some recursion for the nested links. The following three bits of logic are required before we do any recursion or rendering:

  • Determine the top level parent id to know where to start the menu tree.
  • Check to see if an ISiteLink has any child (nested) links.
  • Get the ISiteLink objects in the list that are children of a specific ISiteLink.

We can use a static helper class and some LINQ to handle this logic:

using System.Collections.Generic;
using System.Linq;

namespace Mvc3DynamicMenu.Models
{
    public static class SiteLinkListHelper
    {
        public static int GetTopLevelParentId(IEnumerable siteLinks)
        {
            return siteLinks.OrderBy(i => i.ParentId).Select(i => i.ParentId).FirstOrDefault();
        }

        public static bool SiteLinkHasChildren(IEnumerable siteLinks, int id)
        {
            return siteLinks.Any(i => i.ParentId == id);
        }

        public static IEnumerable GetChildSiteLinks(IEnumerable siteLinks,
            int parentIdForChildren)
        {
            return siteLinks.Where(i => i.ParentId == parentIdForChildren)
                .OrderBy(i => i.SortOrder).ThenBy(i => i.Text);
        }
    }
}

All of these methods are designed to take in a reference to the full site link list. LINQ can be used to query the full list for the data needed in each method.

The GetTopLevelParentId method orders the ISiteLink objects by ParentId from lowest to highest, projects just the ParentId into the result using the Select method, and gets a single result. This method is used to determine the top level of the menu tree. Typically we would use a 0 for the ParentId of a site link entry to indicate that it is at the top level. Using this method gives us a bit of flexibility to not have to know what value is used for that top level indicator. If our site menu entries use a ParentId of 1 to indicate the link is at the top level then this method will return that value and allow our menu build out logic to work without it requiring it to have knowledge of what ParentId to start with. Our site link build out will always work off the list provided to it without expecting the list to adhere to the logic of the build out code.

The SiteLinkHasChildren method uses a site link id to check if Any of the site links in the list have a ParentId equal to the id in question and returns true if any exist, otherwise it returns false. This method is used to do a check for nested child site links for a particular link before running another recursion call.

Update: I refactored this method to use the Any method as suggested by Marius Shulz.

The GetChildSiteLinks method retrieves all ISiteLink objects that have a ParentId equal to the id passed in. This method handles sorting the site links by their SortOrder value as well as their Text value after that (to add a second layer of sorting in case there are links with the same sort order value).

Rendering markup with recursion

A HtmlHelper extension method will be used to render the site menu markup using some recursion and our SiteLinkListHelper class methods.

using System.Collections.Generic;
using System.Web.Mvc;

namespace Mvc3DynamicMenu.Models
{
    public static class HtmlHelperSiteMenu
    {
        public static MvcHtmlString SiteMenuAsUnorderedList(this HtmlHelper helper, List siteLinks)
        {
            if (siteLinks == null || siteLinks.Count == 0)
                return MvcHtmlString.Empty;
            var topLevelParentId = SiteLinkListHelper.GetTopLevelParentId(siteLinks);
            return MvcHtmlString.Create(buildMenuItems(siteLinks, topLevelParentId));
        }

        private static string buildMenuItems(List siteLinks, int parentId)
        {
            var parentTag = new TagBuilder("ul");
            var childSiteLinks = SiteLinkListHelper.GetChildSiteLinks(siteLinks, parentId);
            foreach (var siteLink in childSiteLinks)
            {
                var itemTag = new TagBuilder("li");
                var anchorTag = new TagBuilder("a");
                anchorTag.MergeAttribute("href", siteLink.Url);
                anchorTag.SetInnerText(siteLink.Text);
                if(siteLink.OpenInNewWindow)
                {
                    anchorTag.MergeAttribute("target", "_blank");
                }
                itemTag.InnerHtml = anchorTag.ToString();
                if (SiteLinkListHelper.SiteLinkHasChildren(siteLinks, siteLink.Id))
                {
                    itemTag.InnerHtml += buildMenuItems(siteLinks, siteLink.Id);
                }
                parentTag.InnerHtml += itemTag;
            }
            return parentTag.ToString();
        }
    }
}

The extension method SiteMenuAsUnorderedList takes in the full list of ISiteLink objects and builds out the semantic markup for an unordered list with nested unordered lists for sub menus. This method makes the initial call to the buildMenuItems method to build the sub-menu items of the top level parent id based on the lookup from the SiteLinkListHelper.GetTopLevelParentId method.

The buildMenuItems crafts the ul tag for the particular menu level, queries the child links from the site link list based on the parent id sent in, then walks through each link and creates a li tag and an anchor tag for the actual menu link. The code to build the anchor tag includes a check of the ISiteLink.OpenInNewWindow property and adds the target attribute to the anchor tag if needed. Next, it runs a check on the site link item to see if it has any children. If so, it makes the recursive call to itself, which handles building out its sub menu items (and from there it's turtles all the way down). When the recursion has completed the results are appended to the current menu level ul tag. Finally, the ul tag is returned from the method.

Implementing the interface with a model class

To get our sample code rendering some content we need to create a model class that implements the ISiteLink interface and build out a list of those objects with some sample data. An example of a model class that could be used is as follows:

namespace Mvc3DynamicMenu.Models
{
    public class SiteMenuItem : ISiteLink
    {
        public int Id { get; set; }
        public int ParentId { get; set; }
        public string Text { get; set; }
        public string Url { get; set; }
        public bool OpenInNewWindow { get; set; }
        public int SortOrder { get; set; }
    }
}

A class named SiteMenuManager can be written to handle the logic of loading some sample site menu items.

using System.Collections.Generic;

namespace Mvc3DynamicMenu.Models
{
    public class SiteMenuManager
    {
        public List GetSitemMenuItems()
        {
            var items = new List();
            // Top Level
            items.Add(new SiteMenuItem { Id = 1, ParentId = 0, Text = "Home", 
                Url = "/", OpenInNewWindow = false, SortOrder = 0 });
            items.Add(new SiteMenuItem { Id = 2, ParentId = 0, Text = "Services", 
                Url = "/Services", OpenInNewWindow = false, SortOrder = 2 });
            items.Add(new SiteMenuItem { Id = 3, ParentId = 0, Text = "Contact Us", 
                Url = "/Contact-Us", OpenInNewWindow = false, SortOrder = 1 });
            items.Add(new SiteMenuItem { Id = 4, ParentId = 0, Text = "Our Blog", 
                Url = "https://iwantmymvc.com", OpenInNewWindow = true, SortOrder = 3 });
            // Contact Us Children
            items.Add(new SiteMenuItem { Id = 5, ParentId = 3, Text = "Phone Numbers", 
                Url = "/Contact-Us/Phone-Numbers", OpenInNewWindow = false, SortOrder = 0 });
            items.Add(new SiteMenuItem { Id = 6, ParentId = 3, Text = "Map", 
                Url = "/Contact-Us/Map", OpenInNewWindow = false, SortOrder = 1 });
            // Services Children
            items.Add(new SiteMenuItem { Id = 7, ParentId = 2, Text = "Technical Writing", 
                Url = "/Services/Tech-Writing", OpenInNewWindow = false, SortOrder = 0 });
            items.Add(new SiteMenuItem { Id = 8, ParentId = 2, Text = "Consulting", 
                Url = "/Services/Consulting", OpenInNewWindow = false, SortOrder = 1 });
            items.Add(new SiteMenuItem { Id = 9, ParentId = 2, Text = "Training", 
                Url = "/Services/Training", OpenInNewWindow = false, SortOrder = 2 });
            // Services/TechnicalWriting Children
            items.Add(new SiteMenuItem { Id = 10, ParentId = 7, Text = "Blog Posting", 
                Url = "/Services/Tech-Writing/Blogs", OpenInNewWindow = false, SortOrder = 0 });
            items.Add(new SiteMenuItem { Id = 11, ParentId = 7, Text = "Books", 
                Url = "/Services/Tech-Writing/Books", OpenInNewWindow = false, SortOrder = 1 });

            return items;
        }
    }
}

These two classes could be replaced with other code specific to the backing data source or application used to provide us with the site link data.

Controlling the content

Since we are working with a main site menu we need to have the site links list available to all controller action methods that will use the main layout. One approach to this would be to create a base controller class named BaseController to handle the load of the site menu items and store them in the ViewBag object whenever an action is executed so the layout view(s) can access and render them.

using System.Linq;
using System.Web.Mvc;
using Mvc3DynamicMenu.Models;

namespace Mvc3DynamicMenu.Controllers
{
    public class BaseController : Controller
    {
        protected override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var siteMenuManager = new SiteMenuManager();
            ViewBag.SiteLinks = siteMenuManager.GetSitemMenuItems().ToList();
            base.OnActionExecuting(filterContext);
        }
    }
}

As long as the other controllers inherit from BaseController then the site menu items will be available in the ViewBag.

using System.Web.Mvc;

namespace Mvc3DynamicMenu.Controllers
{
    public class HomeController : BaseController
    {
         public ActionResult Index()
         {
             return View();
         }
    }
}

Assuming we use the stock _Layout.cshtml file for our site shell, it can contain a using statement to reference the namespace of our HtmlHelperSiteMenu class and a call to the SiteMenuAsUnorderedList method:

@using Mvc3DynamicMenu.Models



    
    @ViewBag.Title
    
    
    


    
    @RenderBody()


The ViewBag is a dynamic object, so the method call needs to include a type cast for the ViewBag.SiteLinks parameter. The resulting render of the site links (formatted for readability):


NOTE: The white space after the href=" is a product of the code display in the blog and not part of the render from our HtmlHelper

All that remains to do is to style this puppy out. Grab that designer buddy that owes you a beer (or a muffin) and have them bust out some CSS for you to apply to your unordered lists! You can always offer to help them with the jQuery if they want to do something fancypants. :)

Parting Thoughts

There are a few additional things to consider when working with dynamic menus within MVC. In our example the BaseController would be loading the list of site links from the data source on each action method call. Any controller action method (on a controller that inherits from the base controller class) that is called from a page will result in the code to load the site links being called. While the concept behind a dynamic menu is that it could be changed at any time and thus a fresh call to the data source is needed for each page request, this could pose some performance challenges.

If you don't want every page request to result in a site link query you could implement a caching strategy. Of course, your length of time to cache would need to balance out with your requirements for how fast changes to the site menu should go live. If users of your site management want to be able to create a new page and add a link to it and have it live the minute they click "save" then you can't really implement a long cache time. But you may be able to sell a "processing and propagation" time requirement to the end user and squeeze out 5 minutes or so of caching!

Another thing to be aware of with the BaseController approach is the usage of AJAX calls to controller actions to load data in parts of a page already delivered. This can be adverted with a bit of planning. If you are wanting to have some AJAX calls to controller actions to load page data then you will want to make sure the controllers responsible for that data don't inherit from the BaseController. You may want to rename that BaseController to something more descriptive of its function at that point as well, like BasePageController.

Download the code

Discussion

Artur
Artur
07 Nov, 2011 12:10 PM

Great post. In my current project I'm using very similar approach but instead of definition links in code I defining them in database with additional filed which is a role who can see this link (or 0 which means all)

Shawn Ferguson
Shawn Ferguson
07 Nov, 2011 04:28 PM

This is a great article, thanks for sharing!

08 Nov, 2011 09:40 AM

Good article, thanks for sharing!

However, I would make a small change to the SiteLinkHasChildren method to avoid having to actually count the collection's items (since the item count itself is not needed) just to determine if it contains any – and any is the magic keyword here – items:

public static bool SiteLinkHasChildren(IEnumerable siteLinks, int id)
{
    return siteLinks.Any(i => i.ParentId == id);
}
Czechdude
Czechdude
08 Nov, 2011 09:56 AM

HAHA, I just did that last night:) My approach was creating custom Menu attribute with similar properties and then adding it to controllers.

I do not use the filter, but strongly typed property in a custom WebViewPage.

I inject dependencies with IoC container...

08 Nov, 2011 04:43 PM

Artur and Shawn Ferguson
Thanks for the feedback!

Marius Schulz
Thx! I made an update to the post to use your suggestion. I always forget about the Any method. Maybe this will finally allocate the memory in my brain for it. :)

nick
08 Nov, 2011 05:35 PM

great concept! straight forward and well done.

nick
nick
08 Nov, 2011 06:14 PM

I notice how you have a controller handle the onactionexecuting.

How does this differ from doing it this way? I ask because, I would like to render the menu only once and not have to reload the menu on each request.

public class MenuController : Controller
{
    public ActionResult MenuDynamic()
    {
        var siteMenuManager = new SiteMenuManager();
        ViewBag.SiteLinks = siteMenuManager.GetSitemMenuItems().ToList();

        return PartialView("_MenuDynamic");
    }
    //protected override void OnActionExecuting(ActionExecutingContext filterContext)
    //{
    //    var siteMenuManager = new SiteMenuManager();
    //    ViewBag.SiteLinks = siteMenuManager.GetSitemMenuItems().ToList();
    //    base.OnActionExecuting(filterContext);
    //}
}
08 Nov, 2011 07:07 PM

nick
The use of OnActionExecuting makes it available via the ViewBag to all controllers that inherit from BaseController so the menu can "flow" through to the layout view and be used. If you want to avoid reloading the menu on each request then you are going to want to think about how you can cache the menu info. If you used the controller and action you have above you could decorate it with an OutputCache attribute. Then you would just need to think about how you are going to call that action method to get that content rendered. Or, you could use the OnActionExecuting method and handle caching within the method (check to see if the menu is in cache and not expired, use the value, otherwise load the menu and stick it in cache and then use it).

Gord
Gord
09 Nov, 2011 05:13 PM

Just wanted to say how much I like your blog. Your style of writing makes it easy for my little brain to understand.

Kori
Kori
09 Nov, 2011 06:35 PM

What about appending a "current" to the currently opened menu item. How would that work in this context?

Kori
Kori
09 Nov, 2011 06:36 PM

*sorry, a cssclass called "current", like:


09 Nov, 2011 06:39 PM

Gord
Thank you for the praise! Happy to know that I am finding some success in meeting my goal of helping others. :)

09 Nov, 2011 06:42 PM

Kori
Good question! I will take a look at that later today and add a potential approach as a comment here. Of course, others can feel free to toss in their thoughts on a solution too. :)

Kori
Kori
09 Nov, 2011 08:08 PM

Hey Justin,

I've actually figured it out, I'll share it here.

I had to modify the buildMenuItems signature to include the HtmlHelper from the HtmlHelper function. Those two functions in HtmlHelperSiteMenu.cs become:

public static MvcHtmlString SiteMenuAsUnorderedList(this HtmlHelper helper, List siteLinks)
{
    if (siteLinks == null || siteLinks.Count == 0)
        return MvcHtmlString.Empty;
    var topLevelParentId = SiteLinkListHelper.GetTopLevelParentId(siteLinks);
    return MvcHtmlString.Create(buildMenuItems(helper, siteLinks, topLevelParentId));
}

private static string buildMenuItems(HtmlHelper html, List siteLinks, int parentId)
{
    var user = html.ViewContext.HttpContext.User;
    var parentTag = new TagBuilder("ul");   

    var childSiteLinks = SiteLinkListHelper.GetChildSiteLinks(siteLinks, parentId);
    foreach (var siteLink in childSiteLinks)
    {
        // Don't show unauthorized roles this link
        if (!string.IsNullOrWhiteSpace(siteLink.Role) && !user.IsInRole(siteLink.Role)) { continue; }

        var itemTag = new TagBuilder("li");
        var anchorTag = new TagBuilder("a");
        anchorTag.MergeAttribute("href", siteLink.Url);
        anchorTag.SetInnerText(siteLink.Text);

        if (html.ViewContext.HttpContext.Request.Url.ToString().EndsWith(siteLink.Url)) {
            anchorTag.AddCssClass("current");
        }
    // .. rest of the original function here
}

I've also added, if noticed, the ability to skip links that the user isn't authorized for. The line in buildMenuItems takes care of that, as well as adding string Role { get; } to ISiteLink.cs and the implementation public string Role { get; set; } in SiteMenuItem.cs.

Kori
Kori
09 Nov, 2011 08:27 PM

.. oh, looks like I glossed over the important line itemTag.InnerHtml += buildMenuItems(html, siteLinks, siteLink.Id); .. it needs to have the HtmlHelper passed in for this to work.

Chris
Chris
10 Nov, 2011 02:35 AM

HtmlHelperSiteMenu is a good first attempt, but I belive it's a mistake. These kinds of "helper" classes are very ASP.NET Web Forms-like. Not only is this tedious, it's dangerous--your coworkers are going to kill you when they have to make changes to it. Or you may kill yourself when you have to make changes to the semantic structure some day down the road, and you find yourself having to recompile your binaries every time you want to tweak it.

Instead, use a partial view. That's what they're designed for.

@foreach (ISiteLink link in ViewBag.SiteLinks as IEnumerable) { ... }

14 Nov, 2011 03:47 PM

Hey Justin,

Nice blog post. Great Work!!!.

Thanks,

Jalpesh

14 Nov, 2011 05:13 PM

Jalpesh
Thank you! And thanks for stopping by.

Raj
Raj
09 Jan, 2012 06:52 PM

Hey Justin,

I am an everyday reader of your blog. Trust me I get really good views in MVC. However, I want to make a suggestion that you should include UI snapshots too with codes. It makes easier for a newbie like me in MVC.

Just a though...Keep up the good work man!

Raj

10 Jan, 2012 04:28 AM

Raj
Thank you for the feedback. In some of my older posts I had included screenshots and once I started including sample solution source code with the post I went away from that. But I can see where including screenshots of the UI in the post can still be of use, especially if someone is reading the post without the ability to pull down and work with the source code right away. I will work to include screenshots once again in future posts. Sound good? :) Thanks for reading my stuff! Glad to be of help.

XOS
XOS
18 Jan, 2012 08:47 AM

Any chance you could post a picture of what it does instead of deciphering it through code. Takes ages trawling through websites trying to find what you want...

Rahul
Rahul
22 Jan, 2012 02:12 PM

Agree with XOS. A screenshot will help to understand if this what we are looking for. Also if possible to download source code.

However, I must say a very good and clean job.

24 Jan, 2012 04:23 AM

Rahul
Thx! The sample project source code is available for download. There is a link at the bottom of the post (right above where the comments start).

Abdul Awwal Chaudhary
Abdul Awwal Chaudhary
08 Feb, 2012 12:47 PM

Thanks very good post.

Andy
Andy
21 Feb, 2012 02:37 PM

This is a really interesting piece.

May I ask what the best approach would be if you wanted each controller to display a controller specific menu? Can you still set this in the base controller in a generic manner, so that you don't have to set it in each controller that extends base?

robert badurina
robert badurina
27 Mar, 2012 07:53 PM

why not to use action filter

public class MenuAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var siteMenuManager = new SiteMenuManager();
        filterContext.Controller.ViewBag.SiteLinks = siteMenuManager.GetSitemMenuItems().ToList();
        base.OnActionExecuting(filterContext);
    }
}

and the controller

125 [Menu] public class HomeController : Controller { public ActionResult Index() { return View(); } }125

125

robert badurina
robert badurina
27 Mar, 2012 07:55 PM

sorry for my controller code.

[Menu]
public class HomeController : Controller
{
        public ActionResult Index()
        {
            return View();
        }
}
Bibhu
Bibhu
29 Mar, 2012 12:45 PM

I was trying the example. but here 'ViewBag' donot shown as a known class or attribute so getting error .. please specify what is ViewBag or plase post the code..

robert badurina
robert badurina
31 Mar, 2012 09:34 AM

@Bibhu, ViewBag is a dynamic visible both in the controller and the view. It allows data to be added to it in the controller which is then available in the view. Where do you try to use it?

robert badurina
robert badurina
31 Mar, 2012 09:47 AM

@Bibhu, Are you using mvc 3? ViewBag is a part of mvc 3 and is replacement for the ViewData dictionary from the previous versions.

No new comments are allowed on this post.