0

Has anyone had any success loading _Layout.cshtml from a physical path before?

Problem: At present, MVC throws an error if you attempt to pass something like C:\dedicated\path\to\layouts\_Layout.cshtml to the Layout property in _ViewStart.cshtml due to the executing page hierarchy ("virtual path expected").

Been doing a bit of research and experimentation with this already and have a couple different "hacks" available as workarounds including deriving from RazorView through replacing ExecutePageHierarchy altogether with RunCompile from the RazorEngine NuGet package (probably best bet).

Curious if anyone has come up with a concrete solution for this.

Thanks!

Matt Borja
  • 1,509
  • 1
  • 17
  • 38
  • You'd have to write your own custom view engine for that, which will search in the specific path. – penleychan Jan 16 '18 at 21:17
  • That's one of the workarounds I've looked into, hence wanting to "extend" RazorViewEngine. I really don't want to *replace* Razor. – Matt Borja Jan 16 '18 at 21:30
  • Well, you wouldn't be replacing the default RazorViewEngine, you can have multiple view engines. – penleychan Jan 16 '18 at 21:45
  • Possible duplicate of [Can I specify a custom location to "search for views" in ASP.NET MVC?](https://stackoverflow.com/questions/632964/can-i-specify-a-custom-location-to-search-for-views-in-asp-net-mvc) – NightOwl888 Jan 16 '18 at 21:54
  • That's not quite the same thing. Here I want to continue using Razor for rendering but add support for rendering it in a layout loaded from a physical location (currently unsupported). At a bare minimum, I would have to replicate `WebPageBase.ExecutePageHierarchy` and everything it does just to get at `PopContext` calling `NormalizeLayoutPagePath` and further down the stack until `System.Web.Util.UrlPath.CheckValidVirtualPath` is called and finally throws the error. Still working out this direction though as it may be the closest workaround. – Matt Borja Jan 16 '18 at 21:57
  • @NightOwl888 My reply was to 12seconds but regardless, if you try passing a *physical path* you'll get an exception per my reply. – Matt Borja Jan 16 '18 at 21:58

1 Answers1

0

Well, this is an initial implementation and only supports static HTML files (no Razor rendering), but it works.

using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Web.Mvc;
using System.Web.WebPages;

namespace Enterprise.InHouse.Shared.Web
{
    /// <summary>
    /// Primitive HTML template wrapper for RazorView where physical layout paths are unsupported.
    /// </summary>
    /// <remarks>
    /// Derived from https://github.com/mono/aspnetwebstack/blob/master/src/System.Web.Mvc/RazorView.cs
    /// </remarks>
    public class PhysicalPathView : RazorView
    {
        #region Proof of Concept (TODO: Refactor)

        public static readonly string HeaderPath = ConfigurationManager.AppSettings["PhysicalPathView.HeaderPath"];
        public static readonly string FooterPath = ConfigurationManager.AppSettings["PhysicalPathView.FooterPath"];

        // TODO: Optimize
        private static void StaticTemplateWrap(Action body, TextWriter writer)
        {
            writer.Write(File.ReadAllText(HeaderPath));
            body();
            writer.Write(File.ReadAllText(FooterPath));
        }

        #endregion

        #region RazorView

        public PhysicalPathView(ControllerContext controllerContext, string viewPath, string layoutPath, bool runViewStartPages, IEnumerable<string> viewStartFileExtensions)
            : this(controllerContext, viewPath, layoutPath, runViewStartPages, viewStartFileExtensions, null)
        {
        }

        public PhysicalPathView(ControllerContext controllerContext, string viewPath, string layoutPath, bool runViewStartPages, IEnumerable<string> viewStartFileExtensions, IViewPageActivator viewPageActivator)
            : base(controllerContext, viewPath, layoutPath, runViewStartPages, viewStartFileExtensions, viewPageActivator)
        {
            //LayoutPath = layoutPath ?? string.Empty; // ! Read-only
            //RunViewStartPages = runViewStartPages; // ! Read-only
            //StartPageLookup = StartPage.GetStartPage; // ! Non-existent
            //ViewStartFileExtensions = viewStartFileExtensions ?? Enumerable.Empty<string>(); // ! Read-only
        }

        protected override void RenderView(ViewContext viewContext, TextWriter writer, object instance)
        {
            if (writer == null)
            {
                throw new ArgumentNullException("writer");
            }

            WebViewPage webViewPage = instance as WebViewPage;
            if (webViewPage == null)
            {
                throw new InvalidOperationException(string.Format("Wrong view base: {0}", ViewPath));
                    //String.Format(
                    //    CultureInfo.CurrentCulture,
                    //    MvcResources.CshtmlView_WrongViewBase, // ! Non-existent
                    //    ViewPath));
            }

            // An overriden master layout might have been specified when the ViewActionResult got returned.
            // We need to hold on to it so that we can set it on the inner page once it has executed.
            //webViewPage.OverridenLayoutPath = LayoutPath; // ! Non-existent
            webViewPage.VirtualPath = ViewPath;
            webViewPage.ViewContext = viewContext;
            webViewPage.ViewData = viewContext.ViewData;

            webViewPage.InitHelpers();

            // ! Non-existent
            //if (VirtualPathFactory != null)
            //{
            //    webViewPage.VirtualPathFactory = VirtualPathFactory;
            //}

            // ! Non-existent
            //if (DisplayModeProvider != null)
            //{
            //    webViewPage.DisplayModeProvider = DisplayModeProvider;
            //}

            WebPageRenderingBase startPage = null;

            // ! Non-existent
            //if (RunViewStartPages)
            //{
            //    startPage = StartPageLookup(webViewPage, RazorViewEngine.ViewStartFileName, ViewStartFileExtensions);
            //}

            StaticTemplateWrap(() =>
            {
                webViewPage.ExecutePageHierarchy(new WebPageContext(context: viewContext.HttpContext, page: null, model: null), writer, startPage);
            }, writer);
        }

        #endregion
    }
}

Derive a new view engine class from RazorViewEngine, override its CreateView to return an instance of this class, and register in Global.asax.

I'll update this answer with a more complete solution as it becomes available.


Update (1/17/18) - The above implementation has been cleaned up and is provided below. It still only renders static HTML, which is fine for now, but also exposes a custom RenderViewWrapper helper in the LayoutWrapperViewEngine class which you can implement/refactor any way you want.

LayoutWrapperView.cs

using System;
using System.Collections.Generic;
using System.IO;
using System.Web.Mvc;

namespace Enterprise.Apps.Shared.Web
{
    public class LayoutWrapperView : RazorView
    {
        /// <summary>
        /// Outer lambda for caller to wrap inner lambda.
        /// </summary>
        private readonly Action<Action, TextWriter> RenderViewWrapper;

        public LayoutWrapperView(Action<Action, TextWriter> renderViewWrapper, ControllerContext controllerContext, string viewPath, string layoutPath, bool runViewStartPages, IEnumerable<string> viewStartFileExtensions)
            : this(renderViewWrapper, controllerContext, viewPath, layoutPath, runViewStartPages, viewStartFileExtensions, null) { }

        public LayoutWrapperView(Action<Action, TextWriter> renderViewWrapper, ControllerContext controllerContext, string viewPath, string layoutPath, bool runViewStartPages, IEnumerable<string> viewStartFileExtensions, IViewPageActivator viewPageActivator)
            : base(controllerContext, viewPath, layoutPath, runViewStartPages, viewStartFileExtensions, viewPageActivator)
        {
            RenderViewWrapper = renderViewWrapper;
        }

        protected override void RenderView(ViewContext viewContext, TextWriter writer, object instance)
        {
            if (RenderViewWrapper != null)
            {
                RenderViewWrapper(() =>
                {
                    base.RenderView(viewContext, writer, instance);
                }, writer);
            } else
            {
                base.RenderView(viewContext, writer, instance);
            }
        }
    }
}

LayoutWrapperViewEngine.cs

using System;
using System.Configuration;
using System.IO;
using System.Web.Mvc;

namespace Enterprise.Apps.Shared.Web
{
    /// <summary>
    /// Custom ViewEngine extension for RazorViewEngine designed to support physical paths (i.e. C:\Inetpub\shared\layouts -> \\server\share\layouts)
    /// </summary>
    /// <remarks>
    /// In .NET 4.5.2, physical paths are not supported in _ViewStart.cshtml due to Server.MapPath validation (only virtual paths allowed).
    /// This class provides an alternate ViewEngine to support shared layouts via physical paths as in the following:
    /// 
    /// net use \\server\share\layouts
    /// mklink /d C:\Inetpub\shared\layouts \\server\share\layouts
    /// </remarks>
    public class LayoutWrapperViewEngine : RazorViewEngine
    {
        public static readonly string HeaderPath = ConfigurationManager.AppSettings["LayoutView.HeaderPath"];
        public static readonly string FooterPath = ConfigurationManager.AppSettings["LayoutView.FooterPath"];

        protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
        {
            return new LayoutWrapperView(RenderViewWrapper, controllerContext, viewPath,
                layoutPath: masterPath, runViewStartPages: true, viewStartFileExtensions: FileExtensions, viewPageActivator: ViewPageActivator);
        }

        /// <summary>
        /// Writes static header/footer to response buffer.
        /// </summary>
        /// <param name="body"></param>
        /// <param name="responseBuffer"></param>
        private static void RenderViewWrapper(Action body, TextWriter responseBuffer)
        {
            try
            {
                responseBuffer.Write(File.ReadAllText(HeaderPath));
            } catch (Exception)
            {
                // TODO: Implement exception handling
            }

            body();

            try
            {
                responseBuffer.Write(File.ReadAllText(FooterPath));
            }
            catch (Exception)
            {
                // TODO: Implement exception handling
            }
        }
    }
}

Global.asax.cs

using System.Web.Mvc;
using System.Web.Routing;
using Enterprise.Apps.Shared.Web;

namespace Enterprise.Apps.Web
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);

            // Use only LayoutWrapperViewEngine
            ViewEngines.Engines.Clear();
            ViewEngines.Engines.Add(new LayoutWrapperViewEngine());

            RouteConfig.RegisterRoutes(RouteTable.Routes);
        }
    }
}

Web.config

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <appSettings>
    <add key="LayoutView.HeaderPath" value="C:\Inetpub\shared\layouts\header.html" />
    <add key="LayoutView.FooterPath" value="C:\Inetpub\shared\layouts\footer.html" />
  </appSettings>
</configuration>
Matt Borja
  • 1,509
  • 1
  • 17
  • 38