65.9K
CodeProject 正在变化。 阅读更多。
Home

ASP.NET MVC 中通过控制器自动生成菜单

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (25投票s)

2016 年 9 月 30 日

CPOL

5分钟阅读

viewsIcon

63108

downloadIcon

2696

通过应用于控制器的特性,在 ASP.NET MVC 中完全自动生成菜单。

引言

这个想法的起源是我们想要生成菜单项,而不是通过硬编码或从数据库读取,因为在 MVC 应用程序中,每个视图(或者如果你喜欢的话,可以称之为页面)都由控制器下的操作方法渲染。我们选择了使用特性和反射来实现这一点。

优点

  1. 代码中没有硬编码的菜单名称
  2. 您可以通过在控制器和/或操作方法上应用特性来添加/删除菜单。
  3. 受限菜单,即菜单生成受访问权限控制。这里我们使用了一个特性,但如果需要,您可以添加更多(但您需要相应地更改代码)。
  4. 控制菜单的顺序
  5. 将操作方法提升为顶级菜单(在这种情况下,不要在控制器级别应用特性)。
  6. 使菜单不可导航。例如,控制器名称充当顶级菜单,但您不希望在点击它时跳转到视图,而只希望子菜单(操作)可点击。
  7. 应用图标(使用 fontawsome
  8. 可扩展 - 如果需要更多功能,您可以扩展。

重要:下载源码

代码在 Visual Studio 2015 中可以正常打开。我不确定它是否能在旧版本中打开,您可能需要自己处理(也许可以只复制重要文件)。

代码不包含 Nuget 包(包含它们会增加 20MB 的大小),因此您需要手动安装,否则代码将无法构建。

以下是如何安装 nuget 包

Using the Code

MenuItemAttribute 是起关键作用的。

   [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; }
    } 

所有属性都是可选的,但您可以根据需要使用它们。如果不使用它们会发生什么:

  1. IsClickable - 默认值为 true。这意味着点击时会渲染一个视图。如果设置为 false,则什么也不会发生,但您可以访问子菜单(如果存在)。
  2. Title - 默认情况下,取控制器名称,去掉“Controller”字样。例如,“HomeController”类取“Home”。如果设置在 action 方法上,则取 Action 名称。
  3. Action - 默认值为“Index”,并且仅在应用于 controller 时有效。应用于 action 方法时,它没有效果,总是取 action 名称。
  4. CssIcon - 默认情况下,没有图标。您可以使用 font-awesome 图标。
  5. Order - 默认值为 0,意味着菜单的生成顺序与代码读取控制器的顺序一致。如果设置了该值,则按升序渲染。
  6. ParentController - 如果您想将另一个控制器作为某个控制器的子菜单。

一个示例控制器

    [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();
        }
    }    

好的,这里发生了什么?

  1. 首先,在控制器级别,Action 设置为“Users”,这意味着点击“Admin”菜单时将渲染 Users 视图。
  2. 在“Users”操作上,设置了一个 font-awesome 图标,并且仅限于具有“Admin”角色的用户。AuthorizedRole 是另一个自定义特性,它负责如何实时检查。您可以查看代码并根据需要更改实时逻辑。
  3. 在“Settings”操作上,标题更改为“Site Settings”,并且仅限于拥有“Super user”角色的用户。

这只是一个示例。您可以自己检查代码中的其他控制器。

特性如何转换为菜单

应用于控制器和操作的 MenuItemAttribute 会被代码动态拾取,并返回一个 Menu 类列表,然后代码将其渲染为漂亮的 Bootstrap 菜单。

下面的代码是 MenuGenerator 类的一部分

        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();
        }
 

这个方法是从 Layout 视图请求的 MenuController 中调用的

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

Menu 和 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; }
    }
    

 

菜单视图

@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>
 

结果

我没有花时间设计样式,而是 从这里下载了代码 并进行了一些调整。

可能的扩展

  1. 您仍然可以添加自己的菜单(如果它们指向 static HTML 文件或外部站点),除了动态菜单,只需将它们添加到 Menucontroller 的菜单列表中即可。
  2. 它仅支持两级菜单,您可以稍微修改一下以满足您对三级菜单的需求。

本地化

这一部分是 Gaston Verelst 在给了我 5 星后询问的,所以添加了 :)

您可以在 MenuItemAttribute 类中添加一个新的属性 LanguageKey,并通过更改构造函数使其成为必需的,如下所示:

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

然后 MenuSubMenu 类将从语言键获取名称,该键可用于根据用户的语言偏好检索正确的文本。您可以使用资源文件或数据库(或其他任何内容)来读取键的值。您可以创建一个新的服务类,如下所示:

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
    }
}
 

CreateMenu 函数中,MenuSubMenu 对象的 Name 属性将通过调用 GetText 方法来设置,如下所示:

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

希望这有帮助

我们是如何实现的

我们决定支持两种语言:英语和瑞典语,因此我们在 MeuItem 属性中添加了这两个必需的属性,即带有两个参数的构造函数:

  • SwedishDefault
  • EnglishDefault

我们还根据应用的位置动态生成了一个唯一的键(控制器完整名称 + 操作名称),以便为客户提供本地方言(相同语言但不同文本)支持,如果他们想覆盖我们的默认文本(例如,客户/用户更喜欢“Create New”而不是“Add”来表示按钮)。

然后,菜单根据当前用户首选的语言(我们存储在 cookie 中,但也可以来自其他来源,如数据库)获取正确的文本(如果存在方言,则使用数据库中的,否则使用默认的)。

 

最后

希望这对喜欢并需要它的人有所帮助。如果有什么不起作用或者您需要更多信息,请留言。感谢您阅读到这里。

© . All rights reserved.