基于ASP.NET MVC 4/5 Razor的模块化/插件式架构实现
概述
在日常开发中, 我们经常谈起模块化/插件化架构,这样可既可以提高开效率,又可以实现良好的扩展性,尤其对于产品化的系统有更好的实用性。
架构
我们采用的是MVC5(本文中介绍的方法对于MVC4也是适用的),如下图,解决方案中有四个项目,其中 WeDiscuss 为前端,WeDiscuss.Plugin.Framework 为插件公共类库 WeDiscuss.Plugin.Album 为插件(相册) WeDiscuss.Plugin.News 为插件(新闻),本文只是讲解决插件的实现方式,就不多做其它如果业务逻辑、数据访问层等
注;每个插件都有自已的(M、V、C),内部实现和常用MVC没有区别,这样可以方便的开发,没有其它新知识的引入。
其中,插件层可以在主项目中引用,也可以不引用,或是放到其它目录下(如把插件DLL单独放到“Plugins”目录中),如果不引用就采用在编译完成时复制
下面讲解编译完成复制方法,如想复制到“Plugins”目录中请修改BIN为“Plugins”:
在如下图加入:
copy /Y "$(TargetDir)$(ProjectName).dll" "$(SolutionDir)Wediscuss\Bin\"
如何让ASP.NET加载BIN目录之外的路径的Assembly
我们把各个模块编译出来的assembly和各个模块的配置文件自动放到一个bin平级的plugin目录,然后web应用启动的时候自动扫描这个plugin目录并加载各个模块plugin,这个怎么做到的?大家也许知道,ASP.NET只允许读取Bin目录下的assbmely,不可以读取其他路径,包括Bin\abc等,即使在web.config这样配置probing也不行:(不信你可以试一下)
<configuration> Element <runtime> Element <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <probing privatePath="bin;plugins;"/> </assemblyBinding> </runtime> </configuration>
如何注入菜单
插件能用了,但也想动态注入菜单,这样才实现了自动化,要不还是人工进行菜单注入永远是半自动化,这和我们开发的思想是不想符的,下面就来说一下菜单的注入
1、首称在WeDiscuss.Plugin.Framework 为插件公共类库中建实体类PluginMenu 和PluginMenus
/* * ------------------------------------------------------------------------------- * 功能描述: * * 创建人: JunHan(俊涵) * 创建日期: 2013/12/15 21:59:16 * 创建说明: * * 修改人: * 修改日期: * 修改说明: * * ------------------------------------------------------------------------------- */ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Web.Mvc; namespace WeDiscuss.Plugin.Framework { public class PluginMenus { public List<PluginMenu> MenuList { get; set; } public string CssClass { get; set; } public int MenuType { get; set; } public string Html { get { StringBuilder stringBuilder = new StringBuilder(); foreach (var menu in MenuList) { TagBuilder tagBuilder = new TagBuilder("a"); tagBuilder.MergeAttribute("href", menu.MenuUrl); tagBuilder.InnerHtml = menu.MenuText; tagBuilder.MergeAttribute("class", CssClass); stringBuilder.Append(tagBuilder.ToString(TagRenderMode.Normal) + "\r\n"); } return stringBuilder.ToString(); } } public List<PluginMenu> AvailableList { get { if (MenuList == null) { return new List<PluginMenu>(); } if (MenuType == 0) { return MenuList; } if (!MenuList.Any(o => o.MenuType == MenuType)) { return new List<PluginMenu>(); } return MenuList.Where(o => o.MenuType == MenuType).ToList(); } } } public class PluginMenu { public string MenuText { get; set; } public string MenuUrl { get; set; } public int MenuType { get; set; } public int MenuOrder { get; set; } public bool Visible { get; set; } } }
这样我们就实现了菜单的结构,接下来就是采单的生成或注入方法:
新建 AppPlugin 和 PluginApplication来实现菜单的初使化方法,并将生成好的菜单存放在静态变量中。
/* * ------------------------------------------------------------------------------- * 功能描述: * * 创建人: JunHan(俊涵) * 创建日期: 2013/12/15 23:29:58 * 创建说明: * * 修改人: * 修改日期: * 修改说明: * * ------------------------------------------------------------------------------- */ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace WeDiscuss.Plugin.Framework { public class PluginApplication : BaseMvcPluginApplication { #region Plugin Menu Support public static PluginMenus PluginMenus = new PluginMenus(); public void RegisterMenuItem(PluginMenu menu) { lock (PluginMenus) { if (PluginMenus.MenuList == null) PluginMenus.MenuList = new List<PluginMenu>(); PluginMenus.MenuList.Add(new PluginMenu() { MenuText = menu.MenuText, MenuUrl = menu.MenuUrl, MenuType = menu.MenuType, MenuOrder = menu.MenuOrder }); PluginMenus.MenuList = PluginMenus.MenuList.OrderBy(o => o.MenuOrder).ToList(); } } #endregion public static new PluginApplication Instance { get { return BaseMvcPluginApplication.Instance as PluginApplication; } set { BaseMvcPluginApplication.Instance = value; } } protected override bool ShouldIncludeResourceCore(BaseMvcPluginApplication.ResourceTypes type, IMvcPlugin plugin) { return ShouldIncludeResource(plugin, null); } protected virtual bool ShouldIncludeResource(IMvcPlugin plugin, object resource) { bool should = true; if (plugin != null) { if ((should = plugin.Enabled) && plugin is AppPlugin) should = ((AppPlugin)plugin).ShouldIncludeResource(resource); } return should; } protected override void AddAdditionalRazorViewLocationsCore(List<string> lst) { lst.Add("~/Plugins/PluginDemo/Views.{1}.{0}.cshtml"); } public static PluginApplication SetupApplication(object bundles, object routes) { PluginApplication me = new PluginApplication(bundles, routes); return me; } protected PluginApplication(object bundles, object routes) : base(bundles, routes) { } } public class AppPlugin : BaseMvcPlugin { public PluginApplication _App { get { return (PluginApplication)App; } } public AppPlugin(bool ensureStandardViewLocation = true) : base(ensureStandardViewLocation) { } public void DefineMenuItem(PluginMenu item) { _App.RegisterMenuItem(item); } public virtual bool ShouldIncludeResource(object content) { return true; } } }
2、菜单初使化
在每个插件项目中新建一类,并继承AppPlugin,重写方法:SetupExtensions 调用DefineMenuItem 实现菜单初使化,在菜单的结果中我们看到有MenuType类型,这里我们自定义,一般会用枚举来实现,可以定义为前台或后台等,一个插件可以拥有多个菜单,可以注入多个地方
using System; using System.Collections.Generic; using System.ComponentModel.Composition; using System.Linq; using System.Text; using System.Threading.Tasks; using WeDiscuss.Plugin.Framework; namespace WeDiscuss.Plugin.Album { [Export(typeof(IMvcPlugin))] [MvcPluginMetadata("AlbumPlugin", null, "Demo App Site Album", "")] class AlbumPlugin : AppPlugin { public override void SetupExtensions(IMvcPluginApplication app) { base.SetupExtensions(app); DefineMenuItem(new PluginMenu { MenuText = "相册", MenuUrl = "/Album", MenuType = 1, MenuOrder = 1 }); DefineMenuItem(new PluginMenu { MenuText = "相册管理", MenuUrl = "/ManageAlbum", MenuType = 2, MenuOrder = 1 }); } } }
3、菜单调用
在需要出现插件菜单的地方,我们用以下方法实现菜单的注入并呈现
@using WeDiscuss.Plugin.Framework @{ var menu = PluginApplication.PluginMenus; menu.MenuType = 1; } <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>@ViewBag.Title - 我的 ASP.NET 应用程序</title> @Styles.Render("~/Content/css") @Scripts.Render("~/bundles/modernizr") </head> <body> <div class="navbar navbar-inverse navbar-fixed-top"> <div class="container"> <div class="navbar-header"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> @Html.ActionLink("应用程序名称", "Index", "Home", null, new { @class = "navbar-brand" }) </div> <div class="navbar-collapse collapse"> <ul class="nav navbar-nav"> <li>@Html.ActionLink("主页", "Index", "Home")</li> <li>@Html.ActionLink("关于", "About", "Home")</li> <li>@Html.ActionLink("联系方式", "Contact", "Home")</li> @{ foreach (var item in menu.AvailableList) { <li><a href="@item.MenuUrl">@item.MenuText</a></li> } } </ul> @Html.Partial("_LoginPartial") </div> </div> </div> <div class="container body-content"> @RenderBody() <hr /> <footer> <p>© @DateTime.Now.Year - 我的 ASP.NET 应用程序</p> </footer> </div> @Scripts.Render("~/bundles/jquery") @Scripts.Render("~/bundles/bootstrap") @RenderSection("scripts", required: false) </body> </html>
小结
本文主要讲解了实现方法,并没有讲内部结构是如何实现的,以后的分享中我们会慢慢讲解内部实现逻辑和产品化中的应用和配置。大这有问题或是好的建议想法可以发邮件给我 junhan@wediscuss.cn 如果您修改了代码以实现更好的功能,也烦请转发我一份谢谢!
源码下载
我们站在前辈的肩膀上成长,感谢所有帮助WD成长的人.
源码中没有加入packages,请大家自行加载,源码下载 ,没有密码全部开放!