Simplifies how view locations are determined

AB#9720
This commit is contained in:
Shannon
2021-01-04 15:43:30 +11:00
parent a6f281789b
commit a126977a4e
10 changed files with 120 additions and 228 deletions

View File

@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Core.DependencyInjection;
@@ -27,12 +28,9 @@ namespace Umbraco.Web.Website.DependencyInjection
builder.WithCollectionBuilder<SurfaceControllerTypeCollectionBuilder>()
.Add(builder.TypeLoader.GetSurfaceControllers());
// Set the render & plugin view engines (Super complicated, but this allows us to use the IServiceCollection
// to inject dependencies into the viewEngines)
builder.Services.AddTransient<IConfigureOptions<MvcViewOptions>, RenderMvcViewOptionsSetup>();
builder.Services.AddSingleton<IRenderViewEngine, RenderViewEngine>();
builder.Services.AddTransient<IConfigureOptions<MvcViewOptions>, PluginMvcViewOptionsSetup>();
builder.Services.AddSingleton<IPluginViewEngine, PluginViewEngine>();
// Configure MVC startup options for custom view locations
builder.Services.AddTransient<IConfigureOptions<RazorViewEngineOptions>, RenderRazorViewEngineOptionsSetup>();
builder.Services.AddTransient<IConfigureOptions<RazorViewEngineOptions>, PluginRazorViewEngineOptionsSetup>();
// Wraps all existing view engines in a ProfilerViewEngine
builder.Services.AddTransient<IConfigureOptions<MvcViewOptions>, ProfilingViewEngineWrapperMvcViewOptionsSetup>();

View File

@@ -1,8 +0,0 @@
using Microsoft.AspNetCore.Mvc.Razor;
namespace Umbraco.Web.Website.ViewEngines
{
public interface IPluginViewEngine : IRazorViewEngine
{
}
}

View File

@@ -1,8 +0,0 @@
using Microsoft.AspNetCore.Mvc.Razor;
namespace Umbraco.Web.Website.ViewEngines
{
public interface IRenderViewEngine : IRazorViewEngine
{
}
}

View File

@@ -1,26 +0,0 @@
using System;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace Umbraco.Web.Website.ViewEngines
{
public class PluginMvcViewOptionsSetup : IConfigureOptions<MvcViewOptions>
{
private readonly IPluginViewEngine _pluginViewEngine;
public PluginMvcViewOptionsSetup(IPluginViewEngine pluginViewEngine)
{
_pluginViewEngine = pluginViewEngine ?? throw new ArgumentNullException(nameof(pluginViewEngine));
}
public void Configure(MvcViewOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
options.ViewEngines.Add(_pluginViewEngine);
}
}
}

View File

@@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.Options;
namespace Umbraco.Web.Website.ViewEngines
{
/// <summary>
/// Configure view engine locations for front-end rendering based on App_Plugins views
/// </summary>
public class PluginRazorViewEngineOptionsSetup : IConfigureOptions<RazorViewEngineOptions>
{
/// <inheritdoc/>
public void Configure(RazorViewEngineOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
options.ViewLocationExpanders.Add(new ViewLocationExpander());
}
/// <summary>
/// Expands the default view locations
/// </summary>
private class ViewLocationExpander : IViewLocationExpander
{
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
string[] umbViewLocations = new string[]
{
// area view locations for the plugin folder
string.Concat(Core.Constants.SystemDirectories.AppPlugins, "/{2}/Views/{1}/{0}.cshtml"),
string.Concat(Core.Constants.SystemDirectories.AppPlugins, "/{2}/Views/Shared/{0}.cshtml"),
// will be used when we have partial view and child action macros
string.Concat(Core.Constants.SystemDirectories.AppPlugins, "/{2}/Views/Partials/{0}.cshtml"),
string.Concat(Core.Constants.SystemDirectories.AppPlugins, "/{2}/Views/MacroPartials/{0}.cshtml")
};
viewLocations = umbViewLocations.Concat(viewLocations);
return viewLocations;
}
// not a dynamic expander
public void PopulateValues(ViewLocationExpanderContext context) { }
}
}
}

View File

