ASP.NET MVC 插件框架
介绍如何创建一个ASP.NET MVC插件框架,每个插件可以放在一个单独的文件夹中,并且可以动态地安装/卸载插件。
引言
在本文中,我想分享我的ASP.NET MVC插件框架,它主要包含以下功能:
- 每个插件可以部署在单独的文件夹中;无需将插件程序集复制到bin文件夹,插件文件保持与普通网站相同的结构;
- 在网站运行后动态地安装/卸载插件;
- 插件共享相同的母版页/布局页;
- 插件可以具有相同的控制器名称;
- 支持Web Forms引擎和Razor引擎;
背景
有很多关于构建ASP.NET MVC插件框架的讨论,但大多数都使用了以下技巧或类似的方法:
- 所有视图都嵌入到程序集中,或者
- 将插件程序集复制到bin文件夹,或者
- 使用私有路径指示插件程序集的位置,或者
- 安装插件后,将其视图复制到Views文件夹,
所有这些方法看起来都很吸引人,但维护单个插件非常困难,尤其是当插件的规模变得很大时。
在本文中,您将看到创建一个插件几乎与创建常规ASP.NET MVC Web应用程序完全相同,只需要为每个插件创建一个插件清单文件。
ASP.NET MVC插件实际上是基于另一个插件框架OSGi.NET的扩展,从技术上讲,您可以将其替换为任何其他框架,如MEF、Sharp-develop,只需进行一些包装。
使用代码创建插件
现在让我们从头开始创建一个新的插件,基于该插件框架。我将创建一个媒体管理插件,它可以显示最受欢迎的电视节目。关键步骤如下:
- 下载最新的ASP.NET MVC插件框架源代码(您可以选择MVC3或MVC4,取决于您的Visual Studio版本),并将其解压到一个单独的文件夹中,然后用Visual Studio打开MvcOSGi.sln。解决方案的骨架如下所示:
“Core”下的项目是插件框架,Plugins文件夹是插件容器,启动项目“MvcOSGi.Shell”是一个标准的ASP.NET MVC Web应用程序,它所做的就是,在Application_Start()
中启动OSGi.Net框架,代码如下所示。
protected void Application_Start()
{
//Start OSGi
var bootstapper = new Bootstrapper();
bootstapper.StartBundleRuntime();
//Register Razor view engine for bundle.
ViewEngines.Engines.Add(new BundleRuntimeViewEngine(new BundleRazorViewEngineFactory()));
//Register WebForm view engine.
ViewEngines.Engines.Add(new BundleRuntimeViewEngine(new BundleWebFormViewEngineFactory()));
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
MonitorExtension();
}
上面的代码非常整洁清晰,但我仍然有一些简要的注释。
- 前两行用于启动OSGi.NET,它将加载插件并解析它们的依赖关系,之后,所有插件都应该处于活动状态或准备好使用状态;
- 接下来的两行用于注册插件的视图引擎,这对于插件框架至关重要。在此框架中,每个插件及其视图、程序集和所有私有项都可以部署在一个单独的文件夹中,默认情况下,ASP.NET在用户尝试访问视图时找不到它们,因此我需要自定义一个新的视图引擎来帮助ASP.NET从正确的文件夹定位资源。
- 最后一个MonitorExtension用于挂钩插件更改,例如安装新插件或卸载插件,在这种情况下,我们应该相应地更新母版页。
- 右键单击Plugins文件夹,选择“添加”-“新项目”-“ASP.NET MVC 4 Web应用程序”,输入项目名称为MediaPlugin,然后单击“确定”完成插件项目。
- 将packages\iOpenworks\UIShell.OSGi.dll添加到MediaPlugin的引用中,这是后端插件框架库。
- 创建一个名为PopularTVShowController的控制器及其视图;
- 向MediaPlugin项目添加一个名为Manifest.xml的XML文件,其内容如下所示:
<?xml version="1.0" encoding="utf-8"?>
<Bundle xmlns="urn:uiosp-bundle-manifest-2.0" Name="MediaPlugin"
SymbolicName="MediaPlugin" Version="1.0.0.0" InitializedState="Active">
<Runtime>
<Assembly Path="bin\MediaPlugin.dll" Share="false" />
</Runtime>
<Extension Point="SidebarMenu">
<Item url="/PopularTVShow/Index?plugin=MediaPlugin"
text="Popular Movie" order="0"/>
<Item url="https://osgi.codeplex.com/" text="OSGi.NET" order="0"/>
<Item url="#" text="Style" order="0"/>
<Item url="#" text="Blog" order="0"/>
<Item url="#" text="Archives" order="0"/>
</Extension>
</Bundle>
这是插件清单文件;它指定要向最终用户显示的插件程序集和页面。我稍后会讨论它。
最后,重新生成整个解决方案,然后按F5运行。
您将在主页上看到您的插件页面的链接列表,这是如何发生的:
我稍后将解释如何将插件中的视图显示到母版页。让我们点击“热门电影”链接来查看插件中的页面:
您会发现插件通过查询字符串指定插件名称。这就是框架处理多个插件中相同控制器名称问题的方式。
关注点
如何将插件中的视图显示到母版页?
每个插件都有一个清单文件,其中描述了它所有的资源。以源代码中的BlogPlugin为例,其清单如下:
<?xml version="1.0" encoding="utf-8"?>
<Bundle xmlns="urn:uiosp-bundle-manifest-2.0" Name="BlogPlugin"
SymbolicName="BlogPlugin" Version="1.0.0.0" InitializedState="Active">
<Activator Type="BlogPlugin.Activator" Policy="Immediate" />
<Runtime>
<Assembly Path="bin\BlogPlugin.dll" Share="false" />
</Runtime>
<Extension Point="MainMenu">
<Item url="/Blog/Index?plugin=BlogPlugin"
text="Blog" order="4"/>
<Item url="/Support/Index?plugin=BlogPlugin"
text="Support" order="2"/>
</Extension>
</Bundle>
extension节点表示此插件将在布局页面的主菜单中添加Blog和Support链接。让我们回到MoniteExtension
,它用于监视插件扩展的任何更改,在这种情况下,它将把扩展信息加载到ApplicationViewModel
中,然后动态地渲染布局页/母版页。MoniterExtension
方法如下所示:
private void MonitorExtension()
{
ViewModel = new ApplicationViewModel();
//Register pages in Shell project.
ViewModel.MainMenuItems.Add(new MenuItem
{
Text = "Home",
URL = "/"
});
BundleRuntime.Instance.AddService<ApplicationViewModel>(ViewModel);
_extensionHooker = new ExtensionHooker(
BundleRuntime.Instance.GetFirstOrDefaultService<IExtensionManager>());
_extensionHooker.HookExtension("MainMenu", new MainMenuExtensionHandler(ViewModel));
_extensionHooker.HookExtension("SidebarMenu", new SidebarExtensionHandler(ViewModel));
}
下面是我们在布局页中渲染主菜单的方式:
<div class="header">
<div class="header_resize">
<div class="logo">
<h1><a href="https://codeproject.org.cn/">OSGi.NET</a>
<small>This page is built by <b>Razor Engine</b></small></h1>
</div>
<div class="menu_nav">
<ul>
@{
var viewModel = UIShell.OSGi.BundleRuntime.Instance.
GetFirstOrDefaultService<ApplicationViewModel>();
if (viewModel != null)
{
foreach (var mainMenuItem in
viewModel.MainMenuItems.OrderBy(item => item.Order))
{
<li itemid="@mainMenuItem.Order"><a " +
"href="@mainMenuItem.URL">@mainMenuItem.Text</a> </li>
}
}
}
</ul>
</div>
<div class="clr"></div>
</div>
</div>
这是运行模式:
这是如何发生的:
ASP.NET如何定位插件程序集,因为它的程序集没有复制到bin文件夹?
这有点棘手,ASP.NET从类BuildManager解析程序集,所以您应该在插件激活后将其注册到那里,或在停用时将其移除。
如何支持动态插件安装?
我正在使用一个免费的插件框架OSGi.NET来管理插件,它原生支持动态安装和卸载插件。请参考OSGi.NET模块化框架。
这显示了如何在远程控制台应用程序中安装或停止插件。我基于它包装了MVC插件框架,当安装新插件时,我可以收到通知,然后我只需要将插件中的程序集添加到BuildManager。
默认情况下,ASP.NET应用程序启动后,您就不能再向BuildManager注册程序集了,我在这里做了一个冒险的尝试,就是通过反射获取BuildManager的程序集容器,然后注册新安装的插件程序集。
为什么在动态安装期间不需要重新启动**w3wp**进程?
默认情况下,w3wp会加载Bin文件夹中的所有程序集并锁定它们,没有办法在不重置的情况下释放锁定。但您不必这样做。在OSGi.net中,当插件被移除时,它里面的所有资源都会被释放,所以程序集对其他插件来说是不可见的,就像它从未存在过一样。关键在于插件程序集放置在其私有文件夹中,而不是**bin文件夹**,所以w3wp不会加载它,但osgi.net会加载它,这样,移除插件程序集就不会回收**w3wp**。
历史
- 详细说明动态安装/卸载,以及为什么w3wp不会回收。