Don't miss out Virtual Happy Hour this Friday (April 26).

Try our conversational search powered by Generative AI!

Martin Ryttler
Feb 1, 2018
  3274
(4 votes)

Custom view engine to organize views

Our typical Episerver projects normally includes about ten to twenty page types and about the same amount of block types. Many of these are pages with multiple views. And to keep the project manageable we needed a way to organize these into an easy folder structure. Our approach was to build a custom RazorViewEngine that fitted our Episerver project patterns and that was to group all views for a page type under a folder with the same name as the Model and place them under /Views/Pages. And the same for the block types. Something like this

  • Views
    • Pages
      • StartPage
        • Index.cshtml
        • ...
      • MyProfilePage
        • Edit.cshtml
        • Error.cshtml
        • Index.cshtml
        • Updated.cshtml
        • ...
      • ...
    •  Blocks
      • LoginBlock
        • Error.cshtml
        • Index.cshml
        • ...
      • ...

There are other examples out there of how to handle this but we’ve added a few tweaks of our own to make this fit Episerver projects that makes it worth mentioning:

  • First of all we wanted to assure that this didn’t interfere with any other view handling
  • We wanted to use the name of the Model instead of the Controller since we're really trying to limit our controllers and instead use generic controllers
  • It also needed to work with our pattern using a IPageViewModel (known from Episerver Alloy templates) but we didn’t want to require the use of a IPageViewModel

This is what we ended up with.

namespace Metamatrix.Web.Episerver.Base.Business.Initialization
{
    using System;
    using System.Linq;
    using System.Web.Mvc;
    using EPiServer;
    using Models.Blocks.Base;
    using Models.Pages.Base;
    using Models.Settings;
    using Models.ViewModels;

    /// <summary>
    ///     An RazorViewEngine that's adapted to find view on a path based on the EPi GetOriginalType of the model instead of
    ///     the name of the controller.
    ///     This makes it possible to create more generic controllers
    /// </summary>
    public class ExtendedRazorViewEngine : RazorViewEngine
    {
        /// <summary>
        ///     Finds the specified partial view by using the specified controller context.
        /// </summary>
        /// <param name="controllerContext">The controller context.</param>
        /// <param name="partialViewName">The name of the partial view.</param>
        /// <param name="useCache">true to use the cached partial view.</param>
        /// <returns>
        ///     The partial view.
        /// </returns>
        /// <exception cref="System.ArgumentNullException">When the controllerContext parameter is null</exception>
        /// <exception cref="System.ArgumentException">When partialViewName parameter is null or empty.</exception>
        public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName,
            bool useCache)
        {
            if (controllerContext == null)
                throw new ArgumentNullException(nameof(controllerContext), "The controllerContext parameter is null");

            if (string.IsNullOrEmpty(partialViewName))
                throw new ArgumentException("The viewName parameter is null or empty.", nameof(partialViewName));

            if (controllerContext.Controller != null)
            {
                var modelName = GetModelname(controllerContext.Controller.ViewData.Model);
                if (!string.IsNullOrEmpty(modelName))
                {
                    var cacheKey = $"{modelName}|{partialViewName}";

                    if (useCache && ViewLocationCache != null)
                    {
                        var cachedLocation = ViewLocationCache.GetViewLocation(controllerContext.HttpContext, cacheKey);
                        if (!string.IsNullOrEmpty(cachedLocation))
                            return new ViewEngineResult(CreatePartialView(controllerContext, cachedLocation), this);
                    }

                    string trimmedViewName;
                    if (partialViewName.EndsWith(".cshtml") || partialViewName.EndsWith(".vbhtml"))
                        trimmedViewName = partialViewName.Remove(partialViewName.Length - 7);
                    else
                        trimmedViewName = partialViewName;
                    var args = new object[] {trimmedViewName, modelName};

                    foreach (var location in PartialViewLocationFormats)
                    {
                        var path = string.Format(location, args);
                        if (FileExists(controllerContext, path))
                        {
                            ViewLocationCache?.InsertViewLocation(controllerContext.HttpContext, cacheKey, path);
                            return new ViewEngineResult(CreatePartialView(controllerContext, path), this);
                        }
                    }
                    return new ViewEngineResult(PartialViewLocationFormats.Select(i => string.Format(i, args)));
                }
            }
            return base.FindPartialView(controllerContext, partialViewName, useCache);
        }


