Sharing views between specific controllers

There’s no native way in MVC to share views between controllers without having to put those views in the ‘Shared’ folder which can be annoying especially if you have an inherited controller class structure, or your simply wish to just have one particular controller look for views in the folder of a different controller.

With a little help from DotPeek to see what’s going on inside of the VirtualPathProviderViewEngine, I’ve created a fairly elegant solution to the above problem which consists of a custom ViewEngine and a custom controller Attribute. Lets start with the custom Attribute, its really simple and only contains one property which is the name of the controller that you wish the attributed controller to reference views.  An example:

[AlternateViewEnginePath("ContentEditor")]
public class MediaEditorController : Controller
{  ...  }

The example above is saying that we have a MediaEditor controller that should reference views in the ContentEditor controller’s view folder, pretty simple right! A cool part about how the underlying ViewEngine works is that if a view isn’t found in the ContentEditor controller’s folder, then it will check back to the current controller’s folder, so it has built in fall back support.

The custom ViewEngine is actually pretty simple as well once you know what’s going on inside of the VirtualPathProviderViewEngine. There’s two particular methods: FindView and FindPartialView which the VirtualPathProviderViewEngine figures out which controller/folder to look in. It figures this out by looking at the current ControllerContext’s RouteValues and simply does a lookup of the controller name by using this statement:

controllerContext.RouteData.GetRequiredString("controller");

So all we’ve got to do is override the FindView and FindPartialView methods, create a custom ControllerContext with the “controller” route value to be the value specified in our custom attribute, and then pass this custom ControllerContext into the underlying base FindView/FindPartial view methods:

/// <summary>
/// A ViewEngine that allows a controller's views to be shared with other 
/// controllers without having to put these shared views in the 'Shared' folder.
/// This is useful for when you have inherited controllers.
/// </summary>
public class AlternateLocationViewEngine : RazorViewEngine
{
    public override ViewEngineResult FindPartialView(
        ControllerContext controllerContext, 
        string partialViewName, 
        bool useCache)
    {
        var altContext = GetAlternateControllerContext(controllerContext);
        if (altContext != null)
        {
            //see if we can get the view with the alternate controller 
            //specified, if its found return the result, if its not found
            //then return the normal results which will try to find 
            //the view based on the 'real' controllers name.
            var result = base.FindPartialView(altContext, partialViewName,useCache);
            if (result.View != null)
            {
                return result;
            }
        }
        return base.FindPartialView(controllerContext, partialViewName,useCache);
    }

    public override ViewEngineResult FindView(
        ControllerContext controllerContext, 
        string viewName, 
        string masterName, 
        bool useCache)
    {
        var altContext = GetAlternateControllerContext(controllerContext);
        if (altContext!= null)
        {
            var result = base.FindView(altContext, viewName, masterName, useCache);
            if (result.View != null)
            {
                return result;
            }
        }
        return base.FindView(controllerContext, viewName, masterName, useCache);
    }

    /// <summary>
    /// Returns a new controller context with the alternate controller name in the route values
    /// if the current controller is found to contain an AlternateViewEnginePathAttribute.
    /// </summary>
    /// <param name="currentContext"></param>
    /// <returns></returns>
    private static ControllerContext GetAlternateControllerContext(
        ControllerContext currentContext)
    {
        var controller = currentContext.Controller;
        var altControllerAttribute = controller.GetType()
            .GetCustomAttributes(typeof(AlternateViewEnginePathAttribute), false)
            .OfType<AlternateViewEnginePathAttribute>()
            .ToList();
        if (altControllerAttribute.Any())
        {
            var altController = altControllerAttribute.Single().AlternateControllerName;
            //we're basically cloning the original route data here...
            var newRouteData = new RouteData
                {
                    Route = currentContext.RouteData.Route,
                    RouteHandler = currentContext.RouteData.RouteHandler
                };
            currentContext.RouteData.DataTokens
                .ForEach(x => newRouteData.DataTokens.Add(x.Key, x.Value));
            currentContext.RouteData.Values
                .ForEach(x => newRouteData.Values.Add(x.Key, x.Value));

            //now, update the new route data with the new alternate controller name
            newRouteData.Values["controller"] = altController;

            //now create a new controller context to pass to the view engine
            var newContext = new ControllerContext(
                currentContext.HttpContext, 
                newRouteData, 
                currentContext.Controller);
            return newContext;
        }

        return null;
    }
}

/// <summary>
/// An attribute for a controller that specifies that the ViewEngine 
/// should look for views for this controller using a different controllers name.
/// This is useful if you want to share views between specific controllers 
/// but don't want to have to put all of the views into the Shared folder.
/// </summary>
public class AlternateViewEnginePathAttribute : Attribute
{
    public string AlternateControllerName { get; set; }

    public AlternateViewEnginePathAttribute(string altControllerName)
    {
        AlternateControllerName = altControllerName;
    }
}
Lastly, you’ll need to just register this additional ViewEngine in your global.asax, or IoC, or however you are doing that sort of thing in your application.

Author

Administrator (1)

comments powered by Disqus