Skip to content

What’s Inside Foundation – Features Folder – Implementation

What's Inside Foundationseries will explore the different aspects and patterns used in Episerver reference implementation, Foundation.

In the previous article of this series, I discussed how the features folder organization can be configured into a new Episerver site, using the reference implementation from Foundation.  In this article, I'll go into the detailed implementation and explain how your code can be structured using the features folder organization pattern.

To enable a project to use the features folder organization, we need to implement a few classes, the first one being an extension of the RazorViewEngine. We need to make our own RazorViewEngine implementation so that we can add the features folders into which the engine should look for our controllers and views. To achieve this, we need to append to the LocationFormats our own paths, matching a specific format. For those location format, there is two replacement token that we can use, and they would be:

{0} Action name (or Episerver ContentType)
{1} Controller name

So for an article page, we would create an Article feature folder, in which we would put the PageType, Controller and View.

"~/Features/Article/ArticlePage.cs"
"~/Features/Article/ArticlePageController.cs"
"~/Features/Article/Index.cshtml"

Let's configure our feature folder organization by creating a class that will extend RazorViewEngine

public class FeaturesViewEngine : RazorViewEngine
{
    ...
}

Then, in this class, we would need to declare the following two properties

private static readonly string[] AdditionalPartialViewFormats =
{
    "~/Features/Blocks/{0}.cshtml",
    "~/Features/Blocks/Views/{0}.cshtml",
    "~/Features/Shared/{0}.cshtml"
};
private readonly ConcurrentDictionary<string, bool> _cache = new ConcurrentDictionary<string, bool>();

AdditionalPartialViewFormats will contains a list of the folders where the ViewEngine will look for partial views. The \_cache will be used to check if the file exists for a defined feature.

Then we need to define the constructor.

public FeaturesViewEngine()
{
    ViewLocationCache = new DefaultViewLocationCache();
    var featureFolders = new[]
    {
        "~/Features/%1/{1}/{0}.cshtml",
        "~/Features/%1/{0}.cshtml"
    };
    featureFolders = featureFolders.Union(AdditionalPartialViewFormats).ToArray();
    ViewLocationFormats = ViewLocationFormats
        .Union(featureFolders)
        .ToArray();
    PartialViewLocationFormats = PartialViewLocationFormats
        .Union(featureFolders)
        .ToArray();
    MasterLocationFormats = MasterLocationFormats
        .Union(featureFolders)
        .ToArray();
}

The featureFolders variable is used to hold the basic features folders, to which we had the PartialViews locations from the previously defined property. We then assign those locations to the ViewLocation, PartialViewLocation and MasterLocation.

In the feature folders path declaration, there is a new replacement token that appears. This %1 token will be replaced by the actual name of the feature, obtained from a call to the GetFeatureName method. Following is the declaration for this method that will return the feature name out of a received Controller.

private string GetFeatureName(TypeInfo controllerType)
{
    var tokens = controllerType.FullName?.Split('.');
    if (!tokens?.Any(t => t == "Features") ?? true)
    {
        return "";
    }
    return tokens
        .SkipWhile(t => !t.Equals("features",
            StringComparison.CurrentCultureIgnoreCase))
        .Skip(1)
        .Take(1)
        .FirstOrDefault();
}

The name of the feature will be extracted from the full name of a controller, which is the name of the class controller including the namespace. The method first split the full name into tokens separated by a '.', and then return the first element it finds following the Features token. So for a controller that would have a full name of site.Features.Article.ArticlePageController, the feature name would be Article.

The next methods are methods that must be overridden. CreatePartialView and CreateView will call their base counterpart, passing in the location format strings in which the %1 feature token would have been replaced by the feature name.

protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
{
    if (controllerContext.Controller != null)
        return base.CreatePartialView(controllerContext,
        partialPath.Replace("%1", GetFeatureName(controllerContext.Controller.GetType().GetTypeInfo())));
    return base.CreatePartialView(controllerContext, partialPath);
}

protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
{
    return base.CreateView(controllerContext,
        viewPath.Replace("%1", GetFeatureName(controllerContext.Controller.GetType().GetTypeInfo())),
        masterPath.Replace("%1", GetFeatureName(controllerContext.Controller.GetType().GetTypeInfo())));
}

And the last method that needs to be overridden is the FileExists method that checks if the requested files exist for a feature.

protected override bool FileExists(ControllerContext controllerContext, string virtualPath)
{
    if (controllerContext.HttpContext != null && !controllerContext.HttpContext.IsDebuggingEnabled)
    {
        if (controllerContext.Controller == null)
        {
            return _cache.GetOrAdd(virtualPath, _ => base.FileExists(controllerContext, virtualPath));
        }
        return _cache.GetOrAdd(virtualPath,
            _ => base.FileExists(controllerContext, virtualPath.Replace("%1", GetFeatureName(controllerContext.Controller.GetType().GetTypeInfo()))));
    }
    if (controllerContext.Controller == null)
    {
        return base.FileExists(controllerContext, virtualPath);
    }
    return base.FileExists(controllerContext,
        virtualPath.Replace("%1", GetFeatureName(controllerContext.Controller.GetType().GetTypeInfo())));
}

The last thing that is needed is to register the ViewEngine. To do so, we need to add an InitializationEngine class extension to add our ViewEngine inthe ViewEngines. Then we would create an InitializableModule that will call the InitializeView extended method. Those new classes could be placed into an Initialization feature.

using EPiServer.Framework.Initialization;
using System.Web.Mvc;

namespace site.Features.ViewEngine
{
    public static class InitializationEngineExtensions
    {
        public static void InitializeView(this InitializationEngine context)
        {
              ViewEngines.Engines.Insert(0, new FeaturesViewEngine());
        }
    }
}
using System;
using System.Linq;
using EPiServer.Framework;
using EPiServer.Framework.Initialization;

namespace site.Features.ViewEngine
{
    [InitializableModule]
    [ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
    public class Initialize : IInitializableModule
    {
        void IInitializableModule.Initialize(InitializationEngine context)
        {
            context.InitializeView();
        }

        void IInitializableModule.Uninitialize(InitializationEngine context)
        {

        }
    }
}

The complete example of this implementation can be found in the Foundation GitHub repository (Foundation.Cms.Display.FeaturesViewEngine).

With this code in place, you would now have a working features folder structure, and you can add features to your project.

The general blocks and partial views would be included in the following folders:

  • ~/Features/Blocks/
  • ~/Features/Blocks/Views/
  • ~/Features/Shared/

Everything that would be page or technical features would be placed in their ~/Features/%[Feature] folder.

One thing to note about this implementation is that controller-less blocks would work as is if they are added to the Features/Blocks or Features/Blocks/Views folders. But if you try to add a controller-less block to a feature folder, the GetFeatureName method used when trying to search in every feature folder would then return nothing, since there is no controller linked to the block to identify the feature name. In that case the blocks would not be founded.

Now we know how to implement the feature folder organization for model, pages, controllers and general purpose blocks. I'll cover some ways to be able to add controller-less blocks into the feature folders into the next article of this series.

 REFERENCES

https://marisks.net/2017/02/03/razor-view-engine-for-feature-folders/