A critical vulnerability was discovered in React Server Components (Next.js). Our systems remain protected but we advise to update packages to newest version. Learn More

Kristian Elmholt Kjær
May 19, 2018
  471
(0 votes)

Custom View Locations for Pages, Blocks and more

Background

In a typical Episerver, or MVC Applicaton for that matter, the Views specified in a Controller are resolved by paths defined in the System.Web.Mvc.RazorViewEngine.

The paths for Views are:

  • "~/Views/{1}/{0}.cshtml"
  • "~/Views/Shared/{0}.cshtml"

In Episerver Page Controllers, we need to specify an Index-method with the Page-type as method parameter, like this:

public class StartPageController : PageController<StartPage>
{
    public ActionResult Index(StartPage currentPage)
    {
        return View(currentPage);
    }
}

If we take a look at the paths above, the name of the Controller (except -"Controller") will be used at "{1}", and the name of the Action is used at "{0}", meaning default View path for our Controller above will be:

  • "~/Views/StartPage/Index.cshtml"

Flooding of "Index.cshtml" files

If you like me, use ReSharper and namely the "Navigation -> Search Everywhere/Go to Type" function to navigate/find your Views files, the amount of Index.cshtml files you'd end up - by default - becomes annoying. You probably already know this, but the files and folders in your Website project, will be something like this:

  • Views
    • StartPage
      • Index.cshtml
    • LandingPage
      • Index.cshtml
    • CasePage
      • Index.cshtml
    • ArticlePage
      • Index.cshtml
    • Etc. etc.

Thankfully, we can make up for that!

Creating a custom ViewEngine

Go ahead and create a new class that inherits System.Web.Mvc.RazorViewEngine. Like this:

public class EpiserverViewEngine : System.Web.Mvc.RazorViewEngine
{
}

To define your own paths for Views (Pages and Catalog-nodes) and PartialViews (Blocks, Partials), add a constructor and set the value of ViewLocationFormats and PartialViewLocationFormats. Like this:

public class EpiserverViewEngine : System.Web.Mvc.RazorViewEngine
{
    public EpiserverViewEngine()
    {
        var partialViewLocations = new[]
        {
            "~/Views/Blocks/{1}.cshtml",
            "~/Views/Shared/{0}.cshtml",

            "~/Views/Blocks/{1}/{0}.cshtml",
            "~/Views/Shared/{1}/{0}.cshtml"
        };

        var viewLocations = new[]
        {
            "~/Views/Pages/{1}.cshtml",
            "~/Views/Nodes/{1}.cshtml",
            "~/Views/Catalog/{1}.cshtml",

            "~/Views/Pages/{1}/{0}.cshtml",
            "~/Views/Nodes/{1}/{0}.cshtml",
            "~/Views/Catalog/{1}/{0}.cshtml"
        };

        PartialViewLocationFormats = partialViewLocations;
        ViewLocationFormats = viewLocations;
    }
}

The above paths are the ones I use, and it gives much more flexibility. 

Potential problem

I've seen other examples of this where the paths above, are all set to the ViewLocationFormats property - and that includes both Views and PartialViews. This results in Episerver/MVC potentially resolving PartialViews as regular Views. And as we know, a PartialView should not be rendered with a Layout, which also includes model inheriting being a requirement.

I was trying to render a ContentArea in which I had inserted a Block of mine, and that resulted in an exception with the message:

The model item passed into the dictionary is of type 'EPiServer.Core.ContentArea', but this dictionary requires a model item of type 'Namespace.IPageViewModel<T>'

It took me quite some time debugging why this became a problem suddenly. When it finally struck me, that the PartialView belonging to my Block was being resolved as a regular View. So talk about a eureka-experience!

Register your ViewEngine

To register your new EpiserverViewEngine, create an InitializationModule - like this:

[InitializableModule]
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class ViewEngineInitializationModule : IInitializableModule
{
    public void Initialize(InitializationEngine context)
    {
        System.Web.Mvc.ViewEngines.Engines.Add(new EpiserverViewEngine());
    }

    public void Uninitialize(InitializationEngine context) {}
}

You are now ready to use it. You can leave your StartPageController (example above), and use paths like these instead:

  • Views/
    • Pages/
      • StartPage.cshtml
      • LandingPage.cshtml
      • CasePage.cshtml
      • ArticlePage/ (Folder to bundle stuff together)
        • Index.cshtml
        • _MyArticlePagePartial.cshtml
    • Blocks/
      • NewsletterBlock.cshtml
      • TeaserBlock.cshtml

Feel free to use this, modify it and happy coding!

/Kristian

May 19, 2018

Comments

Please login to comment.
Latest blogs
A day in the life of an Optimizely OMVP: Learning Optimizely Just Got Easier: Introducing the Optimizely Learning Centre

On the back of my last post about the Opti Graph Learning Centre, I am now happy to announce a revamped interactive learning platform that makes...

Graham Carr | Jan 31, 2026

Scheduled job for deleting content types and all related content

In my previous blog post which was about getting an overview of your sites content https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/1/sche...

Per Nergård (MVP) | Jan 30, 2026

Working With Applications in Optimizely CMS 13

💡 Note:  The following content has been written based on Optimizely CMS 13 Preview 2 and may not accurately reflect the final release version. As...

Mark Stott | Jan 30, 2026

Experimentation at Speed Using Optimizely Opal and Web Experimentation

If you are working in experimentation, you will know that speed matters. The quicker you can go from idea to implementation, the faster you can...

Minesh Shah (Netcel) | Jan 30, 2026