代码改变世界

ASP.NET MVC 3 plugin architecture with embedded razor views

2013-02-03 00:49  yezhi  阅读(643)  评论(0)    收藏  举报

http://mikakolari.fi/2011/02/aspnetmvc-3-plugin-architecture-with-embedded-razor-views/

Posted 10.02.2011 by Mika Kolari & filed under ASP.NET MVCIn English.

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 :)

10 Responses to “ASP.NET MVC 3 plugin architecture with embedded razor views”