ASP.NET MVC 3 plugin architecture with embedded razor views
2013-02-03 00:49 yezhi 阅读(643) 评论(0) 收藏 举报The goal is to create a plugin architecture for asp.net mvc 3 usingrazor as view engine. Plugins should be compiled into .dll files and they should be placed in a directory other than ~/bin (like~/extensions/plugins/rating/PageRating.dll) and views (admin views for settings) should be found behind routes like/admin/plugins/rating/{action}
1) Create the asp.net mvc project. Mine is called ’Crash’, because that’s my last name in English. Nomen est omen? =(
2) Add a new class library project to the solution (Crash.Plugins) and add a plugin controller base class
namespace Crash.Plugins{ public abstract class PluginController : Controller { protected string PluginDirectory; // pluginDirectory will have a value like '~/extensions/plugins/rating/' public PluginController(string pluginDirectory) { if (pluginDirectory.EndsWith("/")) { PluginDirectory = pluginDirectory; } else { PluginDirectory = pluginDirectory +"/"; } } public ViewResult RelativeView(string viewName, object model) { viewName = PluginDirectory + viewName; return base.View(viewName,model); } }} |
3) Create a class library project for a plugin (Crash.PageRating) and a plugin class
namespace Crash.PageRating{ public class PageRatingController : PluginController { public PageRatingController(string pluginDirectory) : base(pluginDirectory) { } public ActionResult Index() { return RelativeView("settings.cshtml",new SettingsViewModel()); } [HttpPost] public ActionResult Index(SettingsViewModel model) { return RelativeView("settings.cshtml",model); } }} |
3.1) Create a new razor page in the web project (Crash)
3.2) Cut-paste the razor page to the plugin project (Crash.PageRating)
3.3) Modify it (I didn’t get @model to work)
@{Layout = ";~/Areas/Admin/Views/Shared/admin.cshtml";var viewModel = Model as Crash.PageRating.SettingsViewModel;}<form method="post"><fieldset><legend>Settings</legend><strong>Foo</strong>@Html.TextBox("Foo",viewModel.Foo)<input type="submi" value="Save" /></fieldset></form> |
3.4) Use these instructions to compile that razor page
3.5) Compile the plugin and copy-paste dll file from bin to web project’s dir ’~/extensions/plugins/rating/’
4) Register plugin controllers and views in the web application
4.1) Global.asax.cs (I’m using Castle.Windsor)
namespace Crash{ public class MvcApplication : System.Web.HttpApplication { private static IWindsorContainer container; protected void Application_Start() { RegisterRoutes(RouteTable.Routes); container = new WindsorContainer(); // register 'normal' controllers container.Register(AllTypes.FromThisAssembly().BasedOn<IController>().If(t => t.Name.EndsWith("Controller")).Configure((ConfigureDelegate)(c => c.LifeStyle.Transient))); // string is the route path // type is the plugin controller type // it maps routes like '/admin/plugins/rating/{action}' to Crash.PageRating.PageRatingController Dictionary<string, Type> pluginTypes = new Dictionary<string, Type>(); // location of the plugins var allPluginsDir = new DirectoryInfo(Server.MapPath("~/extensions/plugins/")); foreach(var dir in allPluginsDir.GetDirectories()) { string pluginDir = string.Format("~/extensions/plugins/{0}/", dir.Name); // loop through all dll files, though only one should exist per directory foreach(var dll in dir.GetFiles("*.dll")) { var assembly = Assembly.LoadFrom(dll.FullName); // register compiled razor views // e.g. 'settings.cshtml' is registered as '~/extensions/plugins/rating/settings.cshtml' BoC.Web.Mvc.PrecompiledViews.ApplicationPartRegistry.Register(assembly, pluginDir); // only one controller per plugin in this case var controllerType = assembly.GetTypes().Where(t => typeof(PluginController).IsAssignableFrom(t)).FirstOrDefault(); if(controllerType != null) { // register controller // pass pluginDir to the constructor container.Register(Component.For(controllerType).DependsOn(new { pluginDirectory = pluginDir }).LifeStyle.Transient); // admin route url var pluginUrl = string.Format("admin/plugins/{0}/{{action}}", dir.Name); // map admin route to controller pluginTypes.Add(pluginUrl, controllerType); RouteTable.Routes.MapRoute"plugin_" + dir.Name, pluginUrl, new {controller=controllerType.Name.Replace("Controller",""),action="Index" },new[]{controllerType.Namespace}); } } } AreaRegistration.RegisterAllAreas(); // Controller factory var controllerFactory = new CrashControllerFactory(container.Kernel,pluginTypes); ControllerBuilder.Current.SetControllerFactory(controllerFactory); } }} |
5) Create custom ControllerFactory
namespace Crash{ public class CrashControllerFactory : DefaultControllerFactory { private readonly IKernel kernel; private readonly Dictionary<string,Type> pluginTypes; public CrashControllerFactory(IKernel kernel,Dictionary<string,Type> pluginTypes) { this.kernel = kernel; this.pluginTypes = pluginTypes; } protected override Type GetControllerType(System.Web.Routing.RequestContext requestContext, string controllerName) { var route = requestContext.RouteData.Route as Route; if (route != null) { if (pluginTypes.ContainsKey(route.Url)) { return pluginTypes[route.Url]; } } // this can't resolve types that are in not-referenced assemblies (or not in bin directory, I don't know) return base.GetControllerType(requestContext, controllerName); } protected override IController GetControllerInstance(System.Web.Routing.RequestContext requestContext, Type controllerType) { return kernel.Resolve(controllerType) as IController; } }} |
6) Done. It ain’t pretty, but this was more like a proof of concept and it works :)
浙公网安备 33010602011771号