Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Auto Generate Menu from Controllers in ASP.NET MVC

0.00/5 (No votes)
15 Oct 2016 1  
A complete automatic generation of menu in ASP.NET MVC using attributes applied on controllers.

Introduction

The idea began when we wanted to generate menu items not by hard-coding or reading from the database since in MVC application each view (or a page if you like to call) is rendered by action methods which are under a controller. And we chose to use attributes and reflection for that.

Benefits

  1. No hard-coded menu name in code
  2. You can add/remove menu just by applying attribute on a controller and/or action methods.
  3. Restricted menu, i.e., menu generation is controlled by access rights. Here, we used one attribute but you can add more if needed (but you need to change the code accordingly)
  4. Controlling the order of menu
  5. Promote an action method as a top level menu (in that case, do not apply attribute on the controller level)
  6. Make the menu non navigatable. For example, the controller name serves as a top level menu but you do not want to take to a view when it is clicked but only the sub level menus (actions) are clickable
  7. Apply icons (using fontawsome)
  8. Extensible - you can extend the features if you need more

Important: Source Download

The code opens in Visual studio 2015 without any issues. I am not sure if it opens in older versions and you need to take care of things yourself (may be you can just copy the important files).

The code is without Nuget packages (including them gets 20MB size) so you need to install manually otherwise the code will not build.

Here is how you install nuget packages:

Using the Code

MenuItemAttribute is what plays the key role.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
 public class MenuItemAttribute : Attribute
 {
     public MenuItemAttribute()
     {
         IsClickable = true;
     }
     public bool IsClickable { get; set; }
     public string Title { get; set; }
     public string Action { get; set; }
     public string CssIcon { get; set; }
     public int Order { get; set; }
     public Type ParentController { get; set; }
 }

All properties are optional, but you can use them based on your requirement. Here is what happens if you do not use it.

  1. IsClickable - by default true. That means a view is rendered when you click it. If set to false, then nothing happens, but you can access the submenus (if present).
  2. Title - By default, takes the controller name without the text "Controller". Say "Home" for a "HomeController" class. Or Action name if set on action method
  3. Action - By default "Index" and this is effective only when applied on the controller. On action method, it has no effect and it always takes the action name.
  4. CssIcon - By default, no icon. You can use the font-awesome icons.
  5. Order - By default 0 that means the menu is generated the order code reads the controller. If set, then it renders in ascending order.
  6. ParentController - If you want to make another controller as a sub menu of a controller.

One Example Controller

[MenuItem (Action = "Users")]
public class AdminController : Controller
{
    // GET: Admin
    public ActionResult Index()
    {
        return View();
    }

    [MenuItem(CssIcon = "fa fa-users fa-lg fa-fw")]
    [AuthorizedRole("Admin")]
    public ActionResult Users()
    {
        return View();
    }
    [MenuItem(Title = "Site Settings")]
    [AuthorizedRole("Super user")]
    public ActionResult Settings()
    {
        return View();
    }
}

Ok, what happens here?

  1. First on a controller level, the Action is set to "Users" which means the users' view is rendered when clicking the "Admin" menu
  2. On the "Users" action, a font-awesome icon is set and it is restricted to only the users who have "Admin" role. AuthorizedRole is another custom attribute which takes care of how you check in real time. You can check the code and change the logic in whatever way you like in real time.
  3. On the "Settings" action, the title is changed to "Site Settings" and restricted to only users with "Super user"  role.

This is just one sample. Just check the other controllers in the code by yourself.

How the attribute is converted as menu

The MenuItemAttribute which is applied on Controllers and Actions is picked by the code dynamically and returns the list of Menu class which is then rendered as a nice Bootstrap menu by the code.

