之前有小伙伴问,能不能动态加入菜单,每次都是手敲链接进入插件界面相当的不友好。答案是肯定的。
这里我先做一个简单的实现,如果后续其他的难点都解决了,我会将这里的实现改为一个单独的模块,实现方式也改的更优雅一点。
首先在Mystique.Core项目中添加一个特性类Page, 这个特性只允许在方法上使用,Name属性保存了当前页面的名称。
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] public class Page : Attribute { public Page(string name) { Name = name; } public string Name { get; set; } }第二步,创建一个展示导航栏菜单用的视图模型类PageRoutViewModel,我们会在导航部分使用到它。
public class PageRouteViewModel { public PageRouteViewModel(string pageName, string area, string controller, string action) { PageName = pageName; Area = area; Controller = controller; Action = action; } public string PageName { get; set; } public string Area { get; set; } public string Controller { get; set; } public string Action { get; set; } public string Url { get { return $"{Area}/{Controller}/{Action}"; } } }第三步,我们需要使用反射,从所有启用的插件程序集中加载所有带有Page特性的路由方法,并将他们组合成一个导航栏菜单的视图模型集合。
public static class CollectibleAssemblyLoadContextExtension { public static List<PageRouteViewModel> GetPages(this CollectibleAssemblyLoadContext context) { var entryPointAssembly = context.GetEntryPointAssembly(); var result = new List<PageRouteViewModel>(); if (entryPointAssembly == null || !context.IsEnabled) { return result; } var areaName = context.PluginName; var types = entryPointAssembly.GetExportedTypes().Where(p => p.BaseType == typeof(Controller)); if (types.Any()) { foreach (var type in types) { var controllerName = type.Name.Replace("Controller", ""); var actions = type.GetMethods().Where(p => p.GetCustomAttributes(false).Any(x => x.GetType() == typeof(Page))).ToList(); foreach (var action in actions) { var actionName = action.Name; var pageAttribute = (Page)action.GetCustomAttributes(false).First(p => p.GetType() == typeof(Page)); result.Add(new PageRouteViewModel(pageAttribute.Name, areaName, controllerName, actionName)); } } return result; } else { return result; } } }Notes: 这里其实可以集成MVC的路由系统来生成Url, 这里为了简单演示,就采取了手动拼凑Url的方式,有兴趣的同学可以自己改写一下。
最后我们来修改主站点的母版页_Layout.cshtml, 在导航栏尾部追加动态菜单。
@using Mystique.Core.Mvc.Extensions; @{ var contexts = Mystique.Core.PluginsLoadContexts.All(); var menus = contexts.SelectMany(p => p.GetPages()).ToList(); } ... <header> <nav> <div> <a asp-area="" asp-controller="Home" asp-action="Index">DynamicPluginsDemoSite</a> <button type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <span></span> </button> <div> <ul> <li> <a asp-area="" asp-controller="Home" asp-action="Index">Home</a> </li> <li> <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a> </li> <li> <a asp-area="" asp-controller="Plugins" asp-action="Index">Plugins</a> </li> <li> <a asp-area="" asp-controller="Plugins" asp-action="Assemblies">Assemblies</a> </li> @foreach (var item in menus) { <li> <a href="http://www.likecs.com/Modules/@item.Url">@item.PageName</a> </li> } </ul> </div> </div> </nav> </header>这样基础设施部分的代码就完成了,下面我们来尝试修改插件1的代码,在HelloWorld路由方法上我们添加特性[Page("Plugin One")], 这样按照我们的预想,当插件1启动的时候,导航栏中应该出现Plugin One的菜单项。
[Area("DemoPlugin1")] public class Plugin1Controller : Controller { private INotificationRegister _notificationRegister; public Plugin1Controller(INotificationRegister notificationRegister) { _notificationRegister = notificationRegister; } [Page("Plugin One")] [HttpGet] public IActionResult HelloWorld() { string content = new Demo().SayHello(); ViewBag.Content = content + "; Plugin2 triggered"; TestClass testClass = new TestClass(); testClass.Message = "Hello World"; _notificationRegister.Publish("LoadHelloWorldEvent", JsonConvert.SerializeObject(new LoadHelloWorldEvent() { Str = "Hello World" })); return View(testClass); } } 最终效果下面我们启动程序,来看一下最终的效果,动态菜单功能完成。