MVC Automatic Menu

Posted by Nuri Halperin on Geeks with Blogs See other posts from Geeks with Blogs or by Nuri Halperin
Published on Mon, 22 Mar 2010 11:17:00 GMT Indexed on 2010/03/22 6:31 UTC
Read the original article Hit count: 638

Filed under:

An ex-colleague of mine used to call his SQL script generator "Super-Scriptmatic 2000". It impressed our then boss little, but was fun to say and use. We called every batch job and script "something 2000" from that day on. I'm tempted to call this one Menu-Matic 2000, except it's waaaay past 2000. Oh well.

The problem: I'm developing a bunch of stuff in MVC. There's no PM to generate mounds of requirements and there's no Ux Architect to create wireframe. During development, things change. Specifically, actions get renamed, moved from controller x to y etc. Well, as the site grows, it becomes a major pain to keep a static menu up to date, because the links change. The HtmlHelper doesn't live up to it's name and provides little help. How do I keep this growing list of pesky little forgotten actions reigned in?

The general plan is:

  1. Decorate every action you want as a menu item with a custom attribute
  2. Reflect out all menu items into a structure at load time
  3. Render the menu using as CSS  friendly <ul><li> HTML.

The MvcMenuItemAttribute decorates an action, designating it to be included as a menu item:

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
    public class MvcMenuItemAttribute : Attribute
    {
 
        public string MenuText { get; set; }
 
        public int Order { get; set; }
 
        public string ParentLink { get; set; }
 
        internal string Controller { get; set; }
 
        internal string Action { get; set; }
 
 
        #region ctor
 
        public MvcMenuItemAttribute(string menuText) : this(menuText, 0) { }
        public MvcMenuItemAttribute(string menuText, int order)
        {
            MenuText = menuText;
            Order = order;
        }
 
 
 
        internal string Link { get { return string.Format("/{0}/{1}", Controller, this.Action); } }
 
        internal MvcMenuItemAttribute ParentItem { get; set; }
        #endregion
    }

The MenuText allows overriding the text displayed on the menu. The Order allows the items to be ordered. The ParentLink allows you to make this item a child of another menu item. An example action could then be decorated thusly: [MvcMenuItem("Tracks", Order = 20, ParentLink = "/Session/Index")] . All pretty straightforward methinks.

The challenge with menu hierarchy becomes fairly apparent when you try to render a menu and highlight the "current" item or render a breadcrumb control. Both encounter an  ambiguity if you allow a data source to have more than one menu item with the same URL link. The issue is that there is no great way to tell which link a person click. Using referring URL will fail if a user bookmarked the page. Using some extra query string to disambiguate duplicate URLs essentially changes the links, and also ads a chance of collision with other query parameters. Besides, that smells. The stock ASP.Net sitemap provider simply disallows duplicate URLS. I decided not to, and simply pick the first one encountered as the "current". Although it doesn't solve the issue completely – one might say they wanted the second of the 2 links to be "current"- it allows one to include a link twice (home->deals and products->deals etc), and the logic of deciding "current" is easy enough to explain to the customer.

Now that we got that out of the way, let's build the menu data structure:

public static List<MvcMenuItemAttribute> ListMenuItems(Assembly assembly)
{
    var result = new List<MvcMenuItemAttribute>();
    foreach (var type in assembly.GetTypes())
    {
        if (!type.IsSubclassOf(typeof(Controller)))
        {
            continue;
        }
        foreach (var method in type.GetMethods())
        {
            var items = method.GetCustomAttributes(typeof(MvcMenuItemAttribute), false) as MvcMenuItemAttribute[];

            if (items == null)
            {
                continue;
            }
            foreach (var item in items)
            {
                if (String.IsNullOrEmpty(item.Controller))
                {
                    item.Controller = type.Name.Substring(0, type.Name.Length - "Controller".Length);
                }
                if (String.IsNullOrEmpty(item.Action))
                {
                    item.Action = method.Name;
                }
                result.Add(item);
            }
        }
    }

    return result.OrderBy(i => i.Order).ToList();
}

Using reflection, the ListMenuItems method takes an assembly (you will hand it your MVC web assembly) and generates a list of menu items. It digs up all the types, and for each one that is an MVC Controller, digs up the methods. Methods decorated with the MvcMenuItemAttribute get plucked and added to the output list. Again, pretty simple. To make the structure hierarchical, a LINQ expression matches up all the items to their parent:

public static void RegisterMenuItems(List<MvcMenuItemAttribute> items)
{
    _MenuItems = items;
    _MenuItems.ForEach(i => i.ParentItem =
                            items.FirstOrDefault(p =>
                                                 String.Equals(p.Link, i.ParentLink, StringComparison.InvariantCultureIgnoreCase)));
}

