本章主要讲解如何为框架新增插件化开发功能。
在.net 4.0中,我们可以在Application开始之前,通过PreApplicationStartMethod方法加载所需要的任何东西。那么今天我们主要做的工作就集中在这个时间段:
1.将插件DLL及文件拷贝入主网站目录并编译
2.加载Plugin
首先来说说第一步,由于这步里面,我们主要拷贝DLL及文件,所以我们利用了一个List<Assembly>容器来记录所有的插件的DLL,具体代码如下:
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Text;
5: using System.Web;
6: using System.Reflection;
7: using System.Web.Hosting;
8: using System.IO;
9: using System.Web.Compilation;
10: using TinyFrame.Framework;
11:
12: [assembly: PreApplicationStartMethod(typeof(PluginManager), "Initialize")]
13: namespace TinyFrame.Framework
14: {
15: public class PluginManager
16: {
17: private const string pluginDirectory = "~/Areas";
18: private const string pluginShadowCopyDirectory = "~/Areas/Temp";
19: private static readonly List<Assembly> pluginAssemblies=new List<Assembly>();
20:
21: //初始化方法
22: public static void Initialize()
23: {
24: var pluginPath = HostingEnvironment.MapPath(pluginDirectory);
25: //var pluginPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Areas");
26: foreach (var file in Directory.EnumerateFiles(pluginPath, "*Plugin*.dll", SearchOption.AllDirectories))
27: {
28: pluginAssemblies.Add(Assembly.LoadFile(file));
29: }
30:
31: pluginAssemblies.ForEach(BuildManager.AddReferencedAssembly);
32:
33: AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolve;
34: }
35:
36: private static Assembly AssemblyResolve(object sender, ResolveEventArgs resolveArgs)
37: {
38: var currentAssemblies = AppDomain.CurrentDomain.GetAssemblies();
39: // 如果未加载程序集
40: foreach (var assembly in currentAssemblies)
41: {
42: if (assembly.FullName == resolveArgs.Name || assembly.GetName().Name == resolveArgs.Name)
43: {
44: return assembly;
45: }
46: }
47:
48: return null;
49: }
50:
51: //得到程序集名稱
52: public static List<string> PluginNames()
53: {
54: return pluginAssemblies.Select(c => c.GetName().Name).ToList();
55: }
56: }
57: }
第12行,主要标志本方法将于Application_Start开始之前执行。
第17行,主要定义了主网站中放置插件的目录,这里为Areas目录。
第22行,为我们的初始化方法。
第28行,遍历我们的插件目录,加载名称中包含Plugin关键字的DLL。
第31行,遍历完毕后,将这些DLL加入到BuildManager中,以便于编译操作。
这里讲完之后,剩下的就是来写我们的插件了,这里要进行的是第二步:
首先,新建一个TinyFrame.Plugin.NivoSlider的MVC 4 站点,在这个站点根目录下面添加一个名称为AreaRegister.cs的类文件,内容如下:
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Web;
5: using System.Web.Mvc;
6:
7: namespace TinyFrame.Plugin.Sample
8: {
9: public class AreaRegister : AreaRegistration
10: {
11:
12: public override string AreaName
13: {
14: get
15: {
16: return "TinyFrame.Plugin.NivoSlider";
17: }
18: }
19:
20: public override void RegisterArea(AreaRegistrationContext context)
21: {
22: context.MapRoute(
23: "NivoSlider_default",
24: "NivoSlider/{controller}/{action}/{id}",
25: new { action = "Index", id = UrlParameter.Optional },
26: new string[] { "TinyFrame.Plugin.NivoSlider.Controllers" }
27: );
28: }
29:
30: }
31: }
其中,类本身需要继承自AreaRegistration,以便于使用MVC本身提供的插件化支持。然后通过重载AreaName和RegisterArea方法,实现AreaName的定义和路由的映射。AreaName主要是为了识别返回的Plugin内容区域,而RegisterArea主要是为路由访问提供了支持。需要注意的是,第26行是Controller的命名空间,需要写上,否则会因为插件的目录和主站的目录一致,导致报错。
然后我们来添加测试的Controller和View:
在Controllers文件夹下面添加NivoSliderController.cs类,并返回一个空的页面:
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Web;
5: using System.Web.Mvc;
6:
7: namespace TinyFrame.Plugin.NivoSlider.Controllers
8: {
9: public class NivoSliderController : Controller
10: {
11: public ActionResult Index()
12: {
13: return View();
14: }
15:
16: }
17: }
然后在Views文件夹下新建NivoSlider文件夹,并在其中创建一个名称为Index.cshtml的页面文件:
1: @{
2: ViewBag.Title = "Index";
3: }
4: <div style="width:600px;height:400px;margin:0 auto;">
5: <div class="slider-wrapper theme-default" style="float:left;">
6: <div id="slider" class="nivoSlider">
7: <img src="@Url.Content("~/Content/slider/images/toystory.jpg")" alt="" />
8: <img src="@Url.Content("~/Content/slider/images/up.jpg")" alt="" title="This is an example of a caption" />
9: <img src="@Url.Content("~/Content/slider/images/walle.jpg")" alt="" data-transition="slideInLeft" />
10: <img src="@Url.Content("~/Content/slider/images/nemo.jpg")" alt="" title="#htmlcaption" />
11: </div>
12: <div id="htmlcaption" class="nivo-html-caption">
13: <strong>This</strong> is an example of a <em>HTML</em> caption with <a href="#">a link</a>.
14: </div>
15: </div>
16: </div>
17:
这样,我们的页面就准备好了。
然后右击这个插件项目,选择属性,在“生成”标签页面,我们需要把输出路径从"bin\"修改成 “..\TinyFrame.Web\Areas\”,这样做是为了将最新的DLL文件拷贝到主站目录中。
之后在”生成事件“标签页面,在”后期生成事件命令行“下的文本框中,输入以下命令,以便于将样式文件等也拷贝到主站的Areas插件目录中:
1: xcopy "$(ProjectDir)\Views" "$(TargetDir)\TinyFrame.Plugin.NivoSlider\Views\" /s /i /y
2: xcopy "$(ProjectDir)\Areas" "$(TargetDir)\TinyFrame.Plugin.NivoSlider\Areas\" /s /i /y
3: xcopy "$(ProjectDir)\Content" "$(TargetDir)\TinyFrame.Plugin.NivoSlider\Content\" /s /i /y
其中,TinyFrame.Plugin.NivoSlider就是你之前定义的AreaName。
这样,当项目编译完毕之后,会自动把DLL文件,视图文件,样式文件等拷贝到主站的Areas插件目录下:
最后让我们来配置下主站:
首先在主站的Content目录下添加slider文件夹,并将NivoSlider用到的所有CSS文件,JS文件引入,然后利用ActionLink,来链接至这个幻灯片插件:
1: <!DOCTYPE html>
2:
3: <html>
4: <head>
5: @RenderSection("featured", false)
6: <meta name="viewport" content="width=device-width" />
7: <title>书籍借阅系统</title>
8: <!--EasyUI CSS files-->
9: <link rel="stylesheet" type="text/css" href="@Url.Content("~/Content/jqueryeasyui/themes/default/easyui.css")" />
10: <link rel="stylesheet" type="text/css" href="@Url.Content("~/Content/jqueryeasyui/themes/icon.css")" />
11:
12: <!--BootStrap CSS&JS files-->
13: <link href="@Url.Content("~/Content/mycss/default.css")" rel="stylesheet" type="text/css" />
14: <link href="@Url.Content("~/Content/bootstrap/css/bootstrap.min.css")" rel="stylesheet" type="text/css" />
15: <link href="@Url.Content("~/Content/bootstrap/css/bootstrap-theme.min.css")" rel="stylesheet" type="text/css" />
16: <script src="@Url.Content("~/Content/jquery/jquery.min.js")" type="text/javascript"></script>
17: <script src="@Url.Content("~/Content/bootstrap/js/bootstrap.min.js")" type="text/javascript"></script>
18:
19: <!--EasyUI JS files-->
20: <script src="@Url.Content("~/Content/jqueryeasyui/jquery.easyui.min.js")" type="text/javascript")"></script>
21: <script src="@Url.Content("~/Content/jqueryeasyui/plugins/datagrid-detailview.js")" type="text/javascript")"></script>
22: <script src="@Url.Content("~/Content/jqueryeasyui/DataGrid.js")" type="text/javascript"></script>
23:
24: <!--NivoSlider-->
25: <link href="@Url.Content("~/Content/slider/themes/default/default.css")" rel="stylesheet" type="text/css" />
26: <link href="@Url.Content("~/Content/slider/nivo-slider.css")" rel="stylesheet" type="text/css" />
27: <script src="@Url.Content("~/Content/slider/scripts/jquery.nivo.slider.js")" type="text/javascript"></script>
28: <script src="@Url.Content("~/Content/slider/scripts/base.js")" type="text/javascript"></script>
29: </head>
30: <body>
31: <div class="containerMaster">
32: <ul class="nav nav-pills" id="menu">
33: <li>@Html.ActionLink("书籍入库", "Book", "Home", new { Area = string.Empty }, new { })</li>
34: <li>@Html.ActionLink("借入借出", "BookLend", "Home", new { Area = string.Empty }, new { })</li>
35: <li>@Html.ActionLink("书籍分类", "BookType", "Home", new { Area = string.Empty }, new { })</li>
36: <li>@Html.ActionLink("书籍存放", "BookPlace", "Home", new { Area = string.Empty }, new { })</li>
37: <li>@Html.ActionLink("学生管理", "Student", "Home", new { Area = string.Empty }, new { })</li>
38: @*<li>@Html.ActionLink("人员管理","Manager","Home")</li>*@
39:
<li>@Html.ActionLink("幻灯片插件", "Index", "NivoSlider", new { Area = "TinyFrame.Plugin.NivoSlider" }, new { })</li>
40: </ul>
41: </div>
42: @RenderBody()
43: </body>
44: </html>
需要注意的是,在上图代码中,着色的部分是访问插件的链接,Area = "TinyFrame.Plugin.NivoSlider"是我们之前定义的AreaName。
运行起来项目,让我们看看结果吧:
这节就讲完了,我们用已有的方法实现了一个简易的插件系统,但是,这个插件系统还不完善,做到商用还是远远不够的,缺点有以下几个:
1.无法进行热插拔,插件的更新需要重启网站。
2.无法检测插件的更新。
而下一章节,我们将会打造一个克服以上缺点的强大易用的插件系统。敬请期待。
以下问题及解决方法是我在开发过程中遇到的,在这里我讲答案贴出,以便于记录及追踪:
1).提示无法找到System.Web.Optimization,是否缺少引用?
解决方法:
1.PM> Install-Package Microsoft.AspNet.Web.Optimization 或者
2.删掉web.config中的引用
2).“找到多个与名为“Home”的控制器匹配的类型。如果为此请求(“{controller}/{action}/{id}”)提供服务的路由在搜索匹配此请求的控制器时没有指定命名空间,则会发生此情况。如果是这样,请通过调用含有“namespaces”参数的“MapRoute”方法的重载来注册此路由。”
解决方法:
出现该问题的愿原因是在默认的Golbal.asax.cs文件中已经注册了默认路由
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional }, // Parameter defaults
//new string[] { "Working_with_Areas.Controllers"}
);
}
而在AREAS中有注册了一个同名的Controller
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Admin_default",
"Admin/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional },
//new string[] { "Working_with_Areas.Areas.Admin.Controllers" }
);
}
解决方法就是在每个注册Router的时候加上命名空间即可。
3).运行的时候,经常无法出现最新编译好的页面。
调试plugin的时候,请先将 输出路径改成 bin\ 等调试完毕后,修改成..\TinyFrame.Web\Areas\,否则你调试的页面一直都是Bin目录下的,而非你设置的..\TinyFrame.Web\Areas\ 目录下的。
2014.04.22更新
1.添加了一个强类型的Plugin插件,用于检测强类型程序集是否能够被编译。
2.插件路由新增了controller参数,用于规避某些特定场合下访问出现404的错误。
public class AreaRegister : AreaRegistration
{ public override string AreaName
{
get
{
return "TinyFrame.Plugin.NivoSlider";
}
} public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"NivoSlider_default",
"NivoSlider/{controller}/{action}/{id}",
new { controller = "NivoSlider", action = "Index", id = UrlParameter.Optional },
new string[] { "TinyFrame.Plugin.NivoSlider.Controllers" }
);
} }
3.修改了生成事件中的后期生成命令行,由于插件中的Content或者是Scripts中的某些图片或者是文件需要拷贝到主站中,所以这里添加了拷贝到主站目录的方法。
最后,提供源代码下载: