Thursday, April 23, 2009

Splitting an ASP.NET MVC application into modules

I've been playing a little bit with the concept of splitting an ASP.NET MVC application into some sort of modules so that the final product can be developed separately and (possibly) reused.

Splitting the code into class libraries is easy - you just reference the class library from your main project and that's it. The tricky part is to get the pages into the main project in some usable way.

To solve this riddle I've first created an example ASP.NET MVC application and a class library in the same solution. The class library has been given the following dependencies:

- System.Web.Abstractions
- System.Web.Mvc

and by doing so I was able to effectively create controllers in my class library.

As for the pages I've created a folder called Views in the same way it's done in a regular ASP.NET MVC application but this time in the class library itself. The content of this folder is pretty much the same as with the regular application (\Views\controllername\viewname.aspx).
To get the pages copied to the main project I've used the AfterBuild target as follows:

<Target Name="AfterBuild">
<Exec Command="xcopy /Y /E /R /I Views ..\MVCPluginExample\Plugins\Views" />
</Target>

Hint: to get IntelliSense to work I needed to copy the Web.config from main project to the root of the class library. It works like a charm!

Furthermore I didn't want the "plugin" (so to speak) to be directly referenced from the main project (so that I can really plug'n'run additional parts of the application as needed). To achieve that I've created additional Exec tasks in the AfterBuild target:

<Target Name="AfterBuild">
<Exec Command="xcopy /Y /I /R $(OutputPath)$(AssemblyName).dll ..\MVCPluginExample\bin" />
<Exec Command="xcopy /Y /I /R $(OutputPath)$(AssemblyName).pdb ..\MVCPluginExample\bin" />
<Exec Command="xcopy /Y /E /R /I Views ..\MVCPluginExample\Plugins\Views" />
</Target>

With that at hand I'm able to debug the code in the class library because of the presence of .pdb file and all my views are nicely packaged in separate plugin projects.

But that's not all. To be able to use the views from controllers contained in the library (and to have them in a separate location so that I can easily clean up the main project) I was forced to use constructs as follows:

public ActionResult Index() {
return View("~/Plugins/Views/Home/Index.aspx");
}

Naturally it's a mess and one should never need to specify the full path to the view. It just doesn't make any sense.

It turns out that the list of locations where the MVC engine looks for views is stored in three fields: MasterLocationFormats, ViewLocationFormats and PartialViewLocationFormats (as per WebFormViewEngine.cs lines 23, 28 and 35). Since all we want to do is to instruct the engine to look for views in one more location (~/Plugins/Views/...) all we have to do is to inherit from WebFormViewEngine class and create a constructor that will provide this information. Here's the piece of code that does just that:

public class PluginAwareWebFormViewEngine : WebFormViewEngine {
public PluginAwareWebFormViewEngine() : base() {
ViewLocationFormats = new[] {
"~/Plugins/Views/{1}/{0}.aspx",
"~/Plugins/Views/{1}/{0}.ascx",
"~/Views/{1}/{0}.aspx",
"~/Views/{1}/{0}.ascx",
"~/Views/Shared/{0}.aspx",
"~/Views/Shared/{0}.ascx"
};

PartialViewLocationFormats = ViewLocationFormats;
}
}

Now we can finally get back to providing view names instead of file names in the controller action methods:

public ActionResult Index() {
return View("Index");
}

or even

public ActionResult Index() {
return View();
}

To register this new ViewEngine add the following line to Application_Start event handler in Global.asax.cs

ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new PluginAwareWebFormViewEngine());

What this does is it makes sure that there's only one view engine that is in fact modified by us.

Here you can download the complete solution.

mvc-plugin-example.zip


Have fun!

No comments: