基于OSGI.NET的MVC插件式开发
最近在研究OSGI.NET插件式开发框架。官方网站提供了一个基于OSGI.NET的插件仓库。下载官方的SDK包安装后VS项目模板会多出一组iOpenWorks项目模板。在学习过程中,发现通过iOpenWorks模板创建的应用程序主程序都会附加上iOpenWorks插件仓库相关的插件。如何使用OSGI.NET框架库来创建纯净的插件框架,来实现自己的IDE呢?
基于WinForm、WPF的主程序很简单。这里主要介绍下如何来实现基于MVC的插件式框架。
主要思路:通过MVC中的Area方式来实现对插件页面的访问,一个子项目为一个Area。
需要解决的问题:
1.基于OSGI.NET插件的框架,实现的物理隔离,运行时不会将子项目的dll文件复制到主程序的bin目录下。
2.MVC中Area下的controller创建是根据命名空间结构来查找controller的,如何将Area下的controller对应到子项目的程序集中。
3.Controller如何正确的加载cshtml页面。
4.子项目中的View文件中如何访问资源文件。
接下来我们来逐个解决以上的问题。(关于OSGI.NET,可以在官网上看下视频教程,这里就不介绍了)
1.实现MVC主程序对插件MVC项目下的ControllerAction的访问
新建空的解决方案:Osgi.Net.Mvc
在解决方案下新建MVC4项目,命名为Osgi.Net.Mvc.Web,选择空模板,选择Razor视图引擎
在新建的MVC4项目下添加项目目录Plugins
在解决方案中添加新的MVC4项目,命名为About,选择空模板,选择Razor视图引擎,将About项目的路径,保存到Osgi.Net.Mvc.Web项目的Plugins目录下
删除Global.asax文件和App_Start文件夹,并添加Manifest.xml文件
<?xml version="1.0" encoding="utf-8"?> <Bundle xmlns="urn:uiosp-bundle-manifest-2.0" Name="About" SymbolicName="About" Version="1.0.0.0" InitializedState="Active"> <Runtime> <Assembly Path="bin\About.dll" Share="false" MultipleVersions="false" /> </Runtime> </Bundle>
建成后的空项目如下:
在About下添加控制器Hello创建Index方法。
public class HelloController : Controller { public ActionResult Index() { return Content("About Plugin."); } }
接下来我们来编写代码,在Application启动前启动插件运行时BundleRuntime。
在Global.asax的MvcApplication方法中加入静态方法,并实现BundleRuntime的启动。
public static void BundleStart() { var runtime = new BundleRuntime(); runtime.Start(); }
在命名空间上注册PreApplicationStartMethod,让方法在Application前启动。
[assembly: PreApplicationStartMethod(typeof(MvcApplication), "BundleStart")] namespace Osgi.Net.Mvc.Web { //...... }
此时,插件加载已完成。启动应用程序,访问/hello/index页面返回404.
如何能让主程序能访问插件中的controller呢?
我们知道,MVC的动态编译是通过System.Web.Compilation.BulidManager来管理程序集的。
我们尝试将插件的程序集加入到BulidManager管理的程序集中。
在BundleStart()方法中,BundleRuntime启动后加入代码
foreach (var bundle in runtime.Framework.Bundles) { var bundleData = runtime.GetFirstOrDefaultService<IBundleInstallerService>() .GetBundleDataByName(bundle.SymbolicName); if (bundleData == null) continue; var serviceContainer = runtime.Framework.ServiceContainer; var service = serviceContainer.GetFirstOrDefaultService<IRuntimeService>(); var assemlbies = service.LoadBundleAssembly(bundle.SymbolicName); assemlbies.ForEach(BuildManager.AddReferencedAssembly); }
重新启动应用程序,访问/hello/index页面,这时已经能够看到从About中的HelloController返回的数据了。
2.实现通过Area的方式来访问插件
要实现通过Area的方式来访问插件,就需要修改通过Area来查找Controller的方式
首先给About插件添加Area注册文件AboutAreaRegistration.cs
public class AboutAreaRegistration : AreaRegistration { public override void RegisterArea(AreaRegistrationContext context) { context.MapRoute( "AboutPlugin", "About/{controller}/{action}/{id}", new { action = "Index", id = UrlParameter.Optional }, new[] { "About.Controllers" } ); } public override string AreaName { get { return "About"; } } }
然后创建一个BundleAreaControllerFactroy类来重载DefaultControllerFactory
public class BundleAreaControllerFactroy : DefaultControllerFactory { }
重载GetControllerType方法
1.拦截上下文中的Area信息
2.根据Area信息加载对应的Bundle信息
3.在Bundle中查找对应的contrller
4.如果未找到对应controller,将上下文转交给基类来处理
protected override Type GetControllerType(RequestContext requestContext, string controllerName) { string symbolicName = null; object area; if (requestContext.RouteData.DataTokens.TryGetValue("area", out area)) { symbolicName = area as string; } else { var routeWithArea = requestContext.RouteData.Route as IRouteWithArea; if (routeWithArea != null) { symbolicName = routeWithArea.Area; } var castRoute = requestContext.RouteData.Route as Route; if (castRoute != null && castRoute.DataTokens != null) { symbolicName = castRoute.DataTokens["area"] as string; } } if (symbolicName != null) { var controllerTypeName = controllerName + "Controller"; var runtimeService = BundleRuntime.Instance.GetFirstOrDefaultService<IRuntimeService>(); var assemblies = runtimeService.LoadBundleAssembly(symbolicName); foreach (var assembly in assemblies) { foreach (var type in assembly.GetTypes()) { if (type.Name.ToLower().Contains(controllerTypeName.ToLower()) && typeof(IController).IsAssignableFrom(type)) { return type; } } } } return base.GetControllerType(requestContext, controllerName); }
在Global.asax的Application_Start中注册ControllerFactory
ControllerBuilder.Current.SetControllerFactory(new BundleAreaControllerFactroy());
到这里已经完成了通过Area访问插件的功能。启动应用程序,通过/about/hello/index访问页面,这时就能够看到刚才的页面了。
3.实现能够正确加载插件路径下cshtml文件的视图引擎
在RazorViewEngine对象中提供了一组视图路径模板:AreaViewLocationFormats、AreaMasterLocationFormats、AreaPartialViewLocationFormats、ViewLocationFormats、MasterLocationFormats、PartialViewLocationFormats。我们可以通过修改路径模板来实现对插件路径下的cshtml文件的正确加载。
首先创建BundleRazorViewEngine对象来重载RazorViewEngine
public class BundleRazorViewEngine : RazorViewEngine { }
在构造函数中我们首先将基类中的路径模板存储下来
public class BundleRazorViewEngine : RazorViewEngine { private readonly string[] _baseAreaViewLocationFormats; private readonly string[] _baseAreaMasterLocationFormats; private readonly string[] _baseAreaPartialViewLocationFormats; private readonly string[] _baseViewLocationFormats; private readonly string[] _baseMasterLocationFormats; private readonly string[] _basePartialViewLocationFormats; public BundleRazorViewEngine() { _baseAreaViewLocationFormats = AreaViewLocationFormats; _baseAreaMasterLocationFormats = AreaMasterLocationFormats; _baseAreaPartialViewLocationFormats = AreaPartialViewLocationFormats; _baseViewLocationFormats = ViewLocationFormats; _baseMasterLocationFormats = MasterLocationFormats; _basePartialViewLocationFormats = PartialViewLocationFormats; } }
然后重载FindView方法,在FindView中
1.从上下文中查找Area信息
2.根据Area信息加载插件
3.根据插件路径信息修改路径模板
4.执行基类的FindWiew来返回ViewEngineResult
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache) { string symbolicName = null; object area; if (controllerContext.RouteData.DataTokens.TryGetValue("area", out area)) { symbolicName = area as string; } else { var routeWithArea = controllerContext.RouteData.Route as IRouteWithArea; if (routeWithArea != null) { symbolicName = routeWithArea.Area; } var castRoute = controllerContext.RouteData.Route as Route; if (castRoute != null && castRoute.DataTokens != null) { symbolicName = castRoute.DataTokens["area"] as string; } } if (string.IsNullOrEmpty(symbolicName)) return new ViewEngineResult(new string[0]); var bundle = BundleRuntime.Instance.Framework.GetBundleBySymbolicName(symbolicName); if (bundle == null) return new ViewEngineResult(new string[0]); SetLocationFormats(@"~\" + bundle.Location.Replace(HostingEnvironment.ApplicationPhysicalPath, String.Empty)); return base.FindView(controllerContext, viewName, masterName, useCache); } private void SetLocationFormats(string bundleLocation) { AreaViewLocationFormats = _baseAreaViewLocationFormats .Select(item => item.Replace("~", bundleLocation)).ToArray(); AreaMasterLocationFormats = _baseAreaMasterLocationFormats .Select(item => item.Replace("~", bundleLocation)).ToArray(); AreaPartialViewLocationFormats = _baseAreaPartialViewLocationFormats .Select(item => item.Replace("~", bundleLocation)).ToArray(); ViewLocationFormats = _baseViewLocationFormats .Select(item => item.Replace("~", bundleLocation)).ToArray(); MasterLocationFormats = _baseMasterLocationFormats .Select(item => item.Replace("~", bundleLocation)).ToArray(); PartialViewLocationFormats = _basePartialViewLocationFormats .Select(item => item.Replace("~", bundleLocation)).ToArray(); }
重载FindPartialView方法,方式与重载FindView相同,这里就不贴代码了。
在Global.asax的Application_Start中注册BundleRazorViewEngine
ViewEngines.Engines.Add(new BundleRazorViewEngine());
然后在About的HelloController中添加一个方法,返回ViewResult,在cshtml页面中添加一些文字来测试一下吧。
关于资源文件的访问,下次在说。