The _MenuItems is simply an internal list to keep things around for later rendering. Finally, to package the menu building for easy consumption:

public static void RegisterMenuItems(Type mvcApplicationType)
{
    RegisterMenuItems(ListMenuItems(Assembly.GetAssembly(mvcApplicationType)));
}

To bring this puppy home, a call in Global.asax.cs Application_Start() registers the menu. Notice the ugliness of reflection is tucked away from the innocent developer. All they have to do is call the RegisterMenuItems() and pass in the type of the application. When you use the new project template, global.asax declares a class public class MvcApplication : HttpApplication and that is why the Register call passes in that type.

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    RegisterRoutes(RouteTable.Routes);
 
    MvcMenu.RegisterMenuItems(typeof(MvcApplication));
}

 

What else is left to do? Oh, right, render!

public static void ShowMenu(this TextWriter output)
{
    var writer = new HtmlTextWriter(output);
 
    renderHierarchy(writer, _MenuItems, null);
}
 
public static void ShowBreadCrumb(this TextWriter output, Uri currentUri)
{
    var writer = new HtmlTextWriter(output);
    string currentLink = "/" + currentUri.GetComponents(UriComponents.Path, UriFormat.Unescaped);
 
    var menuItem = _MenuItems.FirstOrDefault(m => m.Link.Equals(currentLink, StringComparison.CurrentCultureIgnoreCase));
    if (menuItem != null)
    {
        renderBreadCrumb(writer, _MenuItems, menuItem);
    }
}
 
private static void renderBreadCrumb(HtmlTextWriter writer, List<MvcMenuItemAttribute> menuItems, MvcMenuItemAttribute current)
{
    if (current == null)
    {
        return;
    }
    var parent = current.ParentItem;
    renderBreadCrumb(writer, menuItems, parent);
    writer.Write(current.MenuText);
    writer.Write(" / ");
 
}
 
 
static void renderHierarchy(HtmlTextWriter writer, List<MvcMenuItemAttribute> hierarchy, MvcMenuItemAttribute root)
{
    if (!hierarchy.Any(i => i.ParentItem == root)) return;
 
    writer.RenderBeginTag(HtmlTextWriterTag.Ul);
    foreach (var current in hierarchy.Where(element => element.ParentItem == root).OrderBy(i => i.Order))
    {
        if (ItemFilter == null || ItemFilter(current))
        {
 
            writer.RenderBeginTag(HtmlTextWriterTag.Li);
            writer.AddAttribute(HtmlTextWriterAttribute.Href, current.Link);
            writer.AddAttribute(HtmlTextWriterAttribute.Alt, current.MenuText);
            writer.RenderBeginTag(HtmlTextWriterTag.A);
            writer.WriteEncodedText(current.MenuText);
            writer.RenderEndTag(); // link
            renderHierarchy(writer, hierarchy, current);
            writer.RenderEndTag(); // li
        }
    }
    writer.RenderEndTag(); // ul
}

The ShowMenu method renders the menu out to the provided TextWriter. In previous posts I've discussed my partiality to using well debugged, time test HtmlTextWriter to render HTML rather than writing out angled brackets by hand. In addition, writing out using the actual writer on the actual stream rather than generating string and byte intermediaries (yes, StringBuilder being no exception) disturbs me.

To carry out the rendering of an hierarchical menu, the recursive renderHierarchy() is used. You may notice that an ItemFilter is called before rendering each item. I figured that at some point one might want to exclude certain items from the menu based on security role or context or something. That delegate is the hook for such future feature.

To carry out rendering of a breadcrumb recursion is used again, this time simply to unwind the parent hierarchy from the leaf node, then rendering on the return from the recursion rather than as we go along deeper. I guess I was stuck in LISP that day.. recursion is fun though.

 

Now all that is left is some usage! Open your Site.Master or wherever you'd like to place a menu or breadcrumb, and plant one of these calls:

<% MvcMenu.ShowBreadCrumb(this.Writer, Request.Url); %> to show a breadcrumb trail (notice lack of "=" after <% and the semicolon).

<% MvcMenu.ShowMenu(Writer); %> to show the menu.

 

As mentioned before, the HTML output is nested <UL> <LI> tags, which should make it easy to style using abundant CSS to produce anything from static horizontal or vertical to dynamic drop-downs.

 

This has been quite a fun little implementation and I was pleased that the code size remained low. The main crux was figuring out how to pass parent information from the attribute to the hierarchy builder because attributes have restricted parameter types. Once I settled on that implementation, the rest falls into place quite easily.

© Geeks with Blogs or respective owner