[转载]基于ASP.NET MVC3 Razor的模块化/插件式架构实现

[转载]基于ASP.NET MVC3 Razor的模块化/插件式架构实现 – Mainz – 博客园.

本文主要探讨了一种基于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)

3-5-2012 5-07-19 PM

其 中,业务模块(class library project)包含其所有的视图、控制器等,模型可以放在里面也可以单独放一个project。主web项目没有引用业务模块,业务模块会编译到主 web项目的~/plugin目录下面(注意:不是bin目录),然后当web应用启动的时候自动加载plugin目录下面的模块。最后运行起来的效果如 下图:

3-6-2012 10-28-51 AM

其中红色的区域都是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

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: ///

&nbsp;

&nbsp;

&nbsp;

<summary>14: /// Gets a value that indicates whether a file exists in the virtual file system.15: /// </summary>

&nbsp;

&nbsp;

&nbsp;

&nbsp;

16: ///

17: /// true if the file exists in the virtual file system; otherwise, false.

18: ///

19: ///The path to the virtual file.

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: ///

&nbsp;

&nbsp;

&nbsp;

<summary>33: /// Gets a virtual file from the virtual file system.34: /// </summary>

&nbsp;

&nbsp;

&nbsp;

&nbsp;

35: ///

36: /// A descendent of theclass that represents a file in the virtual file system.

37: ///

38: ///The path to the virtual file.

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()

60: where GetCompiledType(vp) == null

61: select vp

62: , utcStart);

63: }

64:

65: }

66: }

代码:容器Dictionary保存已编译的视图和虚拟路径,例如path <~/views/team/index.cshtml> type ,路径注册以后,会从容器库全局搜索所有注册过的视图,也就是说即使你视图引用的_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 DictionaryregisteredPaths = new Dictionary();

15:

16: ///

&nbsp;

&nbsp;

&nbsp;

<summary>17: ///18: /// </summary>

&nbsp;

&nbsp;

&nbsp;

&nbsp;

19: ///

20: ///

21: public virtual Type GetCompiledType(string virtualPath)

22: {

23: if (virtualPath == null) throw new ArgumentNullException("virtualPath");

24:

25: //Debug.WriteLine(String.Format("---GetCompiledType : virtualPath", 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, path", applicationPart.FullName, rootVirtualPath));

45:

46: foreach (var type in applicationPart.GetTypes().Where(type =&gt; type.IsSubclassOf(webPageType)))

47: {

48: //Debug.WriteLine(String.Format("-----Register type, path", 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().SingleOrDefault();

62: if (attribute != null)

63: {

64: var rootRelativeVirtualPath = GetRootRelativeVirtualPath(rootVirtualPath ?? "", attribute.VirtualPath);

65:

66: //Debug.WriteLine(String.Format("---Register path/type : pathtype", 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 =&gt; 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 =&gt; 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,接口

dddd

模块配置文件{projectName}.config的位置:

Untitled

为什么每个模块的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的”插件式”开发》。另外,有很多框架提供了模块化/插件开发的框架,例如PrismMEF(Managed Extensibility Framework,.NET 4.0 内置)等。

客户端插件架构

还有一种插件架构是客户端插件架构(JavaScript 模块化),如jQuery UI Widget FactorySilk Project就是很好的例子。

本文源码下载

源码下载请点击此处。运行源码之前务必阅读此文和本文。注意:本文抛砖引玉,力求简单,易懂,并非完整的架构实现,更多丰富的功能实现一切皆有可能,只要在理解的基础上。

赞(0) 打赏
分享到: 更多 (0)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