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 specificISiteLink
.
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 ourHtmlHelper
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
No new comments are allowed on this post.
Discussion
Artur
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
This is a great article, thanks for sharing!
Marius Schulz
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:Czechdude
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...
Justin Schwartzenberger
great concept! straight forward and well done.
nick
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.
Justin Schwartzenberger
Gord
Just wanted to say how much I like your blog. Your style of writing makes it easy for my little brain to understand.
Kori
What about appending a "current" to the currently opened menu item. How would that work in this context?
Kori
*sorry, a cssclass called "current", like:
Justin Schwartzenberger
Justin Schwartzenberger
Kori
Hey Justin,
I've actually figured it out, I'll share it here.
I had to modify the
buildMenuItems
signature to include theHtmlHelper
from the HtmlHelper function. Those two functions inHtmlHelperSiteMenu.cs
become: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 addingstring Role { get; }
toISiteLink.cs
and the implementationpublic string Role { get; set; }
inSiteMenuItem.cs
.Kori
.. oh, looks like I glossed over the important line
itemTag.InnerHtml += buildMenuItems(html, siteLinks, siteLink.Id);
.. it needs to have theHtmlHelper
passed in for this to work.Chris
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) { ... }
Jalpesh Vadgama
Hey Justin,
Nice blog post. Great Work!!!.
Thanks,
Jalpesh
Justin Schwartzenberger
Raj
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
Justin Schwartzenberger
XOS
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
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.
Justin Schwartzenberger
Abdul Awwal Chaudhary
Thanks very good post.
Andy
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
why not to use action filter
and the controller
125 [Menu] public class HomeController : Controller { public ActionResult Index() { return View(); } }125
125
robert badurina
sorry for my controller code.
Bibhu
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
@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
@Bibhu, Are you using mvc 3? ViewBag is a part of mvc 3 and is replacement for the ViewData dictionary from the previous versions.