@@ -1,52 +0,0 @@
using System.Diagnostics;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Umbraco.Web.Website.ViewEngines
{
// TODO: We don't really need to have different view engines simply to search additional places,
// we can just do ConfigureOptions<RazorViewEngineOptions> on startup to add more to the
// default list so this can be totally removed/replaced with configure options logic.
/// <summary>
/// A view engine to look into the App_Plugins folder for views for packaged controllers
/// </summary>
public class PluginViewEngine : RazorViewEngine, IPluginViewEngine
{
public PluginViewEngine(
IRazorPageFactoryProvider pageFactory,
IRazorPageActivator pageActivator,
HtmlEncoder htmlEncoder,
ILoggerFactory loggerFactory,
DiagnosticListener diagnosticListener)
: base(pageFactory, pageActivator, htmlEncoder, OverrideViewLocations(), loggerFactory, diagnosticListener)
{
}
private static IOptions<RazorViewEngineOptions> OverrideViewLocations() => Options.Create(new RazorViewEngineOptions()
{
// This is definitely not doing what it used to do :P see:
// https://github.com/umbraco/Umbraco-CMS/blob/v8/contrib/src/Umbraco.Web/Mvc/PluginViewEngine.cs#L23
AreaViewLocationFormats =
{
// set all of the area view locations to the plugin folder
string.Concat(Core.Constants.SystemDirectories.AppPlugins, "/{2}/Views/{1}/{0}.cshtml"),
string.Concat(Core.Constants.SystemDirectories.AppPlugins, "/{2}/Views/Shared/{0}.cshtml"),
// will be used when we have partial view and child action macros
string.Concat(Core.Constants.SystemDirectories.AppPlugins, "/{2}/Views/Partials/{0}.cshtml"),
string.Concat(Core.Constants.SystemDirectories.AppPlugins, "/{2}/Views/MacroPartials/{0}.cshtml"),
// for partialsCurrent.
string.Concat(Core.Constants.SystemDirectories.AppPlugins, "/{2}/Views/{1}/{0}.cshtml"),
string.Concat(Core.Constants.SystemDirectories.AppPlugins, "/{2}/Views/Shared/{0}.cshtml"),
},
ViewLocationFormats =
{
string.Concat(Core.Constants.SystemDirectories.AppPlugins, "/{2}/Views/{1}/{0}.cshtml"),
}
});
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
@@ -8,15 +8,20 @@ using Umbraco.Core.Logging;
namespace Umbraco.Web.Website.ViewEngines
{
/// <summary>
/// Wraps all view engines with a <see cref="ProfilingViewEngine"/>
/// </summary>
public class ProfilingViewEngineWrapperMvcViewOptionsSetup : IConfigureOptions<MvcViewOptions>
{
private readonly IProfiler _profiler;
public ProfilingViewEngineWrapperMvcViewOptionsSetup(IProfiler profiler)
{
_profiler = profiler ?? throw new ArgumentNullException(nameof(profiler));
}
/// <summary>
/// Initializes a new instance of the <see cref="ProfilingViewEngineWrapperMvcViewOptionsSetup"/> class.
/// </summary>
/// <param name="profiler">The <see cref="IProfiler"/></param>
public ProfilingViewEngineWrapperMvcViewOptionsSetup(IProfiler profiler) => _profiler = profiler ?? throw new ArgumentNullException(nameof(profiler));
/// <inheritdoc/>
public void Configure(MvcViewOptions options)
{
if (options == null)
@@ -29,13 +34,16 @@ namespace Umbraco.Web.Website.ViewEngines
private void WrapViewEngines(IList<IViewEngine> viewEngines)
{
if (viewEngines == null || viewEngines.Count == 0) return;
if (viewEngines == null || viewEngines.Count == 0)
{
return;
}
var originalEngines = viewEngines.ToList();
viewEngines.Clear();
foreach (var engine in originalEngines)
foreach (IViewEngine engine in originalEngines)
{
var wrappedEngine = engine is ProfilingViewEngine ? engine : new ProfilingViewEngine(engine, _profiler);
IViewEngine wrappedEngine = engine is ProfilingViewEngine ? engine : new ProfilingViewEngine(engine, _profiler);
viewEngines.Add(wrappedEngine);
}
}

View File

@@ -1,26 +0,0 @@
using System;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace Umbraco.Web.Website.ViewEngines
{
public class RenderMvcViewOptionsSetup : IConfigureOptions<MvcViewOptions>
{
private readonly IRenderViewEngine _renderViewEngine;
public RenderMvcViewOptionsSetup(IRenderViewEngine renderViewEngine)
{
_renderViewEngine = renderViewEngine ?? throw new ArgumentNullException(nameof(renderViewEngine));
}
public void Configure(MvcViewOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
options.ViewEngines.Add(_renderViewEngine);
}
}
}

View File

@@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.Options;
namespace Umbraco.Web.Website.ViewEngines
{
/// <summary>
/// Configure view engine locations for front-end rendering
/// </summary>
public class RenderRazorViewEngineOptionsSetup : IConfigureOptions<RazorViewEngineOptions>
{
/// <inheritdoc/>
public void Configure(RazorViewEngineOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
options.ViewLocationExpanders.Add(new ViewLocationExpander());
}
/// <summary>
/// Expands the default view locations
/// </summary>
private class ViewLocationExpander : IViewLocationExpander
{
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
string[] umbViewLocations = new string[]
{
"/Views/Partials/{0}.cshtml",
"/Views/MacroPartials/{0}.cshtml",
"/Views/{0}.cshtml"
};
viewLocations = umbViewLocations.Concat(viewLocations);
return viewLocations;
}
// not a dynamic expander
public void PopulateValues(ViewLocationExpanderContext context) { }
}
}
}

View File

@@ -1,94 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Core.Hosting;
using Umbraco.Extensions;
using Umbraco.Web.Models;
namespace Umbraco.Web.Website.ViewEngines
{
// TODO: We don't really need to have different view engines simply to search additional places,
// we can just do ConfigureOptions<RazorViewEngineOptions> on startup to add more to the
// default list so this can be totally removed/replaced with configure options logic.
/// <summary>
/// A view engine to look into the template location specified in the config for the front-end/Rendering part of the cms,
/// this includes paths to render partial macros and media item templates.
/// </summary>
public class RenderViewEngine : RazorViewEngine, IRenderViewEngine
{
/// <summary>
/// Initializes a new instance of the <see cref="RenderViewEngine"/> class.
/// </summary>
public RenderViewEngine(
IRazorPageFactoryProvider pageFactory,
IRazorPageActivator pageActivator,
HtmlEncoder htmlEncoder,
ILoggerFactory loggerFactory,
DiagnosticListener diagnosticListener)
: base(pageFactory, pageActivator, htmlEncoder, OverrideViewLocations(), loggerFactory, diagnosticListener)
{
}
private static IOptions<RazorViewEngineOptions> OverrideViewLocations() => Options.Create(new RazorViewEngineOptions()
{
// NOTE: we will make the main view location the last to be searched since if it is the first to be searched and there is both a view and a partial
// view in both locations and the main view is rendering a partial view with the same name, we will get a stack overflow exception.
// http://issues.umbraco.org/issue/U4-1287, http://issues.umbraco.org/issue/U4-1215
ViewLocationFormats =
{
"/Views/Partials/{0}.cshtml",
"/Views/MacroPartials/{0}.cshtml",
"/Views/{0}.cshtml"
},
AreaViewLocationFormats =
{
"/Views/Partials/{0}.cshtml",
"/Views/MacroPartials/{0}.cshtml",
"/Views/{0}.cshtml"
}
});
public new ViewEngineResult FindView(ActionContext context, string viewName, bool isMainPage)
{
return ShouldFindView(context, isMainPage)
? base.FindView(context, viewName, isMainPage)
: ViewEngineResult.NotFound(viewName, Array.Empty<string>());
}
/// <summary>
/// Determines if the view should be found, this is used for view lookup performance and also to ensure
/// less overlap with other user's view engines. This will return true if the Umbraco back office is rendering
/// and its a partial view or if the umbraco front-end is rendering but nothing else.
/// </summary>
/// <param name="controllerContext"></param>
/// <param name="isPartial"></param>
/// <returns></returns>
private static bool ShouldFindView(ActionContext context, bool isMainPage)
{
return true;
// TODO: Determine if this is required, i don't think it is
////In v8, this was testing recursively into if it was a child action, but child action do not exist anymore,
////And my best guess is that it
//context.RouteData.DataTokens.TryGetValue(Core.Constants.Web.UmbracoDataToken, out var umbracoToken);
//// first check if we're rendering a partial view for the back office, or surface controller, etc...
//// anything that is not ContentModel as this should only pertain to Umbraco views.
//if (!isMainPage && !(umbracoToken is ContentModel))
// return true;
//// only find views if we're rendering the umbraco front end
//return umbracoToken is ContentModel;
}
}
}