One of the last things I posted here was how I was toying with flexible content and template solutions in ASP.NET MVC. For that project, we're going forward with the solution that emerged from that experimentation and I'm excited to see how it turns out.ÂÂ
Along the way, an interesting question about how to create what are essentially plugins for the MVC framework. Basically, if what you're building is an MVC app that will be deployed in dozens, hundreds or thousands of independent implementations, it becomes obvious pretty quickly that you need most of the pieces of the application to be modular so they can added/removed/overridden at least to a minimal level.
I've come up with what I think is a workable solution, which is pretty much just gathering up some of the other ideas already out there into a quick-n-dirty working prototype. The basic idea is this:
Put an encapsulated set of model classes (or none if you depend on central model classes), controller classes and a set of views into its own DLL. The views (using the default WebForms view engine, they're aspx files) go in as resources.
Pull that DLL into your MVC application and the MVC stack inside your "plugin" will work just like if those bits had been a "natural" part of the base MVC application. In fact actually creating the pieces from inside the main app and then moving them over to their own project is the quickest way I found to actually create the bits for the plugins.
When the question came up, I vaguely remembered seeing a question on StackOverflow about how to put views into an assembly/DLL. That question along with one on VirtualPathProviders provided most of the kernel of what I needed.
So, on to how to actually do this.
Start with a standard ASP.NET MVC project (in beta as of the time of this writing). Add whatever model, controller and view elements you would if this bit of functionality wasn't a plugin (this also makes this approach far easier to work if you decide later to pull something out of the core and into a plugin).
Add a Class Library project that's compatible with MVC (adding System.Web.MVC, etc) for your plugin and cut/paste the components into your new project. The views need to be changed to be embedded resources via the properties pane.
Next, you'll need to tweak your controller methods just a bit if you're using the standard "return View();" return approach. Change that return line to:
ViewResult RenderedView = View("~/Plugin/YOURDLL.dll/FULLNAME_YOUR_VIEW.aspx");
return RenderedView;
Getting that path right can be a bit tricky at first. The "~/Plugin/" part is pretty much like a route. It's probably possible/better to actually make that a route, but I haven't looked into it. Then you've got the filename of your DLL. It's the actual view name that isn't straightforward. It's not the file name as you saw it in Visual Studio. Instead, it's the fully-qualified name of the view class, followed by .aspx.
I finally figured that out by loading my DLL in Reflector and looking at the resources folder that way. If you have trouble getting the name right, that's a good thing to try.
Once that's done, make sure the class library builds and add a reference in your MVC app to either the project or the output DLL. Either way, the DLL will end up in the "bin" directory of your MVC app.
Now you've got to modify your MVC app to actually look at the plugin(s) for the views at that long path. The models and controller classes are handled by setting the right namespaces and imports.
This is where the VirtualPathProvider comes in. I created a "Lib" directory in the MVC app and added a class called AssemblyResourceProvider. In there, you set that "~/Plugin" path and handle the digging into the assembly for the view. If you're OK with that path, you can just use my class wholesale.
Lastly, you need to register that path provider in the global.asax:
System.Web.Hosting.HostingEnvironment.RegisterVirtualPathProvider(new AssemblyResourceProvider());
The most straightforward way to "get" it is just to take a look at my project. You can download the solution (VStudio2008) and dig through it. The included plugin is available at this URL:
http://server:PORT/BasicExample/Display/Whatever
Where To Go From Here
One of the bits that this doesn't address, but that will be necessary at least in my implementation of this concept is the registering/unregistering of these plugins with the application. The way it is right now, it's entirely held together by convention. You make links to the plugin controller actions and if the plugin is there, it works. Otherwise, it breaks.
The most obvious solutions to this to me are to add to the Assembly information or other meta-data/manifest in it explaining what's in the plugin, which controllers and methods, etc. The hosting app would then watch the "plugin" path (as defined in the VirtualPathProvider) and take over from there as to whether they're automatically available or need additional "activation", etc.
I'd love to see/hear improvements or if there's an entirely better way to do it that means I should scratch this one. Let me know.
