I find that many of the components I develop in Sitecore need custom markup in the Experience Editor that shouldn’t be present on the live site. Although it’s possible to add this custom markup into my components’ views with @if (Sitecore.Context.PageMode.IsExperienceEditor), I prefer to keep branching logic out of my views as much as possible.

Fortunately Sitecore is built on top of ASP.NET MVC, so it’s easy to create a custom view engine that will serve up different views for our components in the Experience Editor. In this article I’ll demonstrate a custom view engine that will render views suffixed with .EE when users are in the Experience Editor.

Create the View Engine

The first step is to create the ExperienceEditorViewEngine. I've chosen to implement this as a RazorViewEngine decorator so I can easily apply it to other view engines in my applications.

using System.Linq;
using System.Text.RegularExpressions;
using System.Web.Mvc;

namespace SitecoreDemo.Web.Infrastructure
{
  public class ExperienceEditorViewEngine : IViewEngine
  {
    private readonly RazorViewEngine _viewEngine;

    public ExperienceEditorViewEngine(RazorViewEngine viewEngine)
    {
      _viewEngine = viewEngine;
    }

    public ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
    {
      return !IsExperienceEditorMode() ? NullViewEngineResult() :
        _viewEngine.FindPartialView(controllerContext, GetExperienceEditorViewName(partialViewName), false);
    }

    public ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
    {
      return !IsExperienceEditorMode() ? NullViewEngineResult() :
        _viewEngine.FindView(controllerContext, GetExperienceEditorViewName(viewName), masterName, false);
    }

    public void ReleaseView(ControllerContext controllerContext, IView view)
    {
      _viewEngine.ReleaseView(controllerContext, view);
    }

    private static bool IsExperienceEditorMode()
    {
      return Sitecore.Context.PageMode.IsExperienceEditor;
    }

    private static ViewEngineResult NullViewEngineResult()
    {
      return new ViewEngineResult(Enumerable.Empty<string>());
    }

    private static string GetExperienceEditorViewName(string viewName)
    {
      if (IsApplicationRelativePath(viewName))
      {
        return Regex.Replace(viewName, @"^(.*)\.(cshtml)$", "$1.EE.$2");
      }
      return viewName + ".EE";
    }

    private static bool IsApplicationRelativePath(string viewName)
    {
      return viewName[0] == '~' || viewName[0] == '/';
    }
  }
}

Here's a breakdown of the view engine:

public ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
{
  return !IsExperienceEditorMode() ? NullViewEngineResult() :
    _viewEngine.FindPartialView(controllerContext, GetExperienceEditorViewName(partialViewName), useCache);
}

public ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
  return !IsExperienceEditorMode() ? NullViewEngineResult() :
    _viewEngine.FindView(controllerContext, GetExperienceEditorViewName(viewName), masterName, useCache);
 }

The MVC framework will call FindView and FindPartialView to find views and partial views, respectively. If users aren't in the Experience Editor, a NullViewEngineResult will be returned and the next view engine in the view engine collection will continue the search; otherwise, the view name or partial view name will be transformed into the Experience Editor view name (e.g., MyView.EE) and the underlying view engine will search for it. If the view is found, it'll be returned; otherwise the next view engine in the view engine collection will continue the search.

public void ReleaseView(ControllerContext controllerContext, IView view)
{
  _viewEngine.ReleaseView(controllerContext, view);
}

The MVC framework will call ReleaseView to release views after they have rendered.

private static bool IsExperienceEditorMode()
{
  return Sitecore.Context.PageMode.IsExperienceEditor;
}

Returns true if the user is in the Experience Editor, false otherwise.

private static ViewEngineResult NullViewEngineResult()
{
  return new ViewEngineResult(Enumerable.Empty());
}

When MVC is unable to find a view, it throws an exception that notifies you that the view could not be found and lists all of the locations searched, like in the image below.

View not found exception.