The below code is part of the MenuGenerator class 

        public static List<Menu> CreateMenu()
        {
            var menus = new List<Menu>();

            var currentAssembly = Assembly.GetAssembly(typeof(MenuGenerator));
            var allControllers = currentAssembly.GetTypes().Where(t => t.IsSubclassOf(typeof(Controller))).ToList();
            var menuControllers = allControllers.Where(t => t.GetCustomAttribute<MenuItemAttribute>() != null ||
                                                             t.GetMethods().Any(m => m.GetCustomAttribute<MenuItemAttribute>() != null))
                                                             .ToList();
            var submenuControllers = new List<Menu>();
            menuControllers.ForEach(controller =>
            {
                var navigation = controller.GetCustomAttribute<MenuItemAttribute>();
                if (navigation == null) //navigation is set only against actions
                {
                    controller.GetMethods().ToList().ForEach(method =>
                    {
                        navigation = method.GetCustomAttribute<MenuItemAttribute>();
                        if (navigation == null) return;
                        if (!UserHasAccess(method.GetCustomAttribute<AuthorizedRoleAttribute>())) return;
                        Menu actionMenu = CreateAreaMenuItemFromAction(controller, method, navigation);
                        menus.Add(actionMenu);
                    });
                    return;
                }

                if (!UserHasAccess(controller.GetCustomAttribute<AuthorizedRoleAttribute>())) return;
                Menu menu = CreateAreaMenuItemFromController(controller, navigation);
                if (navigation.ParentController != null)
                {
                    if (navigation.ParentController.IsSubclassOf(typeof(Controller)))
                    {
                        menu.ParentControllerFullName = navigation.ParentController.FullName;
                        submenuControllers.Add(menu);
                    }
                }
                menus.Add(menu);
            });
            menus = menus.Except(submenuControllers).ToList();
            submenuControllers.ForEach(sm =>
            {
                var parentMenu = menus.FirstOrDefault(m => m.ControllerFullName == sm.ParentControllerFullName);
                parentMenu?.SubMenus.Add(new SubMenu() { Name = sm.Name, Url = sm.Url });
            });
            return menus.OrderBy(m => m.Order).ToList();
        }
 

And this method is called from the MenuController which is requested from the Layout view

   public class MenuController : Controller
    {
        // GET: Menu
        public PartialViewResult Index()
        {
            List<Menu> menus = MenuGenerator.CreateMenu();
            return PartialView("Partials/_menu", menus);
        }
    }
 

And the Menu and SubMenu

public class Menu
    {
        public Menu()
        {
            SubMenus = new List<SubMenu>();
        }
        public string Name { get; set; }
        public string CssIcon { get; set; }
        public string Url { get; set; }
        public List<SubMenu> SubMenus { get; set; }
        public string ParentControllerFullName { get; set; }
        public string ControllerFullName { get; set; }
        public int Order { get; set; }
    }
 

 

public class SubMenu
{
    public string Name { get; set; }
    public string Url { get; set; }
    public string CssIcon { get; set; }
    public int Order { get; set; }
}

 

The Menu view

@using DynamicMvcMenu.Models
@model List<DynamicMvcMenu.Models.Menu>
<ul class="nav">
    @foreach (Menu menu in Model)
    {
        <li class="dropdown">
            @if (string.IsNullOrWhiteSpace(menu.Url))
            {

                <a href="#" class="dropdown-toggle" id="dropdownCommonMenu" data-toggle="dropdown">
                    <span class="icon">
                        <i class="@menu.CssIcon" aria-hidden="true"></i>

The Result

I did not spend time in designing the style but downloaded the code from here and adjust a bit.

Possible Extensions

  1. You can still add your own menu (if they point to a static HTML file or external site) in addition to the dynamic menu just by adding into the menu list from the Menucontroller.
  2. It supports only two-level menu, you can work a bit to make third-level menu of your requirement

Localization

This section is added as Gaston Verelst asked about it after giving me 5 star :)

You can add a new property LanguageKey into the MenuItemAttribute class and make it mandatory by changing the constructor so that it looks like

public MenuItemAttribute(string LangKey)
{
    IsClickable = true;
    LanguageKey = LangKey;
            
}
 

Then the Menu and SubMenu classes will get the name from the langauge key which can be used to retrieve the right text based on the user's language preference. You can use a Resource file or database (or anything else) to read the value for the key. You may create a new service class like the one below

LanguageService

public class LanguageService
{
    public string GetText(string LangKey)
    {
       //var userlang = get user preferene from cookie or database
       //read from resourse/database or wherever you want
    }
}
 

And in the CreateMenu function, Name property of Menu and SubMenu objects will be set by calling the GetText method like this

menu.Name = LangaugeService.GetText(attribute.LanguageKey);
 

Hope this helps

How we did it

We have decided to support two langauges English and Swedish so we have added these two mandatory properties in the MeuItem attribute, i.e. the constructor with two arguments

  • SwedishDefault
  • EnglishDefault

We also generated a unique key dynamially (Controller fullname + action name) based on where it is applied so that we provide local dialect (same language but different text) support to the customer if they want to overwrite our default text (Say a customer/user prefers "Create New" instead of "Add" for a button)

Then the Menu gets right text (it is a dialect if that exists in database or the default one) based on the current user's preferred langugae (we have stored in cookie but it can however comes from any other source as well like database)

 

Finally

Hope this is useful for some who like it and need it. Please leave a comment if something is not working or if you need more information. Thank you for staying this far.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here