使用 Griffin.MvcContrib 构建 ASP.NET MVC 3 插件架构
如何使用 Griffin.MvcContrib 创建一个灵活的插件系统。
引言
Griffin.MvcContrib 是 ASP.NET MVC3 的一个贡献项目,它包含各种功能,例如可扩展的 HTML 助手(您可以修改现有助手生成的 HTML)、轻松的本地化支持以及对插件开发的相当新的支持。本文的目的是提供一个关于如何使用 Griffin.MvcContrib 开发插件的分步说明。
使用代码
插件支持基于 MVC3 的 Area(区域)支持。如果您以前没有使用过区域,我建议您首先阅读以下文章。
最基本的方法。
第一个示例实际上并不是一个插件系统,它只是展示了如何将代码移至类库。为避免混淆,该示例将只包含一个类库。首先创建一个新的 ASP.NET MVC3 项目(任何类型)和一个类库项目。为了方便起见,我们希望在类库中支持 Razor 和所有 MVC3 向导。这有助于我们添加一些使一切正常运行所需的基本文件。为了获得此支持,我们需要修改类库的项目文件。方法如下:
- 右键单击类库项目,然后选择“卸载项目”
- 右键单击项目文件,然后选择“编辑项目文件”
- 在 `<ProjectGuid>` 下的新行上添加以下 XML 元素:
<ProjectTypeGuids>{E53F8FEA-EAE0-44A6-8774-FFD645390401};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}</ProjectTypeGuids>
- 保存并关闭项目文件。
- 右键单击并重新加载项目。
- 添加对“System.Web”和“System.Web.Mvc”的引用(项目设置)。
您现在已经告知 Visual Studio 启用工具和 Razor 3 视图。现在,我们只需右键单击项目并选择“添加区域”即可添加一个区域。

执行此操作并创建一个您选择的区域。创建一个控制器(带有一个 Index 操作)和一个用于 Index 操作的视图。现在您已经拥有了 MVC 项目的第一个外部 DLL。
从 MVC3 项目添加对类库的引用

恭喜。您现在已经有了第一个基于“插件”的解决方案。按 F5 运行项目。我给我的区域命名为“Ohh”,我的控制器命名为“My”,因此我访问“http://theHostNameAndPort/ohh/my”来访问我的插件控制器。

哎呀。我们可以访问页面。但是找不到视图。
这可以通过 Griffin.MvcContrib 来解决。让我们在 MVC3 项目中安装该项目

然后打开 `global.asax` 并创建一个新方法来映射视图
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes); RegisterViews(); } protected void RegisterViews() { var embeddedProvider = new EmbeddedViewFileProvider(HostingEnvironment.MapPath("~/"), new ExternalViewFixer()); embeddedProvider.Add(new NamespaceMapping(typeof(Lib.Areas.Some.Controllers.MyController).Assembly, "BasicPlugins.Lib")); GriffinVirtualPathProvider.Current.Add(embeddedProvider); HostingEnvironment.RegisterVirtualPathProvider(GriffinVirtualPathProvider.Current); }
第一行创建一个新的文件提供程序,用于从嵌入式资源加载文件。我们告诉它它应该尝试提供文件的路径。下一行简单地添加我们的类库程序集,并告知其根命名空间。为了使此生效,我们还必须将我们的视图更改为嵌入式资源。

现在一切都应该设置好了。
按 F5 并再次访问区域控制器。您现在应该能看到视图了。