ViewEngineResult has two constructors: one that takes a view when a view is found, and one that takes a collection of searched paths when a view is not found. If a view is not found by any view engine, all of the searched paths will be unioned together and returned in an exception. Since the ExperienceEditorViewEngine isn't going to do any work when users are not in the Experience Editor, this method just returns a ViewEngineResult with no searched paths.

private static string GetExperienceEditorViewName(string viewName)
{
  if (IsApplicationRelativePath(viewName))
  {
    return Regex.Replace(viewName, @"^(.*)\.(cshtml)$", "$1.EE.$2");
  }
  return viewName + ".EE";
}

private static bool IsApplicationRelativePath(string viewName)
{
  return viewName[0] == '~' || viewName[0] == '/';
}

In MVC you can lookup views by specifying just the view name, e.g., MyView, or by specifying the application-relative path, e.g., ~/Views/Example/MyView.cshtml. If the view name has been specified as an application-relative path, .EE will be added just before the extension, e.g., ~/Views/Example/MyView.EE.cshtml. If the view name is not an application-relative path, .EE will be appended onto the view name, and the view engine will look for MyView.EE.

Credit to the Regex Wizard Jon Upchurch for the regular expression to append .EE on to application-relative view paths. Credit to the ASP.NET Core MVC developers for the IsApplicationRelativePath code.

Register the View Engine with MVC

Once you've created the view engine, create a pipeline processor to register the view engine in your application like so:

using System.Linq;
using System.Web.Mvc;
using Sitecore.Pipelines;
using SitecoreDemo.Web.Infrastructure;

namespace SitecoreDemo.Web
{
  public class InitializeViewEngines
  {
    public void Process(PipelineArgs args)
    {
      RegisterViewEngines(ViewEngines.Engines);
    }

    private static void RegisterViewEngines(ViewEngineCollection viewEngines)
    {
      var razorViewEngines = viewEngines.OfType<RazorViewEngine>().Reverse();
      foreach (var razorViewEngine in razorViewEngines)
      {
        viewEngines.Insert(0, new ExperienceEditorViewEngine(razorViewEngine));
      }
    }
  }
}

As I stated previously, I chose to implement the ExperienceEditorViewEngine as a decorator so it could wrap other view engines in the application. The RegisterViewEngines method will create decorated versions of view engines that are already registered in the application and add them to the top of the collection in the same order as the originals.

The order in which view engines exist in the collection is important--the first view engine to find a matching view wins. Since some of our components will have two views, Component.cshtml and Component.EE.cshtml, it's important that if we're in the Experience Editor our ExperienceEditorViewEngines run before the other view engines.

For example, consider a scenario where you have two view engines in your view engine collection:

  1. GnarlyViewEngine
  2. TubularViewEngine

After RegisterViewEngines executes, the view engine collection will be as follows:

  1. ExperienceEditorGnarlyViewEngine
  2. ExperienceEditorTubularViewEngine
  3. GnarlyViewEngine
  4. TubularViewEngine

Now when users are in the Experience Editor, experience editor views will be searched in the exact same order that regular views would be searched when not in the experience editor. For components that don't have an experience-editor view, the normal view engines will still be in the collection to find non-experience-editor views.

Patch the View Engine Initializer into the Sitecore Pipeline

Patch the pipeline processor right after Sitecore.Mvc.Pipelines.Loader.InitializeRoutes:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/">
  <sitecore>
    <pipelines>
      <initialize>
        <processor type="SitecoreDemo.Web.InitializeViewEngines, SitecoreDemo.Web"
                   patch:after="processor[@type='Sitecore.Mvc.Pipelines.Loader.InitializeRoutes, Sitecore.Mvc']" />
      </initialize>
    </pipelines>
  </sitecore>
</configuration>

As long as your view engines are registered in the initialize pipeline, it's not really a big deal where you patch your processor in. However, Sitecore adds some custom view paths to the out-of-the-box RazorViewEngine in the InitializeRoutes processor, so I like to register my view engines right afterwards.

