基於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,請大家自行載入,原始碼下載 ,沒有密碼全部開放!