Originally published on: 12/5/2008 8:19:35 PM
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
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.
Here's the OpenSource ViewEngine I wrote, and some tutorials/walkthroughs if you're interested.
I just do this when I cannot use strongly typed Model.
and from there on just use etc...
hope this helps
just do "var model = (Person)Model;" in markup and use "model.Name" from there on...
you can create an implementation of your Service or Repository that inherits from a base repository somewhere further down and put this in your "Models" folder. Then just export this implementation as typeof(IDataSource) or whatever. In your plugin controller you can use IDataSource as parameter in the constructor and add the attribute "[ImportingConstructor]".
This way you can create an instance of you plugin controller and passing a TestDataSource when unit testing and let MEF take care of the real web app exports and imports.
The plugins I've created as code librarys, its seemed the right thing to do. Unfortunately though you end up losing a lot of the intellesense that makes using MVC so nice.
Any ideas on how I might get that back?
Dont know.. any suggestions?
I saw you using ViewData, but can we use the MVC style Model object?
assemblyName = Path.Combine(Path.Combine(HttpRuntime.AppDomainAppPath, folderName), assemblyName);
return new CacheDependency(assemblyName);
}
return base.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
}
public override Stream Open()
{
string[] parts = path.Split('/');
string folderName = parts[1];
string assemblyName = parts[2];
string resourceName = parts[3];
assemblyName = Path.Combine(Path.Combine(HttpRuntime.AppDomainAppPath, folderName), assemblyName);
var assemblyBytes = File.ReadAllBytes(assemblyName);
Assembly assembly = Assembly.Load(assemblyBytes);
if (assembly != null)
return assembly.GetManifestResourceStream(resourceName);
return null;
}
Any suggestions how I can "fix" this problem? :)
Thanks a lot Damrod.
I did a prototype for Plugins in ASP.NET MVC2 using Areas increase you are interested.
See: http://www.veebsbraindump.com/2010/06/asp-net-mvc2-plugins-using-areas/
The easiest way to fix it in the sample "~/Plugin/YOURDLL.dll..." just add the physical folder "Plugin" in the application root and copy a web.config from an working mvc-application views folder.
then you need to make sure to give all the discovery paths for masterpages in your custom view engine