347 lines
11 KiB
C#
347 lines
11 KiB
C#
using Microsoft.AspNetCore.Html;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
|
using Microsoft.AspNetCore.Mvc.Razor;
|
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
|
using Microsoft.AspNetCore.Mvc.ViewEngines;
|
|
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
|
using Microsoft.AspNetCore.Routing;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Options;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Text.Encodings.Web;
|
|
|
|
namespace Finch.Rendering;
|
|
|
|
public class RazorRenderer : IRazorRenderer, IDisposable
|
|
{
|
|
protected IRazorViewEngine ViewEngine { get; set; }
|
|
|
|
protected IRazorPageActivator PageActivator { get; set; }
|
|
|
|
protected ITempDataDictionaryFactory TempDataDictionaryFactory { get; set; }
|
|
|
|
protected IModelMetadataProvider ModelMetadataProvider { get; set; }
|
|
|
|
protected IHttpContextAccessor HttpContextAccessor { get; set; }
|
|
|
|
protected IServiceScope ServiceScope { get; set; }
|
|
|
|
protected HtmlHelperOptions HtmlHelperOptions { get; set; }
|
|
|
|
|
|
public RazorRenderer(IRazorViewEngine viewEngine, IRazorPageActivator pageActivator, IHttpContextAccessor httpContextAccessor, ITempDataDictionaryFactory tempDataDictionaryFactory,
|
|
IModelMetadataProvider modelMetadataProvider, IServiceProvider serviceProvider, IOptions<MvcViewOptions> mvcHelperOptions)
|
|
{
|
|
ViewEngine = viewEngine;
|
|
PageActivator = pageActivator;
|
|
HttpContextAccessor = httpContextAccessor;
|
|
TempDataDictionaryFactory = tempDataDictionaryFactory;
|
|
ModelMetadataProvider = modelMetadataProvider;
|
|
ServiceScope = serviceProvider.CreateScope();
|
|
HtmlHelperOptions = mvcHelperOptions.Value.HtmlHelperOptions;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Renders a razor component to a string
|
|
/// </summary>
|
|
public async Task<string> ComponentAsync<T>(object args = null) where T : ViewComponent
|
|
{
|
|
return await ComponentAsync(typeof(T), args);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Renders a razor component to a string
|
|
/// </summary>
|
|
public async Task<string> ComponentAsync(Type componentType, object args = null)
|
|
{
|
|
return await ComponentAsync(componentType, BuildActionContext(), args);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Renders a razor component to a string
|
|
/// </summary>
|
|
public async Task<string> ComponentAsync(string componentName, object args = null)
|
|
{
|
|
return await ComponentAsync(componentName, BuildActionContext(), args);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Renders a razor component to a string
|
|
/// </summary>
|
|
public async Task<string> ComponentAsync<T>(ActionContext context, object args = null) where T : ViewComponent
|
|
{
|
|
return await ComponentAsync(typeof(T), context, args);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Renders a razor component to a string
|
|
/// </summary>
|
|
public async Task<string> ComponentAsync(Type componentType, ActionContext context, object args = null)
|
|
{
|
|
IViewComponentHelper viewComponentHelper = context.HttpContext.RequestServices.GetRequiredService<IViewComponentHelper>();
|
|
|
|
using StringWriter stringWriter = new();
|
|
|
|
ViewContext viewContext = BuildViewContext(context, stringWriter);
|
|
|
|
(viewComponentHelper as IViewContextAware)?.Contextualize(viewContext);
|
|
IHtmlContent result = await viewComponentHelper.InvokeAsync(componentType, args);
|
|
|
|
result.WriteTo(stringWriter, HtmlEncoder.Default);
|
|
await stringWriter.FlushAsync();
|
|
return stringWriter.ToString();
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Renders a razor component to a string
|
|
/// </summary>
|
|
public async Task<string> ComponentAsync(string componentName, ActionContext context, object args = null)
|
|
{
|
|
IViewComponentHelper viewComponentHelper = context.HttpContext.RequestServices.GetRequiredService<IViewComponentHelper>();
|
|
|
|
using StringWriter stringWriter = new();
|
|
|
|
ViewContext viewContext = BuildViewContext(context, stringWriter);
|
|
|
|
(viewComponentHelper as IViewContextAware)?.Contextualize(viewContext);
|
|
IHtmlContent result = await viewComponentHelper.InvokeAsync(componentName, args);
|
|
|
|
result.WriteTo(stringWriter, HtmlEncoder.Default);
|
|
await stringWriter.FlushAsync();
|
|
return stringWriter.ToString();
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Renders a razor page to a string
|
|
/// </summary>
|
|
public async Task<string> PageAsync<T>(T model) where T : PageModel
|
|
{
|
|
IRazorPage pageResult = FindPage(model.PageContext, model.PageContext.ActionDescriptor.RelativePath);
|
|
|
|
using StringWriter stringWriter = new();
|
|
|
|
RazorView view = new RazorView(ViewEngine, PageActivator, new List<IRazorPage>(), pageResult, HtmlEncoder.Default, new DiagnosticListener("ViewRenderService"));
|
|
|
|
ViewContext viewContext = BuildViewContext(model.PageContext, stringWriter, view);
|
|
viewContext.RouteData = model.RouteData;
|
|
viewContext.ViewData = model.ViewData;
|
|
|
|
Microsoft.AspNetCore.Mvc.RazorPages.Page razorPage = (Microsoft.AspNetCore.Mvc.RazorPages.Page)pageResult;
|
|
razorPage.PageContext = model.PageContext;
|
|
razorPage.ViewContext = viewContext;
|
|
|
|
PageActivator.Activate(razorPage, viewContext);
|
|
|
|
await razorPage.ExecuteAsync();
|
|
await stringWriter.FlushAsync();
|
|
|
|
return stringWriter.ToString();
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Renders a razor view to a string
|
|
/// </summary>
|
|
public async Task<string> ViewAsync(string view, object model = null, Action<ViewContext> onBeforeRender = null)
|
|
{
|
|
return await ViewAsync(BuildActionContext(), view, model, onBeforeRender);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Renders a razor view to a string
|
|
/// </summary>
|
|
public async Task<string> ViewAsync(ActionContext context, string view, object model = null, Action<ViewContext> onBeforeRender = null)
|
|
{
|
|
IView viewResult = FindView(context, view);
|
|
|
|
await using StringWriter stringWriter = new();
|
|
|
|
ViewContext viewContext = BuildViewContext(context, stringWriter, viewResult);
|
|
viewContext.RouteData = context.RouteData;
|
|
viewContext.ViewData.Model = model;
|
|
|
|
onBeforeRender?.Invoke(viewContext);
|
|
|
|
await viewResult.RenderAsync(viewContext);
|
|
await stringWriter.FlushAsync();
|
|
|
|
return stringWriter.ToString();
|
|
}
|
|
|
|
|
|
/// <inheritdoc />
|
|
public void Dispose()
|
|
{
|
|
ServiceScope?.Dispose();
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Build the view context
|
|
/// </summary>
|
|
protected virtual ViewContext BuildViewContext(ActionContext context, StringWriter writer, IView view = null)
|
|
{
|
|
ViewDataDictionary viewData = new(ModelMetadataProvider, context.ModelState); // result.ViewData ?? new ViewData...;
|
|
ITempDataDictionary tempData = TempDataDictionaryFactory.GetTempData(context.HttpContext); // result.TempData ?? TempData...;
|
|
|
|
return new ViewContext(context, view ?? NullView.Instance, viewData, tempData, writer, HtmlHelperOptions);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Builds a new view context
|
|
/// </summary>
|
|
protected virtual ActionContext BuildActionContext()
|
|
{
|
|
HttpContext context = GetHttpContext();
|
|
RouteData routeData = context.GetRouteData();
|
|
return new ActionContext(context, routeData, new ActionDescriptor());
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Get HTTP context or mock one
|
|
/// </summary>
|
|
protected virtual HttpContext GetHttpContext()
|
|
{
|
|
HttpContext context = HttpContextAccessor.HttpContext;
|
|
context ??= new DefaultHttpContext()
|
|
{
|
|
RequestServices = ServiceScope.ServiceProvider
|
|
};
|
|
|
|
return context;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Tries to find a view
|
|
/// </summary>
|
|
protected virtual IView FindView(ActionContext actionContext, string viewName)
|
|
{
|
|
ViewEngineResult getViewResult = ViewEngine.GetView(executingFilePath: null, viewPath: viewName, isMainPage: false);
|
|
if (getViewResult.Success)
|
|
{
|
|
return getViewResult.View;
|
|
}
|
|
|
|
ViewEngineResult findViewResult = ViewEngine.FindView(actionContext, viewName, isMainPage: false);
|
|
if (findViewResult.Success)
|
|
{
|
|
return findViewResult.View;
|
|
}
|
|
|
|
IEnumerable<string> searchedLocations = getViewResult.SearchedLocations.Concat(findViewResult.SearchedLocations);
|
|
string errorMessage = string.Join(Environment.NewLine, new[] { $"Unable to find view '{viewName}'. The following locations were searched:" }.Concat(searchedLocations));
|
|
|
|
throw new InvalidOperationException(errorMessage);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Tries to find a page
|
|
/// </summary>
|
|
protected virtual IRazorPage FindPage(ActionContext actionContext, string pageName)
|
|
{
|
|
RazorPageResult getPageResult = ViewEngine.GetPage(executingFilePath: null, pagePath: pageName);
|
|
if (getPageResult.Page != null)
|
|
{
|
|
return getPageResult.Page;
|
|
}
|
|
|
|
RazorPageResult findPageResult = ViewEngine.FindPage(actionContext, pageName: pageName);
|
|
if (findPageResult.Page != null)
|
|
{
|
|
return findPageResult.Page;
|
|
}
|
|
|
|
IEnumerable<string> searchedLocations = getPageResult.SearchedLocations.Concat(findPageResult.SearchedLocations);
|
|
string errorMessage = string.Join(Environment.NewLine, new[] { $"Unable to find page '{pageName}'. The following locations were searched:" }.Concat(searchedLocations));
|
|
|
|
throw new InvalidOperationException(errorMessage);
|
|
}
|
|
|
|
|
|
class GenericController : ControllerBase { }
|
|
|
|
|
|
class NullView : IView
|
|
{
|
|
public static readonly NullView Instance = new NullView();
|
|
|
|
public string Path => string.Empty;
|
|
|
|
public Task RenderAsync(ViewContext context)
|
|
{
|
|
if (context == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(context));
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
public interface IRazorRenderer
|
|
{
|
|
/// <summary>
|
|
/// Renders a razor component to a string
|
|
/// </summary>
|
|
Task<string> ComponentAsync<T>(object args = null) where T : ViewComponent;
|
|
|
|
/// <summary>
|
|
/// Renders a razor component to a string
|
|
/// </summary>
|
|
Task<string> ComponentAsync(Type componentType, object args = null);
|
|
|
|
/// <summary>
|
|
/// Renders a razor component to a string
|
|
/// </summary>
|
|
Task<string> ComponentAsync(string componentName, object args = null);
|
|
|
|
/// <summary>
|
|
/// Renders a razor component to a string
|
|
/// </summary>
|
|
Task<string> ComponentAsync<T>(ActionContext context, object args = null) where T : ViewComponent;
|
|
|
|
/// <summary>
|
|
/// Renders a razor component to a string
|
|
/// </summary>
|
|
Task<string> ComponentAsync(Type componentType, ActionContext context, object args = null);
|
|
|
|
/// <summary>
|
|
/// Renders a razor component to a string
|
|
/// </summary>
|
|
Task<string> ComponentAsync(string componentName, ActionContext context, object args = null);
|
|
|
|
/// <summary>
|
|
/// Renders a razor page to a string
|
|
/// </summary>
|
|
Task<string> PageAsync<T>(T model) where T : PageModel;
|
|
|
|
/// <summary>
|
|
/// Renders a razor view to a string
|
|
/// </summary>
|
|
Task<string> ViewAsync(string view, object model = null, Action<ViewContext> onBeforeRender = null);
|
|
|
|
/// <summary>
|
|
/// Renders a razor view to a string
|
|
/// </summary>
|
|
Task<string> ViewAsync(ActionContext context, string view, object model = null, Action<ViewContext> onBeforeRender = null);
|
|
} |