I just started a new job, they are working with Features folders instead of the more common convention where you have Controllers in one folder, Models in another, Views in another and so on. I've never thought about using Feature folders before, I've liked the convention, but I can really see some benefits with Feature Folders, especially when working with EPiServer where you often create Features, like a Slideshowblock for example, it's nice to have everything in the same place!

If you use the conventions, you would place the SlideshowBlockController in the Controllers folder, the SlideshowBlock in the Model/Blocks folder and the partial view in the Views/Shared folder.

But if you use Feature Folders instead, you would place everything in the same folder, Features/Slideshow, pretty neat, right?! :)

Here's an image showing how I've restructured the Alloy project using Feature folders instead of the conventions.

If you want to use Feature folders in your own project there's a couple of things you will need todo:

  1. Create a Features folder
  2. Copy the Web.config and the _viewstart.cshtml and place them in the Features folder
    If you're moving everything from the Views folder, do a find and replace in the whole solution on "~/Views/" and replace it with "~/Features/"
  3. Create a custom View engine and register it in Global.asax

The third step is a bit tricky, so I will show you how I did below, you can just copy paste it into your project, and modify it however you want. Don't forget to register the View engine in Global.asax like this(Im also removing the WebForms view engine because I don't need it)

protected void Application_Start()  
{
    ........
    ViewEngines.Engines.Remove(ViewEngines.Engines.OfType<WebFormViewEngine>().First());
    ViewEngines.Engines.Add(new FeaturesLayoutViewEngines());
}

The idea is that Im registering a View Engine path looking like this:
~/Features/%FEATURE%/{0}.cshtml.

It would be possible to register it like this ~/%FEATURE%/{0}.cshtml and then skip the removing of the second part in the namespace, that would mean that you would be able to place your Features in any folder, but I want to restrict this functionallity to just the Features folder.

This means that the system will look for my views in the %FEATURE% folder. But...how does it know which feature to look for?

Great question!

Im overriding the methods CreateView, CreatePartialView and FileExists and before calling the base implementation I replace the %FEATURE% with the Controllers namespace, pretty neat!

The reason for doing all of this is to allow multiple controllers in the same feature folder, it wouldn't work with just a ~/Feature/{1}/{0}.cshtml path because then you would be tied to just one controller per folder.

Examples:

The View Engine path for my feature StartPage would look like this: ~/Features/StartPage/index.cshtml because the StartPage controller namespace is Alloy.Features.StartPage.

For the HeadingBlock feature it would be ~/Features/StartPage/HeadingBlock.cshtml

Im also registering the path ~/Features/Shared/{0}.cshtml so I can support the Header/Footer partials and stuff like that. Feel free to add your own! :)

Below is the full code for the View Engine, have fun!

using System.Linq;  
using System.Web.Mvc;

namespace Alloy  
{
    public class FeaturesLayoutViewEngine : RazorViewEngine
    {
        private const string FeaturePlaceholder = "%FEATURE%";

        public FeaturesLayoutViewEngine()
        {
            var viewEnginePaths = new[] {
                "~/Features/" + FeaturePlaceholder + "/{0}.cshtml",
                "~/Features/" + FeaturePlaceholder + "/{0}.vbhtml",
                "~/Features/Shared/{0}.cshtml"
            };

            base.PartialViewLocationFormats = viewEnginePaths;
            base.ViewLocationFormats = viewEnginePaths;
            base.MasterLocationFormats = viewEnginePaths;
        }

        /// <summary>
        /// Replaces the %FEATURE% placeholder in the virtualpath and checks if the requested view exists.
        /// </summary>
        /// <param name="controllerContext"></param>
        /// <param name="viewPath"></param>
        /// <param name="masterPath"></param>
        /// <returns></returns>
        protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
        {
            if (!viewPath.Contains(FeaturePlaceholder) || controllerContext.Controller == null)
            {
                return base.CreateView(controllerContext, viewPath, masterPath);
            }

            var fullFeaturePath = GetFeaturePath(controllerContext.Controller);
            if (fullFeaturePath == null) return base.CreateView(controllerContext, viewPath, masterPath);
            viewPath = ReplaceFeaturePlaceholder(viewPath, fullFeaturePath);
            return base.CreateView(controllerContext, viewPath, masterPath);
        }

        /// <summary>
        /// Replaces the %FEATURE% placeholder in the virtualpath and checks if the requested partial view exists.
        /// </summary>
        /// <param name="controllerContext"></param>
        /// <param name="partialPath"></param>
        /// <returns></returns>
        protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
        {
            if (!partialPath.Contains(FeaturePlaceholder) || controllerContext.Controller == null)
            {
                return base.CreatePartialView(controllerContext, partialPath);
            }

            var fullFeaturePath = GetFeaturePath(controllerContext.Controller);
            if (fullFeaturePath == null) return base.CreatePartialView(controllerContext, partialPath);
            partialPath = ReplaceFeaturePlaceholder(partialPath, fullFeaturePath);
            return base.CreatePartialView(controllerContext, partialPath);
        }

        /// <summary>
        /// Replaces the %FEATURE% placeholder in the virtualpath and checks if the requested file exists.
        /// </summary>
        /// <param name="controllerContext"></param>
        /// <param name="virtualPath"></param>
        /// <returns></returns>
        protected override bool FileExists(ControllerContext controllerContext, string virtualPath)
        {
            if (!virtualPath.Contains(FeaturePlaceholder) || controllerContext.Controller == null)
            {
                return base.FileExists(controllerContext, virtualPath);
            }

            var fullFeaturePath = GetFeaturePath(controllerContext.Controller);
            if (fullFeaturePath == null) return base.FileExists(controllerContext, virtualPath);
            virtualPath = ReplaceFeaturePlaceholder(virtualPath, fullFeaturePath);
            return base.FileExists(controllerContext, virtualPath);
        }

        /// <summary>
        /// Takes the namespace from the controller
        /// Splits it, removes the first and second item from the namespace(Project name and Features folder.)
        /// Joins together the remaining pieces as a Path to the feature.
        /// </summary>
        /// <param name="controller"></param>
        /// <returns></returns>
        private string GetFeaturePath(ControllerBase controller)
        {
            var controllerNamespace = controller.GetType().Namespace;

            if (string.IsNullOrEmpty(controllerNamespace)) return null;
            var sections = controllerNamespace.Split('.').ToList();
            if (sections.Count < 3) return null;

            sections.RemoveAt(0); //Removes the project name.
            sections.RemoveAt(0); //Removes "Features";

            var fullFeaturePath = string.Join("/", sections);
            return fullFeaturePath;
        }

        /// <summary>
        /// Replaces the %FEATURE% placeholder with the actual path.
        /// </summary>
        /// <param name="virtualPath"></param>
        /// <param name="featurePath"></param>
        /// <returns></returns>
        private string ReplaceFeaturePlaceholder(string virtualPath, string featurePath)
        {
            return virtualPath.Replace(FeaturePlaceholder, featurePath);
        }
    }
}