Demo Time

With the view engines registered, you can test drive the new functionality. As a demo, I've created a tile wrapper component that has three placeholders. In the Experience Editor, a dotted border will be drawn around the placeholders to make them easy for content authors to locate; on the live site the borders will not be present (note: I am not advocating this as a valid scenario for separate views, this is just for demonstration purposes).

The controller and action method are simple:

using System.Web.Mvc;
using Sitecore.Mvc.Controllers;

namespace SitecoreDemo.Web.Controllers
{
  public class TileWrapperController : SitecoreController
  {
    public ActionResult TileWrapper()
    {
      return View();
    }
  }
}

In my Views folder I've created the two separate views:

Separate views in Visual Studio.

Now when I navigate to a page with my TileWrapper component, a different view is rendered in the Experience Editor and on the live site:

Side-by-side views.

Notice how the TileWrapper on the left has a dotted border around the placeholders whereas the border is not present on the right.

Let me know in the comments if this custom View Engine helps you in your projects or how you've extended it to fit your own needs. Good luck!

Update (June 5, 2016)

I discovered an issue with the ExperienceEditorViewEngine code above that can cause Experience Editor views to never be rendered in the Experience Editor on content management servers. To avoid this issue, I've updated the code to force the ExperienceEditorViewEngine to always call into its underlying view engine with the useCache parameter set to false in the FindPartialView and FindView methods.

When MVC does view look ups, it first has all of the view engines look up views using their view location cache; if no view is found, it then has all of the view engines look up views by searching for the views on disk. This makes perfect sense from a performance standpoint, but was the source of a subtle bug.

Consider what happens when someone accesses a component (Component.cshtml) that has an Experience Editor view (Component.EE.cshtml) from outside the Experience Editor right after application start up. For simplicity's sake, imagine there's just one RazorViewEngine, and one ExperienceEditorViewEngine in the application.

  1. The ExperienceEditorViewEngine will be instructed to lookup the view (Component.EE.cshtml) from its cache, but since the application isn't in Experience Editor mode, it will be skipped.
  2. The RazorViewEngine will be instructed to lookup the view (Component.cshtml) from its cache, but since the application has just started, no view locations are cached, and it will not find the view.
  3. The ExperienceEditorViewEngine will be instructed to lookup the view (Component.EE.cshtml) from disk, but since the application isn't in Experience Editor mode, it will be skipped.
  4. The RazorViewEngine will be instructed to lookup the view (Component.cshtml) from disk, it will find it, cache the view location, and return the view.

Now, consider what happens when a content author tries to access that component from the Experience Editor:

  1. The ExperienceEditorViewEngine will be instructed to lookup the view (Component.EE.cshtml) from its cache, but since it hasn't found the view before, it will not find the view.
  2. The RazorViewEngine will be instructed to lookup the view from its cache (Component.cshtml), and since it found the view before, it will return the view.

In this scenario, the ExperienceEditorViewEngine will never get a chance to cache the view location of Component.EE.cshtml because the RazorViewEngine will always find Component.cshtml from its cache first.

Forcing the ExperienceEditorViewEngine to skip caching and always look for the view on disk was a quick fix for this issue. Another approach could be to force the ExperienceEditorViewEngine to look up the view when not in Experience Editor mode, but still return NullViewEngineResult(), thereby forcing the ExperienceEditorViewEngine to cache Experience Editor view locations even when not in Experience Editor mode.

Naturally I'd like for the ExperienceEditorViewEngine to take advantage of the view location cache, but pragmatically I think there are a few things that make the solution above acceptable: first, the ExperienceEditorViewEngine only ever does work in the Experience Editor, so the live site will never be affected by the lack of view location caching in the ExperienceEditorViewEngine; second, none of our application's pages have that many components, so view location caching isn't going to improve page load time by an appreciable amount.

Let me know your thoughts on this issue in the comments!