Click here to Skip to main content
15,507,526 members
Articles / Web Development / ASP.NET / ASP.NET4.0
Article
Posted 30 Sep 2016

Stats

54.3K views
2.6K downloads
39 bookmarked

Auto Generate Menu from Controllers in ASP.NET MVC

Rate me:
Please Sign up or sign in to vote.
4.95/5 (26 votes)
15 Oct 2016CPOL5 min read
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:

Image 1

Using the Code

MenuItemAttribute is what plays the key role.

C#
[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

C#
[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 

C#
        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

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

And the Menu and SubMenu

C#
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; }
    }
 

 

C#
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

C#
@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>
                    </span>
                    @menu.Name
                </a>
            }
            else
            {
                <a href="@Url.Content(menu.Url)">
                    <span class="icon">
                        <i class="@menu.CssIcon" aria-hidden="true"></i>
                    </span>
                    @menu.Name
                </a>
            }
            @if (menu.SubMenus.Any())
            {
                <a class="dropdown-toggle" data-toggle="dropdown" href="#">
                    <span class="caret"></span>
                </a>
                <ul class="dropdown-menu navmenu-nav" role="menu" aria-labelledby="dropdownCommonMenu">

                    @foreach (SubMenu subMenu in menu.SubMenus)
                    {
                        <li role="menuitem">
                            <a href="@Url.Content(subMenu.Url)">
                                <span class="icon">
                                    <i class="@subMenu.CssIcon" aria-hidden="true"></i>
                                </span>
                                @subMenu.Name
                            </a>
                        </li>
                    }
                </ul>
            }

        </li>

    }
</ul>
 

The Result

Image 2

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

C#
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

C#
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

C#
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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect CGI
India India
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Questionvery nice article Pin
mahmood kabi23-Dec-19 8:56
mahmood kabi23-Dec-19 8:56 
AnswerRe: very nice article Pin
Prabu ram29-Jul-21 21:05
Prabu ram29-Jul-21 21:05 
PraiseAwesome Pin
Saeed pi24-Oct-19 0:46
professionalSaeed pi24-Oct-19 0:46 
GeneralASP net core menu Pin
Member 1143488516-Feb-19 22:58
Member 1143488516-Feb-19 22:58 
GeneralRe: ASP net core menu Pin
Prabu ram29-Jul-21 21:09
Prabu ram29-Jul-21 21:09 
QuestionExcelent! Pin
zekikaya13-Sep-18 4:23
zekikaya13-Sep-18 4:23 
QuestionImplementation Error Pin
Anumbe27-Jan-18 2:59
professionalAnumbe27-Jan-18 2:59 
Great piece!!
First, I'm a beginner and I'm following the implementation in an MVC application and my framework is .NET Core 2.0, but having error in the following classes:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;

namespace AccountPlus.Web.AccountPlusBusinessService.Services
{
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; }
}
}

the annotated line is given error. What do I need to do?
Sencondly,
using Microsoft.AspNetCore.Authorization;

namespace AccountPlus.Web.AccountPlusBusinessService.Services
{
public class AuthorizedRoleAttribute : AuthorizeAttribute
{
public AuthorizedRoleAttribute(string role)
{
Role = role;
}
protected override bool AuthorizeCore(HttpContextBase piHttpContext)
{
//write actual check here
return Role == "Admin";
}

protected override void HandleUnauthorizedRequest(AuthorizationContext piFilterContext)
{
base.HandleUnauthorizedRequest(piFilterContext);


}

public string Role { get; private set; }
}


}

In this class, HttpContextBase , AuthorizationContext, AuthorizeCore, HandleUnauthorizedRequest are giving errors. What do I need to do to clear these errors.

Thank you for the anticipated result.
QuestionIf submenu is not belong parent controller, it can't show icon Pin
CuongLee15-Aug-17 17:18
CuongLee15-Aug-17 17:18 
AnswerRe: If submenu is not belong parent controller, it can't show icon Pin
Prabu ram20-Aug-17 19:22
Prabu ram20-Aug-17 19:22 
QuestionMenu if user has more than One Role, Editor & Publisher? Pin
longnights1-Feb-17 8:55
longnights1-Feb-17 8:55 
AnswerRe: Menu if user has more than One Role, Editor & Publisher? Pin
Prabu ram1-Feb-17 18:55
Prabu ram1-Feb-17 18:55 
QuestionDynamicMvcMenu not listed in NuGet Pin
norwich195117-Oct-16 3:30
norwich195117-Oct-16 3:30 
AnswerRe: DynamicMvcMenu not listed in NuGet Pin
Prabu ram17-Oct-16 19:11
Prabu ram17-Oct-16 19:11 
GeneralMy vote of 5 Pin
Gaston Verelst10-Oct-16 3:44
Gaston Verelst10-Oct-16 3:44 
GeneralRe: My vote of 5 Pin
Prabu ram15-Oct-16 0:13
Prabu ram15-Oct-16 0:13 
GeneralRe: My vote of 5 Pin
Gaston Verelst18-Oct-16 2:16
Gaston Verelst18-Oct-16 2:16 
GeneralMy vote of 5 Pin
HGDiaz3-Oct-16 10:08
HGDiaz3-Oct-16 10:08 
GeneralRe: My vote of 5 Pin
Prabu ram15-Oct-16 0:14
Prabu ram15-Oct-16 0:14 
GeneralMy vote of 5 Pin
Vaso Elias3-Oct-16 5:40
Vaso Elias3-Oct-16 5:40 
GeneralRe: My vote of 5 Pin
Prabu ram15-Oct-16 0:15
Prabu ram15-Oct-16 0:15 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.