SaaS CMS has officially launched! Learn more now.

Martin Ryttler
Feb 1, 2018
  3328
(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 London Dev Meetup 11th July 2024

On 11th July 2024 in London Niteco and Netcel along with Optimizely ran the London Developer meetup. There was an great agenda of talks that we put...

Scott Reed | Jul 19, 2024

Optimizely release SaaS CMS

Discover the future of content management with Optimizely SaaS CMS. Enjoy seamless updates, reduced costs, and enhanced flexibility for developers...

Andy Blyth | Jul 17, 2024 | Syndicated blog

A day in the life of an Optimizely Developer - London Meetup 2024

Hello and welcome to another instalment of A Day In The Life Of An Optimizely Developer. Last night (11th July 2024) I was excited to have attended...

Graham Carr | Jul 16, 2024

Creating Custom Actors for Optimizely Forms

Optimizely Forms is a powerful tool for creating web forms for various purposes such as registrations, job applications, surveys, etc. By default,...

Nahid | Jul 16, 2024

Optimizely SaaS CMS Concepts and Terminologies

Whether you're a new user of Optimizely CMS or a veteran who have been through the evolution of it, the SaaS CMS is bringing some new concepts and...

Patrick Lam | Jul 15, 2024

How to have a link plugin with extra link id attribute in TinyMce

Introduce Optimizely CMS Editing is using TinyMce for editing rich-text content. We need to use this control a lot in CMS site for kind of WYSWYG...

Binh Nguyen Thi | Jul 13, 2024