        /// <summary>
        ///     Finds the specified view by using the specified controller context and master view name.
        /// </summary>
        /// <param name="controllerContext">The controller context.</param>
        /// <param name="viewName">The name of the view.</param>
        /// <param name="masterName">The name of the master view.</param>
        /// <param name="useCache">true to use the cached view.</param>
        /// <returns>
        ///     The page view.
        /// </returns>
        /// <exception cref="System.ArgumentNullException">When controllerContext parameter is null</exception>
        /// <exception cref="System.ArgumentException">When the viewName parameter is null or empty.</exception>
        public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName,
            string masterName, bool useCache)
        {
            if (controllerContext == null)
                throw new ArgumentNullException(nameof(controllerContext), "The controllerContext parameter is null");

            if (string.IsNullOrEmpty(viewName))
                throw new ArgumentException("The viewName parameter is null or empty.", nameof(viewName));

            if (controllerContext.Controller != null)
            {
                var modelName = GetModelname(controllerContext.Controller.ViewData.Model);
                if (!string.IsNullOrEmpty(modelName))
                {
                    var cacheKey = $"{modelName}|{viewName}";

                    if (useCache && ViewLocationCache != null)
                    {
                        var cachedLocation = ViewLocationCache.GetViewLocation(controllerContext.HttpContext, cacheKey);
                        if (!string.IsNullOrEmpty(cachedLocation))
                            return new ViewEngineResult(CreateView(controllerContext, cachedLocation, masterName),
                                this);
                    }

                    string trimmedViewName;
                    if (viewName.EndsWith(".cshtml") || viewName.EndsWith(".vbhtml"))
                        trimmedViewName = viewName.Remove(viewName.Length - 7);
                    else
                        trimmedViewName = viewName;
                    var args = new object[] {trimmedViewName, modelName};

                    foreach (var location in ViewLocationFormats)
                    {
                        var path = string.Format(location, args);
                        if (FileExists(controllerContext, path))
                        {
                            ViewLocationCache?.InsertViewLocation(controllerContext.HttpContext, cacheKey, path);
                            return new ViewEngineResult(CreateView(controllerContext, path, masterName), this);
                        }
                    }
                    return new ViewEngineResult(ViewLocationFormats.Select(i => string.Format(i, args)));
                }
            }
            return base.FindView(controllerContext, viewName, masterName, useCache);
        }

        private string GetModelname(object model)
        {
            if (model is BaseBlock || model is BasePage)
                return model.GetOriginalType().Name;

            var viewModel = model as IPageViewModel<BasePage>;
            return viewModel?.CurrentPage.GetOriginalType().Name ?? "";
        }
    }
}

It’s actually the method GetModelName that fulfills the three requirements above. We’re checking if the model inherits from either our BaseBlock or BasePage, or if it’s an IPageViewModel<BasePage>. In our solutions BaseBlock and BasePage are the base classes for all our blocks and pages. Slim base classes that inherits from Episervers BlockData and PageData. We also use the GetOriginalType extension to make sure that we get our Type and not the generated proxy.

I hope all of you’re using some kind of base classes for your blocks and pages (if not you should consider it) but otherwise it should be easy to adapt the GetModelName method to fit your needs.

We put this to use by registering a new ViewEngine in the Application_Start event and here we specify the actual path patterns to use. The symbol {0} is replaced by the view name, and {1} is the name of the model.

	public class EPiServerApplication : Global
	{
		protected void Application_Start()
		{

			...

			RegisterViewLocations();

			...

		}

		private static void RegisterViewLocations()
		{
			ViewEngines.Engines.Add(new ExtendedRazorViewEngine
			{
				ViewLocationFormats = new[]
				{
					"~/Views/Pages/{1}/{0}.cshtml"
				},
				PartialViewLocationFormats = new[]
				{
					"~/Views/Shared/Partials/{0}.cshtml",
					"~/Views/Pages/{1}/{0}.cshtml",
					"~/Views/Blocks/{1}/{0}.cshtml"
				}
			});
		}

	}
}

That's wraps this up. I hope that it could be of some use or atleast give you guys some ideas to be creative.

Feb 01, 2018

Comments

valdis
valdis Feb 1, 2018 11:41 AM

why not feature folders?

Please login to comment.
Latest blogs
Optimizely Unit Testing Using CmsContentScaffolding Package

Introduction Unit tests shouldn't be created just for business logic, but also for the content and rules defined for content creation (available...

MilosR | Apr 26, 2024

Solving the mystery of high memory usage

Sometimes, my work is easy, the problem could be resolved with one look (when I’m lucky enough to look at where it needs to be looked, just like th...

Quan Mai | Apr 22, 2024 | Syndicated blog

Search & Navigation reporting improvements

From version 16.1.0 there are some updates on the statistics pages: Add pagination to search phrase list Allows choosing a custom date range to get...

Phong | Apr 22, 2024

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