• Publisert
  • 4 min

Changing view folder location in CMS7 MVC

In CMS 7 Mvc projects the views folder can get messy. Here is an attempt to simplify the folder layout.

When using the default page view location in CMS7 Mvc projects I end up creating several folders with just one index.cshtml view. To simplify this I can put all page views in a single folder and be explicit about the name of the view to use in my action methods, or I can try to make a new convention. Here I will illustrate one approach based on registering a new view engine to handle it.

Background

After creating a few page and block types in a new project I ended up with one view folder per page/block controller and one index.cshtml file in each such folder; Following the pattern of "~/Views/[Controller]/[Action].cshtml". This is a nice enough convention but for most simple content types I would like to group the views together in one folder for page views and one for blocks.

My first thought was to simply be explicit about it and specify the view location in controller action code. Like this:

public ActionResult Index(ArticlePage currentPage)
{
   return View("~/Views/Pages/ArticleIndex.cshtml", currentPage);
}

But this does not feel like the best solution. Developers get headaches when repeating themselves, and the first parameter to the View method specifying the view folder location and file extension seems repetitive to me. I would like something a little more elegant.

So, the pattern I would like to use is something along the lines of ~/Views/Pages/[PageType][Action].cshtml. After thinking about this I realized this is basically the same as ~/Views/Pages/[Controller][Action].cshtml, and such a pattern can be handled nicely by registering a new view engine. That way my code can resolve the view locations by convention, and I don’t need to specify the view file explicitly in each controller action’s return statement.

I’ll use a page data type called ArticlePage and Razor as the view engine to illustrate. (Supporting other view engines as well is easy, but beside the scope here.)

A new view engine is born

I created a new view engine class that inherits from System.Web.Mvc.RazorViewEngine and then defined some rules for resolving view locations. Pretty straight forward (when you know that {1} is controller name {0} is action name and {2} is area name), I think. Here’s the code:

public class ContentViewEngine : RazorViewEngine
{
  public ContentViewEngine()
  {
    string[] areaViewLocationFormats = new[]
    {
      "~/Areas/{2}/Views/Blocks/{1}{0}.cshtml",
      //"~/Areas/{2}/Views/Blocks/{1}.cshtml",
      "~/Areas/{2}/Views/Pages/{1}{0}.cshtml"
    };

    string[] viewLocationFormats = new[]
    {
      "~/Views/Blocks/{1}{0}.cshtml",
      //"~/Views/Blocks/{1}.cshtml",
      "~/Views/Pages/{1}{0}.cshtml"
    };

    AreaViewLocationFormats = areaViewLocationFormats;
    AreaMasterLocationFormats = areaViewLocationFormats;
    AreaPartialViewLocationFormats = areaViewLocationFormats;
    ViewLocationFormats = viewLocationFormats;
    MasterLocationFormats = viewLocationFormats;
    PartialViewLocationFormats = viewLocationFormats;
    FileExtensions = new[] { "cshtml" };
  }
}

As you might see this will match block views without the action name. It seems like a good idea to be able to omit the action name for blocks, but I’m not sure this will always be the case, so I included two patterns for blocks¹.

I got some weird Stack Overflow Exceptions when rendering partial views from within block views (also partial) with the rule for matching blocks without the action name. I tried changing the view engine registration order around and also tried to include only one Razor view engine to get full control but I couldn’t get around it. So I commented it out and renamed all my block views (a suffix of "Index" for all of them, basically).

Then I created some registration logic to make sure my custom view engine kicks in before the standard Razor view engine²:

public class ViewEngineRegistration
{
  public static void Register(ViewEngineCollection engines)
  {
    RazorViewEngine razorViewEngine = engines
      .OfType<RazorViewEngine>()
      .FirstOrDefault();

    ContentViewEngine contentViewEngine = new ContentViewEngine();

    if (null == razorViewEngine)
    {
      engines.Add(contentViewEngine);
    }
    else
    {
      int index = engines.IndexOf(razorViewEngine);
      engines.Insert(index, contentViewEngine);
    }
  }
}

Hook it up in global application start:

protected void Application_Start()
{
  //other stuff omitted
  ViewEngineRegistration.Register(ViewEngines.Engines);
}

Now I can write my action method like this:

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

And structure my views like this:
~/Views/Blocks/Image.cshtml

~/Views/Pages/ArticleIndex.cshtml
~/Views/Pages/SearchIndex.cshtml
~/Views/Pages/StartIndex.cshtml
… 

Summary

My solution has less folders with just one file in them, so I’m happy with the result. I think this folder structure is something other EPiServer CMS developers will find familiar as well, since many has used a similar folder layout for templates (.aspx) and units (.ascx) in the past. And this is just an additional view engine, so you could always create a view following standard folder layout if you like. A richer page controller with more than one action method returning different views, for instance, might be a good candidate for using the standard Razor view engine.

One drawback right now is that JetBrains’s ReSharper doesn’t understand the additional view engine and complains with the message "Cannot resolve view 'Index'" when inspecting the controller class (tested in version 7.1). I have created a support ticket with JetBrains requesting a solution to this issue. Update: Please vote for the fix here: http://youtrack.jetbrains.com/issue/RSRP-337276

¹ The first version of this view engine I called PageViewEngine and it did not specify rules for blocks, but after thinking about it I renamed it to ContentViewEngine and made rules for both blocks and pages.

² The order of resolving is kind of important to think about. I suppose a more explicit one should be higher up in the chain, just like when registering routes. If I were to create two Razor views with matching location for my action method, I would like my new view engine to handle it because it is more explicit than the standard one.