Skip to content

Commit

Permalink
Refactor MVC View Engine (#375)
Browse files Browse the repository at this point in the history
Fixes #361
  • Loading branch information
sebastienros authored Oct 10, 2021
1 parent 94569fc commit aa9e948
Show file tree
Hide file tree
Showing 14 changed files with 109 additions and 191 deletions.
6 changes: 2 additions & 4 deletions Fluid.MvcSample/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using Fluid.MvcSample.Models;
using Fluid.MvcViewEngine;
using Fluid.ViewEngine;
using Fluid.MvcViewEngine;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
Expand All @@ -14,7 +12,7 @@ public class Startup
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.Configure<FluidViewEngineOptions>(options =>
services.Configure<FluidMvcViewOptions>(options =>
{
options.Parser = new CustomFluidViewParser();
options.TemplateOptions.MemberAccessStrategy = UnsafeMemberAccessStrategy.Instance;
Expand Down
23 changes: 23 additions & 0 deletions Fluid.MvcViewEngine/FluidMvcViewOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Fluid.ViewEngine;
using System.Collections.Generic;

namespace Fluid.MvcViewEngine
{
public class FluidMvcViewOptions : FluidViewEngineOptions
{
/// <summary>
/// Gets les list of view location formats.
/// </summary>
/// <remarks>
/// The first argument '{0}' is the view name.
/// The second argument '{1}' is the controller name.
/// The third argument '{2}' is the area name.
/// </remarks>
/// <example>
/// "Views/{1}/{0}"
/// "Views/Shared/{0}"
/// </example>
public IList<string> ViewLocationFormats { get; } = new List<string>();

}
}
129 changes: 12 additions & 117 deletions Fluid.MvcViewEngine/FluidRendering.cs
Original file line number Diff line number Diff line change
@@ -1,152 +1,47 @@
using Fluid.Ast;
using Fluid.Parser;
using Fluid.ViewEngine;
using Fluid.ViewEngine;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;

namespace Fluid.MvcViewEngine
{
/// <summary>
/// This class is registered as a singleton. As such it can store application wide
/// state.
/// This class is registered as a singleton.
/// </summary>
public class FluidRendering : IFluidRendering
public class FluidRendering
{
static FluidRendering()
{
}
private readonly FluidViewRenderer _fluidViewRenderer;

public FluidRendering(
IMemoryCache memoryCache,
IOptions<FluidViewEngineOptions> optionsAccessor,
IOptions<FluidMvcViewOptions> optionsAccessor,
IWebHostEnvironment hostingEnvironment)
{
_memoryCache = memoryCache;
_hostingEnvironment = hostingEnvironment;
_options = optionsAccessor.Value;

_options.TemplateOptions.MemberAccessStrategy.Register<ViewDataDictionary>();
_options.TemplateOptions.MemberAccessStrategy.Register<ModelStateDictionary>();
_options.TemplateOptions.FileProvider = new FileProviderMapper(_options.IncludesFileProvider ?? _hostingEnvironment.ContentRootFileProvider, "Views");
_options.TemplateOptions.FileProvider = new FileProviderMapper(_options.IncludesFileProvider ?? _hostingEnvironment.ContentRootFileProvider, _options.ViewsPath);

_fluidViewRenderer = new FluidViewRenderer(_options);

_options.ViewsFileProvider ??= _hostingEnvironment.ContentRootFileProvider;
}

private readonly IMemoryCache _memoryCache;
private readonly IWebHostEnvironment _hostingEnvironment;
private readonly FluidViewEngineOptions _options;

public async ValueTask<string> RenderAsync(string path, object model, ViewDataDictionary viewData, ModelStateDictionary modelState)
public async Task RenderAsync(TextWriter writer, string path, object model, ViewDataDictionary viewData, ModelStateDictionary modelState)
{
var context = new TemplateContext(_options.TemplateOptions);
context.SetValue("ViewData", viewData);
context.SetValue("ModelState", modelState);
context.SetValue("Model", model);

// Provide some services to all statements
context.AmbientValues[Constants.ViewPathIndex] = path;
context.AmbientValues[Constants.SectionsIndex] = new Dictionary<string, IReadOnlyList<Statement>>();

var template = ParseLiquidFile(path, _options.ViewsFileProvider ?? _hostingEnvironment.ContentRootFileProvider, true);

var body = await template.RenderAsync(context, _options.TextEncoder);

// If a layout is specified while rendering a view, execute it
if (context.AmbientValues.TryGetValue(Constants.LayoutIndex, out var layoutPath))
{
context.AmbientValues[Constants.ViewPathIndex] = layoutPath;
context.AmbientValues[Constants.BodyIndex] = body;
var layoutTemplate = ParseLiquidFile((string)layoutPath, _options.ViewsFileProvider ?? _hostingEnvironment.ContentRootFileProvider, false);

return await layoutTemplate.RenderAsync(context, _options.TextEncoder);
}

return body;
}

public List<string> FindViewStarts(string viewPath, IFileProvider fileProvider)
{
var viewStarts = new List<string>();
int index = viewPath.Length - 1;
while (!String.IsNullOrEmpty(viewPath) &&
!(String.Equals(viewPath, "Views", StringComparison.OrdinalIgnoreCase)))
{
index = viewPath.LastIndexOf('/', index);

if (index == -1)
{
return viewStarts;
}

viewPath = viewPath.Substring(0, index + 1) + Constants.ViewStartFilename;

var viewStartInfo = fileProvider.GetFileInfo(viewPath);
if (viewStartInfo.Exists)
{
viewStarts.Add(viewPath);
}

index = index - 1;
}

return viewStarts;
}

public IFluidTemplate ParseLiquidFile(string path, IFileProvider fileProvider, bool includeViewStarts)
{
return _memoryCache.GetOrCreate(path, viewEntry =>
{
var subTemplates = new List<IFluidTemplate>();

// Default sliding expiration to prevent the entries for being kept indefinitely
viewEntry.SlidingExpiration = TimeSpan.FromHours(1);

var fileInfo = fileProvider.GetFileInfo(path);
viewEntry.ExpirationTokens.Add(fileProvider.Watch(path));

if (includeViewStarts)
{
// Add ViewStart files
foreach (var viewStartPath in FindViewStarts(path, fileProvider))
{
// Redefine the current view path while processing ViewStart files
var callbackTemplate = new FluidTemplate(new CallbackStatement((writer, encoder, context) =>
{
context.AmbientValues[Constants.ViewPathIndex] = viewStartPath;
return new ValueTask<Completion>(Completion.Normal);
}));

var viewStartTemplate = ParseLiquidFile(viewStartPath, fileProvider, false);

subTemplates.Add(callbackTemplate);
subTemplates.Add(viewStartTemplate);
}
}

using (var stream = fileInfo.CreateReadStream())
{
using (var sr = new StreamReader(stream))
{
var fileContent = sr.ReadToEnd();
if (_options.Parser.TryParse(fileContent, out var template, out var errors))
{
subTemplates.Add(template);

return new CompositeFluidTemplate(subTemplates);
}
else
{
throw new ParseException(errors);
}
}
}
});
await _fluidViewRenderer.RenderViewAsync(writer, path, context);
}
}
}
30 changes: 18 additions & 12 deletions Fluid.MvcViewEngine/FluidTagHelper.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
using System.Threading.Tasks;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace Fluid.MvcViewEngine
{
[HtmlTargetElement("fluid")]
public class FluidTagHelper : TagHelper
{
public IFluidRendering _fluidRendering { get; set; }
private FluidRendering _fluidRendering { get; set; }

public FluidTagHelper(IFluidRendering fluidRendering)
public FluidTagHelper(FluidRendering fluidRendering)
{
_fluidRendering = fluidRendering;
}
Expand All @@ -21,21 +22,26 @@ public FluidTagHelper(IFluidRendering fluidRendering)

public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
static async Task Awaited(TagHelperOutput o, ValueTask<string> t)
static async Task Awaited(TagHelperOutput o, StringWriter sw, Task t)
{
await t;
o.TagName = null;
o.Content.AppendHtml(await t);
o.Content.AppendHtml(sw.ToString());
}

var task = _fluidRendering.RenderAsync(View, Model, null, null);
if (task.IsCompletedSuccessfully)
using (var sw = new StringWriter())
{
output.TagName = null;
output.Content.AppendHtml(task.Result);
return Task.FromResult(output);
}
var task = _fluidRendering.RenderAsync(sw, View, Model, null, null);

if (task.IsCompletedSuccessfully)
{
output.TagName = null;
output.Content.AppendHtml(sw.ToString());
return Task.FromResult(output);
}

return Awaited(output, task);
return Awaited(output, sw, task);
}
}
}
}
7 changes: 3 additions & 4 deletions Fluid.MvcViewEngine/FluidView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ namespace Fluid.MvcViewEngine
public class FluidView : IView
{
private string _path;
private IFluidRendering _fluidRendering;
private FluidRendering _fluidRendering;

public FluidView(string path, IFluidRendering fluidRendering)
public FluidView(string path, FluidRendering fluidRendering)
{
_path = path;
_fluidRendering = fluidRendering;
Expand All @@ -25,8 +25,7 @@ public string Path

public async Task RenderAsync(ViewContext context)
{
var result = await _fluidRendering.RenderAsync(Path, context.ViewData.Model, context.ViewData, context.ModelState);
context.Writer.Write(result);
await _fluidRendering.RenderAsync(context.Writer, Path, context.ViewData.Model, context.ViewData, context.ModelState);
}
}
}
26 changes: 14 additions & 12 deletions Fluid.MvcViewEngine/FluidViewEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Fluid.ViewEngine;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewEngines;
Expand All @@ -12,15 +11,15 @@ namespace Fluid.MvcViewEngine
{
public class FluidViewEngine : IFluidViewEngine
{
private IFluidRendering _fluidRendering;
private FluidRendering _fluidRendering;
private readonly IWebHostEnvironment _hostingEnvironment;
public static readonly string ViewExtension = ".liquid";
private const string ControllerKey = "controller";
private const string AreaKey = "area";
private FluidViewEngineOptions _options;
private FluidMvcViewOptions _options;

public FluidViewEngine(IFluidRendering fluidRendering,
IOptions<FluidViewEngineOptions> optionsAccessor,
public FluidViewEngine(FluidRendering fluidRendering,
IOptions<FluidMvcViewOptions> optionsAccessor,
IWebHostEnvironment hostingEnvironment)
{
_options = optionsAccessor.Value;
Expand All @@ -30,27 +29,30 @@ public FluidViewEngine(IFluidRendering fluidRendering,

public ViewEngineResult FindView(ActionContext context, string viewName, bool isMainPage)
{
return LocatePageFromViewLocations(context, viewName, isMainPage);
return LocatePageFromViewLocations(context, viewName);
}

private ViewEngineResult LocatePageFromViewLocations(
ActionContext actionContext,
string viewName,
bool isMainPage)
private ViewEngineResult LocatePageFromViewLocations(ActionContext actionContext, string viewName)
{
var controllerName = GetNormalizedRouteValue(actionContext, ControllerKey);
var areaName = GetNormalizedRouteValue(actionContext, AreaKey);

var fileProvider = _options.ViewsFileProvider ?? _hostingEnvironment.ContentRootFileProvider;

var checkedLocations = new List<string>();

foreach (var location in _options.ViewLocationFormats)
{
var view = string.Format(location, viewName, controllerName);
if(fileProvider.GetFileInfo(view).Exists)
var view = string.Format(location, viewName, controllerName, areaName);

if (fileProvider.GetFileInfo(view).Exists)
{
return ViewEngineResult.Found("Default", new FluidView(view, _fluidRendering));
}

checkedLocations.Add(view);
}

return ViewEngineResult.NotFound(viewName, checkedLocations);
}

Expand Down
8 changes: 5 additions & 3 deletions Fluid.MvcViewEngine/FluidViewEngineOptionsSetup.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using Fluid.ViewEngine;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Options;

namespace Fluid.MvcViewEngine
{
public class FluidViewEngineOptionsSetup : ConfigureOptions<FluidViewEngineOptions>
/// <summary>
/// Defines the default configuration of <see cref="FluidMvcViewOptions"/>.
/// </summary>
internal class FluidViewEngineOptionsSetup : ConfigureOptions<FluidMvcViewOptions>
{
public FluidViewEngineOptionsSetup(IWebHostEnvironment webHostEnvironment)
: base(options =>
Expand Down
11 changes: 0 additions & 11 deletions Fluid.MvcViewEngine/IFluidRendering.cs

This file was deleted.

6 changes: 3 additions & 3 deletions Fluid.MvcViewEngine/MvcViewFeaturesMvcBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ public static IMvcBuilder AddFluid(this IMvcBuilder builder, Action<FluidViewEng
}

builder.Services.AddOptions();
builder.Services.AddTransient<IConfigureOptions<FluidViewEngineOptions>, FluidViewEngineOptionsSetup>();
builder.Services.AddTransient<IConfigureOptions<FluidMvcViewOptions>, FluidViewEngineOptionsSetup>();

if (setupAction != null)
{
builder.Services.Configure(setupAction);
}

builder.Services.AddTransient<IConfigureOptions<MvcViewOptions>, FluidMvcViewOptionsSetup>();
builder.Services.AddSingleton<IFluidRendering, FluidRendering>();
builder.Services.AddTransient<IConfigureOptions<MvcViewOptions>, MvcViewOptionsSetup>();
builder.Services.AddSingleton<FluidRendering>();
builder.Services.AddSingleton<IFluidViewEngine, FluidViewEngine>();
return builder;

Expand Down
Loading

0 comments on commit aa9e948

Please sign in to comment.