Try our conversational search powered by Generative AI!

Kristian Elmholt Kjær
May 19, 2018
  4224
(3 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

valdis
valdis May 20, 2018 07:34 PM

hi,

I would rather look for some use-cases driven coupling (e.g. feature folders). colleague of mine has been busy lately to improve FeatureFolders view engine.. ;)

May 21, 2018 09:32 AM

Hi

One thing to be aware of is that GetPartialView is called when @Html.DisplayFor (or @Html.PropertyFor) is called so register patterns using only {1} and not {0} (that is only using controller and not the actual object/type passed into DisplayFor/PropertyFor, that will be {0}) can give some unwanted result. 

If you register the custom engine with "Engines.Add(yourEngine)" (as you do) then it will be fine since then the default view engine is registered before the custom one and that one handles DisplayFor for primitive types like string. If you however register the custom engine with "Engines.Insert(0, yourEngine)" then you could get unexpected results.

You can try that by register the engine with "Engines.Insert(0, yourEngine)" instead and then in the view for a block call something like @Html.DisplayFor(m => m.SomeString). That call will fail with a message saying something like "cant cast string to theBlock". 

So the recommendation is to always (as you do) register a custom view engine after the default one.

Puspack Travel
Puspack Travel Sep 8, 2018 06:46 AM

its very helpful for all the new comers. You mentions all the step very clearly and its very informative. Puspack give a thumbs up for your effort.

Balvinder Singh
Balvinder Singh Sep 10, 2018 11:16 AM

very nice information you share with us. i like it. please update this kind of more information here

Lyricsism

Please login to comment.
Latest blogs
Optimizely and the never-ending story of the missing globe!

I've worked with Optimizely CMS for 14 years, and there are two things I'm obsessed with: Link validation and the globe that keeps disappearing on...

Tomas Hensrud Gulla | Apr 18, 2024 | Syndicated blog

Visitor Groups Usage Report For Optimizely CMS 12

This add-on offers detailed information on how visitor groups are used and how effective they are within Optimizely CMS. Editors can monitor and...

Adnan Zameer | Apr 18, 2024 | Syndicated blog

Azure AI Language – Abstractive Summarisation in Optimizely CMS

In this article, I show how the abstraction summarisation feature provided by the Azure AI Language platform, can be used within Optimizely CMS to...

Anil Patel | Apr 18, 2024 | Syndicated blog

Fix your Search & Navigation (Find) indexing job, please

Once upon a time, a colleague asked me to look into a customer database with weird spikes in database log usage. (You might start to wonder why I a...

Quan Mai | Apr 17, 2024 | Syndicated blog

The A/A Test: What You Need to Know

Sure, we all know what an A/B test can do. But what is an A/A test? How is it different? With an A/B test, we know that we can take a webpage (our...

Lindsey Rogers | Apr 15, 2024

.Net Core Timezone ID's Windows vs Linux

Hey all, First post here and I would like to talk about Timezone ID's and How Windows and Linux systems use different IDs. We currently run a .NET...

sheider | Apr 15, 2024