本文主要探討了一種基於ASP.NET MVC3 Razor的模塊化(Plugin)/插件(plugin)式架構的實現方法。本文借鑒了《Compile your asp.net mvc Razor views into a seperate dll》作者提供的方法。
項目管家 - 專注於項目管理軟件,項目管理工具和項目管理系統,讓團隊協作更高效
敬請注意。其實ASP.NET MVC的模塊化(Plugin)/插件(plugin)式架構討論的很多,但基於Razor視圖引擎的很少(如:MVC2插件架構例子都是基於WebForm的,MVCContrib Portable Areas也是,還有這個Plugin架構)。要么就是非常復雜非常重量級的框架,例如Orchard CMS的模塊化做的很好,可惜太重量級了,也沒獨立的模塊可以剝離出來。所以我們追尋的是簡單的基於ASP.NET MVC3 Razor的模塊化(Plugin)/插件(plugin)式架構的實現方法。本文最后實現的項目結構如下圖:(插件都放到~/Plugin目錄下,按功能划分模塊,每個模塊都有M,V,C)
其中,業務模塊(class library project)包含其所有的視圖、控制器等,模型可以放在里面也可以單獨放一個project。主web項目沒有引用業務模塊,業務模塊會編譯到主web項目的~/plugin目錄下面(注意:不是bin目錄),然后當web應用啟動的時候自動加載plugin目錄下面的模塊。最后運行起來的效果如下圖:
其中紅色的區域都是plugin進去的,那個tab的標題plugin到母版頁的主菜單,tab內容也來自plugin。下面說說如何實現這樣的ASP.NET MVC插件式plugin架構(模塊化架構)。
實現的難點在動態加載UI視圖(*.cshtml, _layout.cshtml, _viewStart.cshtml)
廢話少說,直入要害。基於ASP.NET MVC3 Razor的編譯發生在兩個層面:
- 控制器(Controller), 模型(Models),和其它所有的C#代碼等有msbuild(或者VisualStudio)編譯到bin目錄下的程序集(assembly)
- 視圖(*.aspx, *.cshtml)由ASP.NET在運行時動態編譯。當一個Razor視圖(*.cshtml)顯示前,Razor視圖引擎調用BuildManager把視圖(*.cshtml)編譯到動態程序集assembly,然后使用Activator.CreateInstance來實例化新編譯出來的對象,最后顯示出來。如果視圖(*.cshtml)用到@model綁定model,那么還會自動加載bin或者GAC里面的Model。
所以如果我們要動態加載插件(plugin),用反射bin目錄下的程序集(assembly)的方法很容易搞定上面的第一部分(C#代碼的部分),但UI視圖的部分(上面第二部分)(特別是*.cshtml, 母版_layout.cshtml, 基視圖_viewStart.cshtml)就比較難搞定。而且每次報錯都是一樣的,那就是Controller找不到相應的視圖View,基本不知所雲而且根本不是要點:view …. or its master was not found or no view engine supports the searched locations. The following locations were searched: …,因此要搞定UI視圖的部分(上面第二部分)(特別是*.cshtml, 母版_layout.cshtml, 基視圖_viewStart.cshtml),就需要自己動手了,基本原理是:
- 重載RazorBuildProvider,用來動態編譯視圖
- 實現一個自定義VirtualPathProvider,從虛擬路徑自定義判斷讀取資源(從插件中加載資源),如果要使用編譯的視圖就返回編譯的VirtualFile
- 實現一個容器Dictionary保存已編譯的視圖和虛擬路徑,例如path <~/views/team/index.cshtml> type <Area.Module2.Views.Team._Page_Views_Team_Index_cshtml>,或者path <~/views/_viewstart.cshtml> type <Area.Module1.Views._Page_Views__ViewStart_cshtml>
代碼:自定義VirtualPathProvider,從虛擬路徑自定義判斷讀取資源(從插件中加載資源),如果要使用編譯的視圖就返回編譯的VirtualFile
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Reflection;
5: using System.Web.Caching;
6: using System.Web.Hosting;
7: using System.Web.WebPages;
8:
9: namespace Common.Framework
10: {
11: public class CompiledVirtualPathProvider: VirtualPathProvider
12: {
13: /// <summary>
14: /// Gets a value that indicates whether a file exists in the virtual file system.
15: /// </summary>
16: /// <returns>
17: /// true if the file exists in the virtual file system; otherwise, false.
18: /// </returns>
19: /// <param name="virtualPath">The path to the virtual file.</param>
20: public override bool FileExists(string virtualPath)
21: {
22: return
23: GetCompiledType(virtualPath) != null
24: || Previous.FileExists(virtualPath);
25: }
26:
27: public Type GetCompiledType(string virtualPath)
28: {
29: return ApplicationPartRegistry.Instance.GetCompiledType(virtualPath);
30: }
31:
32: /// <summary>
33: /// Gets a virtual file from the virtual file system.
34: /// </summary>
35: /// <returns>
36: /// A descendent of the <see cref="T:System.Web.Hosting.VirtualFile"/> class that represents a file in the virtual file system.
37: /// </returns>
38: /// <param name="virtualPath">The path to the virtual file.</param>
39: public override VirtualFile GetFile(string virtualPath)
40: {
41: if (Previous.FileExists(virtualPath))
42: {
43: return Previous.GetFile(virtualPath);
44: }
45: var compiledType = GetCompiledType(virtualPath);
46: if (compiledType != null)
47: {
48: return new CompiledVirtualFile(virtualPath, compiledType);
49: }
50: return null;
51: }
52:
53: public override System.Web.Caching.CacheDependency GetCacheDependency(string virtualPath, System.Collections.IEnumerable virtualPathDependencies, DateTime utcStart)
54: {
55: if (virtualPathDependencies == null)
56: return Previous.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
57:
58: return Previous.GetCacheDependency(virtualPath,
59: from vp in virtualPathDependencies.Cast<string>()
60: where GetCompiledType(vp) == null
61: select vp
62: , utcStart);
63: }
64:
65: }
66: }
代碼:容器Dictionary保存已編譯的視圖和虛擬路徑,例如path <~/views/team/index.cshtml> type <Area.Module2.Views.Team._Page_Views_Team_Index_cshtml>,路徑注冊以后,會從容器庫全局搜索所有注冊過的視圖,也就是說即使你視圖引用的_layout.cshtml和_viewStart.cshtml在其他的Class library project照樣可以找到。
1: using System;
2: using System.Collections.Generic;
3: using System.Diagnostics;
4: using System.Linq;
5: using System.Reflection;
6: using System.Web;
7: using System.Web.WebPages;
8:
9: namespace Common.Framework
10: {
11: public class DictionaryBasedApplicationPartRegistry : IApplicationPartRegistry
12: {
13: private static readonly Type webPageType = typeof(WebPageRenderingBase);
14: private readonly Dictionary<string, Type> registeredPaths = new Dictionary<string, Type>();
15:
16: /// <summary>
17: ///
18: /// </summary>
19: /// <param name="virtualPath"></param>
20: /// <returns></returns>
21: public virtual Type GetCompiledType(string virtualPath)
22: {
23: if (virtualPath == null) throw new ArgumentNullException("virtualPath");
24:
25: //Debug.WriteLine(String.Format("---GetCompiledType : virtualPath <{0}>", virtualPath));
26:
27: if (virtualPath.StartsWith("/"))
28: virtualPath = VirtualPathUtility.ToAppRelative(virtualPath);
29: if (!virtualPath.StartsWith("~"))
30: virtualPath = !virtualPath.StartsWith("/") ? "~/" + virtualPath : "~" + virtualPath;
31: virtualPath = virtualPath.ToLower();
32: return registeredPaths.ContainsKey(virtualPath)
33: ? registeredPaths[virtualPath]
34: : null;
35: }
36:
37: public void Register(Assembly applicationPart)
38: {
39: ((IApplicationPartRegistry)this).Register(applicationPart, null);
40: }
41:
42: public virtual void Register(Assembly applicationPart, string rootVirtualPath)
43: {
44: //Debug.WriteLine(String.Format("---Register assembly <{0}>, path <{1}>", applicationPart.FullName, rootVirtualPath));
45:
46: foreach (var type in applicationPart.GetTypes().Where(type => type.IsSubclassOf(webPageType)))
47: {
48: //Debug.WriteLine(String.Format("-----Register type <{0}>, path <{1}>", type.FullName, rootVirtualPath));
49:
50: ((IApplicationPartRegistry)this).RegisterWebPage(type, rootVirtualPath);
51: }
52: }
53:
54: public void RegisterWebPage(Type type)
55: {
56: ((IApplicationPartRegistry)this).RegisterWebPage(type, string.Empty);
57: }
58:
59: public virtual void RegisterWebPage(Type type, string rootVirtualPath)
60: {
61: var attribute = type.GetCustomAttributes(typeof(PageVirtualPathAttribute), false).Cast<PageVirtualPathAttribute>().SingleOrDefault<PageVirtualPathAttribute>();
62: if (attribute != null)
63: {
64: var rootRelativeVirtualPath = GetRootRelativeVirtualPath(rootVirtualPath ?? "", attribute.VirtualPath);
65:
66: //Debug.WriteLine(String.Format("---Register path/type : path <{0}> type <{1}>", rootRelativeVirtualPath.ToLower(),
67: // type.FullName));
68: registeredPaths[rootRelativeVirtualPath.ToLower()] = type;
69: }
70: }
71:
72: static string GetRootRelativeVirtualPath(string rootVirtualPath, string pageVirtualPath)
73: {
74: string relativePath = pageVirtualPath;
75: if (relativePath.StartsWith("~/", StringComparison.Ordinal))
76: {
77: relativePath = relativePath.Substring(2);
78: }
79: if (!rootVirtualPath.EndsWith("/", StringComparison.OrdinalIgnoreCase))
80: {
81: rootVirtualPath = rootVirtualPath + "/";
82: }
83: relativePath = VirtualPathUtility.Combine(rootVirtualPath, relativePath);
84: if (!relativePath.StartsWith("~"))
85: {
86: return !relativePath.StartsWith("/") ? "~/" + relativePath : "~" + relativePath;
87: }
88: return relativePath;
89: }
90: }
91: }
下面的代碼很關鍵,用PreApplicationStartMethod關鍵字(.NET 4.0開始支持)使得代碼在Application_Start之前執行。
有關[assembly: PreApplicationStartMethod(typeof(SomeClassLib.Initializer), "Initialize")]詳細信息請參考這個頁面和這個頁面。
1: using System.Web;
2: using System.Web.Compilation;
3: using System.Web.Hosting;
4: using Common.Framework;
5: using Common.PrecompiledViews;
6:
7: [assembly: PreApplicationStartMethod(typeof(PreApplicationStartCode), "Start")]
8:
9: namespace Common.Framework
10: {
11: public static class PreApplicationStartCode
12: {
13: private static bool _startWasCalled;
14:
15: public static void Start()
16: {
17: if (_startWasCalled)
18: {
19: return;
20: }
21: _startWasCalled = true;
22:
23: //Register virtual paths
24: HostingEnvironment.RegisterVirtualPathProvider(new CompiledVirtualPathProvider());
25:
26: //Load Plugin Folder,
27: PluginLoader.Initialize();
28: }
29: }
30: }
代碼:PluginLoader,加載plugin目錄里面的東東(assembly和module配置文件)
1: using System;
2: using System.Collections.Generic;
3: using System.IO;
4: using System.Linq;
5: using System.Reflection;
6: using System.Text;
7: using System.Threading;
8: using System.Web;
9: using System.Web.Compilation;
10: using System.Web.Hosting;
11: using Common.Framework;
12: using Common.PrecompiledViews;
13:
14: //[assembly: PreApplicationStartMethod(typeof(PluginLoader), "Initialize")]
15:
16: namespace Common.PrecompiledViews
17: {
18: public class PluginLoader
19: {
20: public static void Initialize(string folder = "~/Plugin")
21: {
22: LoadAssemblies(folder);
23: LoadConfig(folder);
24: }
25:
26: private static void LoadConfig(string folder, string defaultConfigName="*.config")
27: {
28: var directory = new DirectoryInfo(HostingEnvironment.MapPath(folder));
29: var configFiles = directory.GetFiles(defaultConfigName, SearchOption.AllDirectories).ToList();
30: if (configFiles.Count == 0) return;
31:
32: foreach (var configFile in configFiles.OrderBy(s => s.Name))
33: {
34: ModuleConfigContainer.Register(new ModuleConfiguration(configFile.FullName));
35: }
36: }
37:
38: private static void LoadAssemblies(string folder)
39: {
40: var directory = new DirectoryInfo(HostingEnvironment.MapPath(folder));
41: var binFiles = directory.GetFiles("*.dll", SearchOption.AllDirectories).ToList();
42: if (binFiles.Count == 0) return;
43:
44: foreach (var plug in binFiles)
45: {
46: //running in full trust
47: //************
48: //if (GetCurrentTrustLevel() != AspNetHostingPermissionLevel.Unrestricted)
49: //set in web.config, probing to plugin\temp and copy all to that folder
50: //************************
51: var shadowCopyPlugFolder = new DirectoryInfo(AppDomain.CurrentDomain.DynamicDirectory);
52: var shadowCopiedPlug = new FileInfo(Path.Combine(shadowCopyPlugFolder.FullName, plug.Name));
53: File.Copy(plug.FullName, shadowCopiedPlug.FullName, true); //TODO: Exception handling here...
54: var shadowCopiedAssembly = Assembly.Load(AssemblyName.GetAssemblyName(shadowCopiedPlug.FullName));
55:
56: //add the reference to the build manager
57: BuildManager.AddReferencedAssembly(shadowCopiedAssembly);
58: }
59: }
60:
61: //private static AspNetHostingPermissionLevel GetCurrentTrustLevel()
62: //{
63: // foreach (AspNetHostingPermissionLevel trustLevel in
64: // new AspNetHostingPermissionLevel[]
65: // {
66: // AspNetHostingPermissionLevel.Unrestricted,
67: // AspNetHostingPermissionLevel.High,
68: // AspNetHostingPermissionLevel.Medium,
69: // AspNetHostingPermissionLevel.Low,
70: // AspNetHostingPermissionLevel.Minimal
71: // })
72: // {
73: // try
74: // {
75: // new AspNetHostingPermission(trustLevel).Demand();
76: // }
77: // catch (System.Security.SecurityException)
78: // {
79: // continue;
80: // }
81:
82: // return trustLevel;
83: // }
84:
85: // return AspNetHostingPermissionLevel.None;
86: //}
87:
88: }
89: }
此外,使用SingleFileGenerator的優點是性能提升,缺點是修改了視圖就要重新編譯。
如何讓ASP.NET加載BIN目錄之外的路徑的Assembly
我們把各個模塊編譯出來的assembly和各個模塊的配置文件自動放到一個bin平級的plugin目錄,然后web應用啟動的時候自動掃描這個plugin目錄並加載各個模塊plugin,這個怎么做到的?大家也許知道,ASP.NET只允許讀取Bin目錄下的assbmely,不可以讀取其他路徑,包括Bin\abc等,即使在web.config這樣配置probing也不行:(不信你可以試一下)
1: <configuration> Element
2: <runtime> Element
3: <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
4: <probing privatePath="bin;bin\abc;plugin;"/>
5: </assemblyBinding>
6: </runtime>
7: </configuration>
這個和TrustLevel有關,在Full Trust的情況下,可以這樣讀取非Bin目錄下的assembly:
首先在和Bib平級的地方建一個目錄Plugin,然后在模塊class library project的屬性里面加一個postBuildEvent,就是說在編譯完成以后把模塊的assbmely自動拷貝到主web項目的plugin目錄:
1: copy /Y "$(TargetDir)$(ProjectName).dll" "$(SolutionDir)ModularWebApplication\Plugin\"
2: copy /Y "$(TargetDir)$(ProjectName).config" "$(SolutionDir)ModularWebApplication\Plugin\"
3:
然后用下面的代碼加載Plugin目錄下的assembly:(只看LoadAssembly那一段)
1: using System;
2: using System.Collections.Generic;
3: using System.IO;
4: using System.Linq;
5: using System.Reflection;
6: using System.Text;
7: using System.Threading;
8: using System.Web;
9: using System.Web.Compilation;
10: using System.Web.Hosting;
11: using Common.Framework;
12: using Common.PrecompiledViews;
13:
14: //[assembly: PreApplicationStartMethod(typeof(PluginLoader), "Initialize")]
15:
16: namespace Common.PrecompiledViews
17: {
18: public class PluginLoader
19: {
20: public static void Initialize(string folder = "~/Plugin")
21: {
22: LoadAssemblies(folder);
23: LoadConfig(folder);
24: }
25:
26: private static void LoadConfig(string folder, string defaultConfigName="*.config")
27: {
28: var directory = new DirectoryInfo(HostingEnvironment.MapPath(folder));
29: var configFiles = directory.GetFiles(defaultConfigName, SearchOption.AllDirectories).ToList();
30: if (configFiles.Count == 0) return;
31:
32: foreach (var configFile in configFiles.OrderBy(s => s.Name))
33: {
34: ModuleConfigContainer.Register(new ModuleConfiguration(configFile.FullName));
35: }
36: }
37:
38: private static void LoadAssemblies(string folder)
39: {
40: var directory = new DirectoryInfo(HostingEnvironment.MapPath(folder));
41: var binFiles = directory.GetFiles("*.dll", SearchOption.AllDirectories).ToList();
42: if (binFiles.Count == 0) return;
43:
44: foreach (var plug in binFiles)
45: {
46: //running in full trust
47: //************
48: //if (GetCurrentTrustLevel() != AspNetHostingPermissionLevel.Unrestricted)
49: //set in web.config, probing to plugin\temp and copy all to that folder
50: //************************
51: var shadowCopyPlugFolder = new DirectoryInfo(AppDomain.CurrentDomain.DynamicDirectory);
52: var shadowCopiedPlug = new FileInfo(Path.Combine(shadowCopyPlugFolder.FullName, plug.Name));
53: File.Copy(plug.FullName, shadowCopiedPlug.FullName, true); //TODO: Exception handling here...
54: var shadowCopiedAssembly = Assembly.Load(AssemblyName.GetAssemblyName(shadowCopiedPlug.FullName));
55:
56: //add the reference to the build manager
57: BuildManager.AddReferencedAssembly(shadowCopiedAssembly);
58: }
59: }
60:
61: //private static AspNetHostingPermissionLevel GetCurrentTrustLevel()
62: //{
63: // foreach (AspNetHostingPermissionLevel trustLevel in
64: // new AspNetHostingPermissionLevel[]
65: // {
66: // AspNetHostingPermissionLevel.Unrestricted,
67: // AspNetHostingPermissionLevel.High,
68: // AspNetHostingPermissionLevel.Medium,
69: // AspNetHostingPermissionLevel.Low,
70: // AspNetHostingPermissionLevel.Minimal
71: // })
72: // {
73: // try
74: // {
75: // new AspNetHostingPermission(trustLevel).Demand();
76: // }
77: // catch (System.Security.SecurityException)
78: // {
79: // continue;
80: // }
81:
82: // return trustLevel;
83: // }
84:
85: // return AspNetHostingPermissionLevel.None;
86: //}
87:
88: }
89: }
如果不是Full Trust,例如Medium Trust的情況下參考這個帖子《Developing-a-plugin-framework-in-ASPNET-with-medium-trust》。
如何在_layout.cshtml的主菜單注入plugin的菜單
在母版頁_layout.cshtml有個主菜單,一般是這樣寫的:
1: <ul>
2: <li>@Html.ActionLink("Home", "Index", "Home")</li>
3: <li>@Html.ActionLink("About", "About", "Home")</li>
4: <li>@Html.ActionLink("Team", "Index", "Team")</li>
5: </ul>
現在我們如何實現從模塊插入plugin到這個主菜單呢?這個有點難。因為大家知道,_layout.cshml母版沒有controller。怎么實現呢?方法是用controller基類,讓所有controller繼承自這個基類。然后在基類里面,讀取plugin目錄里面的配置文件,獲取所有模塊需要插入的主菜單項,然后放入viewBag,這樣在_Layout.cshtml就可以獲取viewBag,類似這樣:
1: <ul>
2: @foreach (MainMenuItemModel entry in ViewBag.MainMenuItems)
3: {
4: <li>@Html.ActionLink(entry.Text,
5: entry.ActionName,
6: entry.ControllerName)</li>
7: }
8: </ul>
代碼:基類Controller,讀取plugin目錄里面的配置文件,獲取所有模塊需要插入的主菜單項,然后放入viewBag
1: using System;
2: using System.Collections;
3: using System.Collections.Generic;
4: using System.ComponentModel;
5: using System.Linq;
6: using System.Net.Mime;
7: using System.Text;
8: using System.Web.Mvc;
9:
10: namespace Common.Framework
11: {
12: public class BaseController : Controller
13: {
14: protected override void Initialize(System.Web.Routing.RequestContext requestContext)
15: {
16: base.Initialize(requestContext);
17:
18: // retireve data from plugins
19: IEnumerable<ModuleConfiguration> ret = ModuleConfigContainer.GetConfig();
20:
21: var data = (from c in ret
22: from menu in c.MainMenuItems
23: select new MainMenuItemModel
24: {
25: Id = menu.Id, ActionName = menu.ActionName, ControllerName = menu.ControllerName, Text = menu.Text
26: }).ToList();
27:
28: ViewBag.MainMenuItems = data.AsEnumerable();
29: }
30:
31: }
32: }
代碼:ModuleConfigContainer,用到單例模式,只讀取一次
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Text;
5:
6: namespace Common.Framework
7: {
8: public static class ModuleConfigContainer
9: {
10: static ModuleConfigContainer()
11: {
12: Instance = new ModuleConfigDictionary();
13: }
14:
15: internal static IModuleConfigDictionary Instance { get; set; }
16:
17: public static void Register(ModuleConfiguration item)
18: {
19: Instance.Register(item);
20: }
21:
22: public static IEnumerable<ModuleConfiguration> GetConfig()
23: {
24: return Instance.GetConfigs();
25: }
26: }
27: }
代碼:ModuleConfigDictionary
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Text;
5:
6: namespace Common.Framework
7: {
8: public class ModuleConfigDictionary : IModuleConfigDictionary
9: {
10: private readonly Dictionary<string, ModuleConfiguration> _configurations = new Dictionary<string, ModuleConfiguration>();
11:
12: public IEnumerable<ModuleConfiguration> GetConfigs()
13: {
14: return _configurations.Values.AsEnumerable();
15: }
16:
17: public void Register(ModuleConfiguration item)
18: {
19: if(_configurations.ContainsKey(item.ModuleName))
20: {
21: _configurations[item.ModuleName] = item;
22: }
23: else
24: {
25: _configurations.Add(item.ModuleName, item);
26: }
27: }
28: }
29: }
代碼:ModuleConfiguration,讀取模塊的配置文件
1: using System;
2: using System.Collections.Generic;
3: using System.IO;
4: using System.Linq;
5: using System.Text;
6: using System.Xml;
7: using System.Xml.Linq;
8:
9: namespace Common.Framework
10: {
11: public class ModuleConfiguration
12: {
13: public ModuleConfiguration(string filePath)
14: {
15: try
16: {
17: var doc = XDocument.Load(filePath);
18: var root = XElement.Parse(doc.ToString());
19:
20: if (!root.HasElements) return;
21:
22: var module = from e in root.Descendants("module")
23: //where e.Attribute("name").Value == "xxxx"
24: select e;
25:
26: if (!module.Any()) return;
27:
28: ModuleName = module.FirstOrDefault().Attribute("name").Value;
29:
30: var menus = from e in module.FirstOrDefault().Descendants("menu")
31: select e;
32:
33: if (!menus.Any()) return;
34:
35: var menuitems = menus.Select(xElement => new MainMenuItemModel
36: {
37: Id = xElement.Attribute("id").Value,
38: Text = xElement.Attribute("text").Value,
39: ActionName = xElement.Attribute("action").Value,
40: ControllerName = xElement.Attribute("controller").Value
41: }).ToList();
42:
43: MainMenuItems = menuitems;
44: }
45: catch
46: {
47: //TODO: logging
48: }
49: }
50: public string ModuleName { get; set; }
51: public IEnumerable<MainMenuItemModel> MainMenuItems { get; set; }
52: }
53: }
每個模塊的配置文件為{projectName}.config,格式如下:
1: <?xml version="1.0" encoding="utf-8" ?>
2: <configuration>
3: <module name="Module2">
4: <mainmenu>
5: <menu id="modul2" text="Team" action="Index" controller="Team"/>
6: </mainmenu>
7: </module>
8: </configuration>
為了簡單起見,只保留了注入主菜單的部分,為了讓讀者簡單易懂。明白了以后你自己可以任意擴展…
代碼:IModuleConfigDictionary,接口
模塊配置文件{projectName}.config的位置:
為什么每個模塊的Class library project都需要一個web.config呢?因為如果沒有這個,那就沒有Razor智能提示,大家可以參考這篇文章《How to get Razor intellisense for @model in a class library project》。
閑話幾句插件式架構(Plugin Architecture)或者模塊化(Modular)架構
插件式架構(Plugin Architecture)或者模塊化(Modular)架構是大型應用必須的架構,關於什么是Plugin,什么是模塊化模式,這種架構的優缺點等我就不說了,自己百谷歌度。關於.NET下面的插件式架構和模塊化開發實現方法,基本上用AppDomain實現,當檢測到一個新的插件Plugin時,實例化一個新的AppDomain並加載Assembly反射類等,由於AppDomain很好的隔離各個Plugin,所以跨域通信要用MarshalByRefObject類,具體做法可以參考這篇文章《基於AppDomain的"插件式"開發》。另外,有很多框架提供了模塊化/插件開發的框架,例如Prism、MEF(Managed Extensibility Framework,.NET 4.0 內置)等。
客戶端插件架構
還有一種插件架構是客戶端插件架構(Javascript 模塊化),如jQuery UI Widget Factory,Silk Project就是很好的例子。
本文源碼下載
源碼下載請點擊此處。運行源碼之前務必閱讀此文和本文。注意:本文拋磚引玉,力求簡單,易懂,並非完整的架構實現,更多豐富的功能實現一切皆有可能,只要在理解的基礎上。