您还可以尝试在类库中设置断点。调试应该像以前一样工作。仍然存在一个“问题”。我们无法在运行时修改视图,每次调整视图时都需要重新编译项目。幸运的是,Griffin.MvcContrib 也可以解决这个问题。再次打开 `global.asax` 中的 `RegisterViews` 方法。让我们添加一个磁盘文件提供程序。它应该在嵌入式提供程序之前添加,因为将使用找到的第一个文件。
var diskLocator = new DiskFileLocator(); diskLocator.Add("~/", Path.GetFullPath(Server.MapPath("~/") + @"..\BasicPlugins.Lib\")); var viewProvider = new ViewFileProvider(diskLocator); GriffinVirtualPathProvider.Current.Add(viewProvider);
您现在可以在运行时编辑视图了。
构建插件系统
上一部分向您展示了如何将控制器、模型和视图放入类库。只要您不想以更松耦合和动态的方式开发功能,这非常有用。本节将建议一个您可以在构建插件时使用的结构。
以下部分假定您过去曾使用过依赖注入容器。该容器用于向所有插件提供应用程序扩展点。我更喜欢使用具有模块系统的容器,以便每个插件都可以处理自己的所有注册。我在本文中使用 Autofac 及其 `Module` 扩展。
让我们将插件和 MVC3 项目之间共享的所有代码放在一个单独的类库中。您也可以遵循分离接口模式,仅在该项目中定义所有接口。从而消除了插件与主应用程序之间的所有直接依赖关系。最后要记住的一件事是,如果不同区域中有同名的控制器,则默认路由配置效果不佳。要克服这个问题,您必须手动更改所有路由映射以包含命名空间。还要从路由名称中删除“_default”。
修改后的注册
public override void RegisterArea(AreaRegistrationContext context) { context.MapRoute( "Messaging_default", "Messaging/{controller}/{action}/{id}", new { action = "Index", id = UrlParameter.Optional }, new[] { GetType().Namespace + ".Controllers" } ); }
创建一个名为“YourApp.PluginBase”之类的新类库,并添加我们的基本扩展点
public interface IRouteRegistrar { void Register(RouteTable routes); }
用于注册任何自定义路由。
public interface IMenuRegistrar { void Register(IMenuWithChildren mainMenu); }
允许插件在主应用程序菜单中注册自己。
在此示例中,我们仅使用这两个扩展。请随时在您自己的项目中添加任意数量的扩展 " src="https://codeproject.org.cn/script/Forums/Images/smiley_wink.gif" />
结构
我们需要为插件提供某种结构,以便在开发和生产中都可以轻松管理它们。因此,所有插件都将放置在一个名为“Plugins”的子目录中。类似这样
ProjectName\Plugins\ ProjectName\Plugins\PluginName ProjectName\Plugins\PluginName\Plugin.PluginName ProjectName\Plugins\PluginName\Plugin.PluginName.Tests ProjectName\ProjectName.Mvc3
通过右键单击解决方案中的解决方案文件夹来添加新的解决方案文件夹。请注意,解决方案文件夹不在磁盘上,因此您每次向其中添加新项目时都必须手动将文件夹附加到位置文本框。

右键单击解决方案资源管理器中的 Plugins 文件夹,然后添加一个名为“PluginName”的新项目。不要忘记在位置文本框中附加 `\Plugins\Plugin.Name\`。
由于这次我们不希望主应用程序对插件有任何引用,因此我们必须手动将它们复制到主应用程序的插件文件夹中。我们使用生成后事件来完成此操作。不要忘记复制项目的所有依赖项,因为没有任何东西会为您处理这些(与 MVC3 项目对插件没有直接引用付出的代价)。

完整结构

菜单项
请记住,您无法卸载插件,并且所有用户都可以使用它们。最简单的解决方法是仅当用户具有某个角色时才显示菜单项,这意味着您应该为每个插件创建一个(或多个)角色(如果插件不应供所有用户使用)。Griffin.MvcContrib 包含一个基本的菜单实现,允许您控制菜单项是否可见。
示例代码
var item = new RouteMenuItem("mnuMyPluginIndex", "List messages", new { controller = "Home", action = "Index", area = "Messages"}); item.Role = "MessageViewer"; menuRegistrar.Add(item);
以后在生成菜单时,您可以使用 `menuItem.IsVisible` 来确定是否包含该项。
你好容器
在这个练习中,我们将使用 Autofac 作为容器。它包含一个巧妙的模块系统,有助于我们保持项目松耦合。每个插件都需要创建一个继承自 `Autofac.Module` 的类,并使用该类注册插件提供的所有服务。有了这个,我们只需要扫描所有插件程序集以查找任何容器模块,并使用我们的容器调用它们。
public class ContainerModule : Module { protected override void Load(ContainerBuilder builder) { builder.RegisterType<MenuRegistrar>().AsImplementedInterfaces().SingleInstance(); builder.RegisterType<HelloService>().AsImplementedInterfaces().InstancePerLifetimeScope(); base.Load(builder); } }
插件通过以下代码片段从主应用程序加载
var moduleType = typeof (IModule); var modules = plugin.GetTypes().Where(moduleType.IsAssignableFrom); foreach (var module in modules) { var mod = (IModule) Activator.CreateInstance(module); builder.RegisterModule(mod); }
开发期间的视图
由于我们希望在开发期间能够在运行时修改视图,因此我们必须告诉 ASP.NET MVC3 在哪里可以找到我们的插件视图。我们通过使用 GriffinVirtualPathProvider 和一个自定义文件定位器来实现,如下所示:
public class PluginFileLocator : IViewFileLocator { public PluginFileLocator() { _basePath = Path.GetFullPath(HostingEnvironment.MapPath("~") + @"..\Plugins\"); } public string GetFullPath(string uri) { var fixedUri = uri; if (fixedUri.StartsWith("~")) fixedUri = VirtualPathUtility.ToAbsolute(uri); if (!fixedUri.StartsWith("/Areas", StringComparison.OrdinalIgnoreCase)) return null; // extract area name: var pos = fixedUri.IndexOf('/', 7); if (pos == -1) return null; var areaName = fixedUri.Substring(7, pos - 7); var path = string.Format("{0}{1}\\Plugin.{1}{2}", _basePath, areaName, fixedUri.Replace('/', '\\')); return File.Exists(path) ? path : null; } }
它只需获取请求的 URI,并使用我上面描述的命名约定进行转换。其他所有内容都由 Griffin.MvcContrib 处理。
有趣的是,由于 Griffin.MvcContrib 中有一个巧妙的辅助类,该提供程序仅在开发期间加载
if (VisualStudioHelper.IsInVisualStudio) GriffinVirtualPathProvider.Current.Add(_diskFileProvider);
加载插件
好的。我们创建了一些插件(及其依赖项),它们通过生成后事件被复制到 bin 文件夹。因此,我们必须以某种方式加载它们。为此,我们创建一个如下所示的新类
public class PluginService { private static PluginFinder _finder; private readonly DiskFileLocator _diskFileLocator = new DiskFileLocator(); private readonly EmbeddedViewFileProvider _embededProvider = new EmbeddedViewFileProvider(new ExternalViewFixer()); private readonly PluginFileLocator _fileLocator = new PluginFileLocator(); private readonly ViewFileProvider _diskFileProvider; public PluginService() { _diskFileProvider = new ViewFileProvider(_fileLocator, new ExternalViewFixer()); if (VisualStudioHelper.IsInVisualStudio) GriffinVirtualPathProvider.Current.Add(_diskFileProvider); GriffinVirtualPathProvider.Current.Add(_embededProvider); } public static void PreScan() { _finder = new PluginFinder("~/bin/"); _finder.Find(); } public void Startup(ContainerBuilder builder) { foreach (var assembly in _finder.Assemblies) { // Views handling _embededProvider.Add(new NamespaceMapping(assembly, Path.GetFileNameWithoutExtension(assembly.Location))); _diskFileLocator.Add("~/", Path.GetFullPath(HostingEnvironment.MapPath("~/") + @"\..\..\" + Path.GetFileNameWithoutExtension(assembly.Location))); //Autofac integration builder.RegisterControllers(assembly); var moduleType = typeof (IModule); var modules = assembly.GetTypes().Where(moduleType.IsAssignableFrom); foreach (var module in modules) { var mod = (IModule) Activator.CreateInstance(module); builder.RegisterModule(mod); } } } // invoke extension points public void Integrate(IContainer container) { foreach (var registrar in container.Resolve<IEnumerable<IMenuRegistrar>>()) { registrar.Register(MainMenu.Current); } foreach (var registrar in container.Resolve<IEnumerable<IRouteRegistrar>>()) { registrar.Register(RouteTable.Routes); } } }
插件现在已加载,并且扩展点已传递给它们。
最后的润色
最后要更改的是 `global.asax`
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes); _pluginServicee = new PluginService(); RegisterContainer(); HostingEnvironment.RegisterVirtualPathProvider(GriffinVirtualPathProvider.Current); _pluginServicee.Integrate(_container); } private void RegisterContainer() { var builder = new ContainerBuilder(); builder.RegisterControllers(Assembly.GetExecutingAssembly()); _pluginServicee.Startup(builder); _container = builder.Build(); DependencyResolver.SetResolver(new AutofacDependencyResolver(_container)); }
历史
- 2012-05-24 首次修订
- 2012-11-26 修复了 ProjectTypeGuid 格式
代码
示例代码位于github,Griffin.MvcContrib 也是如此。Griffin.MvcContrib 也可以从 nuget 安装。