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>