Apr 29 2013

A View Engine for ASP.NET MVC Feature-Based Organized

Category: MVC | ASP.NETMatt @ 11:25

I am a huge fan of ASP.NET MVC.  It is leaps and bounds ahead of WebForms, and if you’re doing web development on the Microsoft platform, it’s arguably the best overall solution available.  But it is far from perfect.  One of the things that has bugged me about it since the very beginning is the default organization conventions, meaning separate folders for controllers, view models, and views.  These conventions can be replaced though.  Read on to see how.

The Default Conventions

If you create a new MVC project using the typical File –> New Project approach, you’re going to end up with something that looks like this:

image

Your controllers go in one folder, your views in another, and your models in yet another folder.  I suppose one could come up with a worse way to organize things, but you’d have to work at it a bit.  This approach is akin to creating separate folders for classes, interfaces, enums, etc.  Type-based organization flies in the face of good design principles.  The Common Closure Principle doesn’t exactly fit here, but I believe the intent does.  Instead of organizing by type, I much prefer to organize things by feature, with all the files that are involved in a feature living as close to one another as is possible.  Why, you ask?  Things that are likely to change together should live close together because it makes navigating (and therefore maintaining) the code much simpler.  I don’t want to bounce around between three or four folders while trying to implement a feature, nor hunt around to see where something is used.  Instead, I want everything that is related, everything that I should have in my mental context, in one convenient location.   I don’t want 50 controllers to weed through, I want to see the one controller that implements the feature I’m working on.  I don’t want 100 view folders to scan, I want to see the relevant views sitting right beside my controller.  Everything else is just noise.

But, that’s not how MVC works.  Out of the box it wants you to have separate folders for each type, with the implementation of each feature spread all across your project. Areas can help a bit with clutter and organization, and Visual Studio and many of the other productivity tools out there try to alleviate some of this pain by making it easy to bounce around, but the core of the problem is still there: we should organize by feature, not by type.  Fortunately this is actually quite doable. 

A Custom ViewEngine

ASP.NET MVC will actually let us put our controllers wherever we want.  As long as it implements IController, we can define routes to it.  And models can also live wherever we want.  The problem is the views.  The default convention is to resolve views using something like “~/Views/{Controller}/{Action}.”   It’s a little more complex than that, but not by much.  What I’d like is a convention that supports this type of layout:

image

Notice how everything related to logging in is in the same place: the controller, the models, and the views.  I can ignore everything outside of this scope because it isn’t relevant to the feature. 

We can take a naïve approach and swap this convention in easily enough, enabling us to do something like “~/Features/{Controller}/{Action}.” Here’s an example from Mohamed Meligy.  This approach somewhat works, but it will break down if we ever have a feature folder whose name doesn’t match 1-to-1 with the corresponding controller.  And that’s going to happen the first time you have two controllers related to the same feature.  Instead, assuming we keep our folder names and namespaces in sync, we can take a more robust approach and use the namespace of the controller to locate a corresponding “Views” folder:

public class FolderPerFeatureConventionViewEngine : RazorViewEngine
{
    //This needs to be initialized to the root namespace of your MVC project.
    //Usually, the namespace of your Global.asax's codebehind will do the trick.
    private static readonly string RootNamespace = typeof (MvcApplication).Namespace;

    private static string GetPath(ControllerContext controllerContext, string viewName)
    {
        //TODO: Cache?
        var controllerType = controllerContext.Controller.GetType();
        var featureFolder = "~" + controllerType.Namespace.Replace(RootNamespace, string.Empty).Replace(".", "/");

        var path = featureFolder + "/Views/" + viewName + ".cshtml";
        return path;
    }

    public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
    {
        var path = GetPath(controllerContext, viewName);

        if (VirtualPathProvider.FileExists(path))
        {
            return new ViewEngineResult(CreateView(controllerContext, path, null), this);
        }
        else
        {
            return new ViewEngineResult(new[] {path});
        }
    }

    public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
    {
        var path = GetPath(controllerContext, partialViewName);

        if (VirtualPathProvider.FileExists(path))
        {
            return new ViewEngineResult(CreateView(controllerContext, path, null), this);
        }
        else
        {
            return new ViewEngineResult(new[] { path });
        }
    }
}

Another nice benefit of this approach is that it negates the need for areas.  I can now organize my features however I want, keeping related files packaged together, and avoid the complexity that comes with areas.

Future Work

This code is definitely not production-tested.  I threw it together quickly, and I’m quite sure there are holes in it.  It could also benefit from some caching.  If you take this and extend it, please let me know, and I will update this post with any improvements that are made.

Tags: ,

blog comments powered by Disqus