ASP.NET MVC 中通过控制器自动生成菜单
通过应用于控制器的特性,在 ASP.NET MVC 中完全自动生成菜单。
引言
这个想法的起源是我们想要生成菜单项,而不是通过硬编码或从数据库读取,因为在 MVC 应用程序中,每个视图(或者如果你喜欢的话,可以称之为页面)都由控制器下的操作方法渲染。我们选择了使用特性和反射来实现这一点。
优点
- 代码中没有硬编码的菜单名称
- 您可以通过在控制器和/或操作方法上应用特性来添加/删除菜单。
- 受限菜单,即菜单生成受访问权限控制。这里我们使用了一个特性,但如果需要,您可以添加更多(但您需要相应地更改代码)。
- 控制菜单的顺序
- 将操作方法提升为顶级菜单(在这种情况下,不要在控制器级别应用特性)。
- 使菜单不可导航。例如,控制器名称充当顶级菜单,但您不希望在点击它时跳转到视图,而只希望子菜单(操作)可点击。
- 应用图标(使用 fontawsome)
- 可扩展 - 如果需要更多功能,您可以扩展。
重要:下载源码
代码在 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; }
}
所有属性都是可选的,但您可以根据需要使用它们。如果不使用它们会发生什么:
IsClickable
- 默认值为true
。这意味着点击时会渲染一个视图。如果设置为false
,则什么也不会发生,但您可以访问子菜单(如果存在)。Title
- 默认情况下,取控制器名称,去掉“Controller
”字样。例如,“HomeController
”类取“Home
”。如果设置在action
方法上,则取Action
名称。Action
- 默认值为“Index
”,并且仅在应用于controller
时有效。应用于action
方法时,它没有效果,总是取 action 名称。CssIcon
- 默认情况下,没有图标。您可以使用 font-awesome 图标。Order
- 默认值为0
,意味着菜单的生成顺序与代码读取控制器的顺序一致。如果设置了该值,则按升序渲染。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();
}
}
好的,这里发生了什么?
- 首先,在控制器级别,Action 设置为“Users”,这意味着点击“Admin”菜单时将渲染 Users 视图。
- 在“Users”操作上,设置了一个 font-awesome 图标,并且仅限于具有“Admin”角色的用户。
AuthorizedRole
是另一个自定义特性,它负责如何实时检查。您可以查看代码并根据需要更改实时逻辑。 - 在“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>
结果
我没有花时间设计样式,而是 从这里下载了代码 并进行了一些调整。
可能的扩展
- 您仍然可以添加自己的菜单(如果它们指向
static
HTML 文件或外部站点),除了动态菜单,只需将它们添加到Menucontroller
的菜单列表中即可。 - 它仅支持两级菜单,您可以稍微修改一下以满足您对三级菜单的需求。
本地化
这一部分是 Gaston Verelst 在给了我 5 星后询问的,所以添加了 :)
您可以在 MenuItemAttribute
类中添加一个新的属性 LanguageKey
,并通过更改构造函数使其成为必需的,如下所示:
public MenuItemAttribute(string LangKey)
{
IsClickable = true;
LanguageKey = LangKey;
}
然后 Menu
和 SubMenu
类将从语言键获取名称,该键可用于根据用户的语言偏好检索正确的文本。您可以使用资源文件或数据库(或其他任何内容)来读取键的值。您可以创建一个新的服务类,如下所示:
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
函数中,Menu
和 SubMenu
对象的 Name
属性将通过调用 GetText
方法来设置,如下所示:
menu.Name = LangaugeService.GetText(attribute.LanguageKey);
希望这有帮助
我们是如何实现的
我们决定支持两种语言:英语和瑞典语,因此我们在 MeuItem 属性中添加了这两个必需的属性,即带有两个参数的构造函数:
- SwedishDefault
- EnglishDefault
我们还根据应用的位置动态生成了一个唯一的键(控制器完整名称 + 操作名称),以便为客户提供本地方言(相同语言但不同文本)支持,如果他们想覆盖我们的默认文本(例如,客户/用户更喜欢“Create New”而不是“Add”来表示按钮)。
然后,菜单根据当前用户首选的语言(我们存储在 cookie 中,但也可以来自其他来源,如数据库)获取正确的文本(如果存在方言,则使用数据库中的,否则使用默认的)。
最后
希望这对喜欢并需要它的人有所帮助。如果有什么不起作用或者您需要更多信息,请留言。感谢您阅读到这里。