以编程方式将 Razor 页面呈现为 HTML 字符串
Programmatically render Razor Page as HTML string
目标
- 我想在后端生成一个 HTML 字符串
使用 HtmlToPDF 库将其转换为 PDF。
- 出于 debugging/tweaking 目的,我还希望能够在浏览器中轻松查看生成的 HTML。当
IsDevelopment()
. 时,页面只会是 public
- 我希望它尽可能简单。
我正在使用 ASP.NET Core 3.1
方法
剃刀页面
我想我会尝试 new Razor Pages,因为它们被宣传为超级简单。
@page
@using MyProject.Pages.Pdf
@model IndexModel
<h2>Test</h2>
<p>
@Model.Message
</p>
namespace MyProject.Pages.Pdf
{
public class IndexModel : PageModel
{
private readonly MyDbContext _context;
public IndexModel(MyDbContext context)
{
_context = context;
}
public string Message { get; private set; } = "PageModel in C#";
public async Task<IActionResult> OnGetAsync()
{
var count = await _context.Foos.CountAsync();
Message += $" Server time is { DateTime.Now } and the Foo count is { count }";
return Page();
}
}
}
这在浏览器中有效 - 耶!
渲染并获取 HTML 字符串
我发现 Render a Razor Page to string 似乎符合我的要求。
但这就是麻烦的开始:(
问题
首先,我觉得很奇怪,当你通过 _razorViewEngine.FindPage
找到页面时,它不知道如何填充 ViewContext
或 Model
。我认为 IndexModel
的工作是填充这些。我希望可以向 ASP.NET 询问 IndexModel
页面,就是这样。
无论如何...下一个问题。为了呈现页面,我必须手动创建一个 ViewContext
并且我必须为其提供一个 Model
。但是页面就是模型,因为它是页面,所以它不是简单的 ViewModel。它依赖于 DI,并且期望执行 OnGetAsync()
以填充模型。这几乎是一个陷阱 22。
我还尝试通过 _razorViewEngine.FindView
获取视图而不是页面,但这也需要一个模型,所以我们回到 catch-22。
另一个问题。 debug/tweaking 页面的目的是轻松查看生成的内容。但是,如果我必须在 IndexModel
之外创建一个 Model
,那么它不再代表某处服务中实际生成的内容。
所有这些让我想知道我是否走在正确的道路上。还是我遗漏了什么?
请参考以下步骤将部分视图呈现为字符串:
将接口添加到名为 IRazorPartialToStringRenderer.cs 的服务文件夹。
public interface IRazorPartialToStringRenderer
{
Task<string> RenderPartialToStringAsync<TModel>(string partialName, TModel model);
}
使用以下代码将 C# class 文件添加到名为 RazorPartialToStringRenderer.cs 的服务文件夹中:
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
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.Rendering;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing;
namespace RazorPageSample.Services
{
public class RazorPartialToStringRenderer : IRazorPartialToStringRenderer
{
private IRazorViewEngine _viewEngine;
private ITempDataProvider _tempDataProvider;
private IServiceProvider _serviceProvider;
public RazorPartialToStringRenderer(
IRazorViewEngine viewEngine,
ITempDataProvider tempDataProvider,
IServiceProvider serviceProvider)
{
_viewEngine = viewEngine;
_tempDataProvider = tempDataProvider;
_serviceProvider = serviceProvider;
}
public async Task<string> RenderPartialToStringAsync<TModel>(string partialName, TModel model)
{
var actionContext = GetActionContext();
var partial = FindView(actionContext, partialName);
using (var output = new StringWriter())
{
var viewContext = new ViewContext(
actionContext,
partial,
new ViewDataDictionary<TModel>(
metadataProvider: new EmptyModelMetadataProvider(),
modelState: new ModelStateDictionary())
{
Model = model
},
new TempDataDictionary(
actionContext.HttpContext,
_tempDataProvider),
output,
new HtmlHelperOptions()
);
await partial.RenderAsync(viewContext);
return output.ToString();
}
}
private IView FindView(ActionContext actionContext, string partialName)
{
var getPartialResult = _viewEngine.GetView(null, partialName, false);
if (getPartialResult.Success)
{
return getPartialResult.View;
}
var findPartialResult = _viewEngine.FindView(actionContext, partialName, false);
if (findPartialResult.Success)
{
return findPartialResult.View;
}
var searchedLocations = getPartialResult.SearchedLocations.Concat(findPartialResult.SearchedLocations);
var errorMessage = string.Join(
Environment.NewLine,
new[] { $"Unable to find partial '{partialName}'. The following locations were searched:" }.Concat(searchedLocations)); ;
throw new InvalidOperationException(errorMessage);
}
private ActionContext GetActionContext()
{
var httpContext = new DefaultHttpContext
{
RequestServices = _serviceProvider
};
return new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
}
}
}
在ConfigureServices
方法中注册服务Startup
class:
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddTransient<IRazorPartialToStringRenderer, RazorPartialToStringRenderer>();
}
使用 RenderPartialToStringAsync() 方法将 Razor 页面呈现为 HTML 字符串:
public class ContactModel : PageModel
{
private readonly IRazorPartialToStringRenderer _renderer;
public ContactModel(IRazorPartialToStringRenderer renderer)
{
_renderer = renderer;
}
public void OnGet()
{
}
[BindProperty]
public ContactForm ContactForm { get; set; }
[TempData]
public string PostResult { get; set; }
public async Task<IActionResult> OnPostAsync()
{
var body = await _renderer.RenderPartialToStringAsync("_ContactEmailPartial", ContactForm); //transfer model to the partial view, and then render the Partial view to string.
PostResult = "Check your specified pickup directory";
return RedirectToPage();
}
}
public class ContactForm
{
public string Email { get; set; }
public string Message { get; set; }
public string Name { get; set; }
public string Subject { get; set; }
public Priority Priority { get; set; }
}
public enum Priority
{
Low, Medium, High
}
调试截图如下:
更多详细步骤,请查看此博客Rendering A Partial View To A String。
我成功破解了!毕竟我走错了路……解决方案是使用 ViewComponent
。但它仍然很时髦!
感谢
- https://github.com/jakakonda/View2String/blob/master/View2String/Services/ViewRendererService2.cs
- https://forums.asp.net/t/2132316.aspx?Render+ViewComponent+as+string+from+controller
- 还有很多...
解决方案
将 PageModel 转换为 ViewComponent
namespace MyProject.ViewComponents
{
public class MyViewComponent : ViewComponent
{
private readonly MyDbContext _context;
public MyViewComponent(MyDbContext context)
{
_context = context;
}
public async Task<IViewComponentResult> InvokeAsync()
{
var count = await _context.Foos.CountAsync();
var message = $"Server time is { DateTime.Now } and the Foo count is { count }";
return View<string>(message);
}
}
}
并且视图放在Pages/Shared/Components/My/Default.cshtml
@model string
<h2>Test</h2>
<p>
@Model
</p>
服务
using System;
using System.IO;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing;
public class RenderViewComponentService
{
private readonly IServiceProvider _serviceProvider;
private readonly ITempDataProvider _tempDataProvider;
private readonly IViewComponentHelper _viewComponentHelper;
public RenderViewComponentService(
IServiceProvider serviceProvider,
ITempDataProvider tempDataProvider,
IViewComponentHelper viewComponentHelper
)
{
_serviceProvider = serviceProvider;
_tempDataProvider = tempDataProvider;
_viewComponentHelper = viewComponentHelper;
}
public async Task<string> RenderViewComponentToStringAsync<TViewComponent>(object args)
where TViewComponent : ViewComponent
{
var viewContext = GetFakeViewContext();
(_viewComponentHelper as IViewContextAware).Contextualize(viewContext);
var htmlContent = await _viewComponentHelper.InvokeAsync<TViewComponent>(args);
using var stringWriter = new StringWriter();
htmlContent.WriteTo(stringWriter, HtmlEncoder.Default);
var html = stringWriter.ToString();
return html;
}
private ViewContext GetFakeViewContext(ActionContext actionContext = null, TextWriter writer = null)
{
actionContext ??= GetFakeActionContext();
var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary());
var tempData = new TempDataDictionary(actionContext.HttpContext, _tempDataProvider);
var viewContext = new ViewContext(
actionContext,
NullView.Instance,
viewData,
tempData,
writer ?? TextWriter.Null,
new HtmlHelperOptions());
return viewContext;
}
private ActionContext GetFakeActionContext()
{
var httpContext = new DefaultHttpContext
{
RequestServices = _serviceProvider,
};
var routeData = new RouteData();
var actionDescriptor = new ActionDescriptor();
return new ActionContext(httpContext, routeData, actionDescriptor);
}
private 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;
}
}
}
用法
来自 Razor 页面(debug/tweaking 页面)
注意文件后面没有代码
@page
@using MyProject.ViewComponents
@await Component.InvokeAsync(typeof(MyViewComponent))
有路由数据
@page "{id}"
@using MyProject.ViewComponents
@await Component.InvokeAsync(typeof(MyViewComponent), RouteData.Values["id"])
来自控制器
[HttpGet]
public async Task<IActionResult> Get()
{
var html = await _renderViewComponentService
.RenderViewComponentToStringAsync<MyViewComponent>();
// do something with the html
return Ok(new { html });
}
使用 FromRoute
[HttpGet("{id}")]
public async Task<IActionResult> Get([FromRoute] int id)
{
var html = await _renderViewComponentService
.RenderViewComponentToStringAsync<MyViewComponent>(id);
// do something with the html
return Ok(new { html });
}
奇怪
非常不幸的是,注入的 IViewComponentHelper
不能开箱即用。
所以我们做了这个非常不直观的事情来让它工作。
(_viewComponentHelper as IViewContextAware).Contextualize(viewContext);
这会导致一连串奇怪的事情发生,例如假的 ActionContext
和 ViewContext
需要 TextWriter
但它没有用于任何事情!事实上 ViewContext
这个洞根本就没有用到。它只需要存在:(
另外 NullView
... 由于某种原因 Microsoft.AspNetCore.Mvc.ViewFeatures.NullView
是 Internal
所以我们基本上必须 copy/paste 它到我们自己的代码中。
以后可能会改进
无论如何:IMO 这比使用 IRazorViewEngine
更简单,它几乎在每个网络搜索中都会出现 :)
目标
- 我想在后端生成一个 HTML 字符串 使用 HtmlToPDF 库将其转换为 PDF。
- 出于 debugging/tweaking 目的,我还希望能够在浏览器中轻松查看生成的 HTML。当
IsDevelopment()
. 时,页面只会是 public
- 我希望它尽可能简单。
我正在使用 ASP.NET Core 3.1
方法
剃刀页面
我想我会尝试 new Razor Pages,因为它们被宣传为超级简单。
@page
@using MyProject.Pages.Pdf
@model IndexModel
<h2>Test</h2>
<p>
@Model.Message
</p>
namespace MyProject.Pages.Pdf
{
public class IndexModel : PageModel
{
private readonly MyDbContext _context;
public IndexModel(MyDbContext context)
{
_context = context;
}
public string Message { get; private set; } = "PageModel in C#";
public async Task<IActionResult> OnGetAsync()
{
var count = await _context.Foos.CountAsync();
Message += $" Server time is { DateTime.Now } and the Foo count is { count }";
return Page();
}
}
}
这在浏览器中有效 - 耶!
渲染并获取 HTML 字符串
我发现 Render a Razor Page to string 似乎符合我的要求。
但这就是麻烦的开始:(
问题
首先,我觉得很奇怪,当你通过 _razorViewEngine.FindPage
找到页面时,它不知道如何填充 ViewContext
或 Model
。我认为 IndexModel
的工作是填充这些。我希望可以向 ASP.NET 询问 IndexModel
页面,就是这样。
无论如何...下一个问题。为了呈现页面,我必须手动创建一个 ViewContext
并且我必须为其提供一个 Model
。但是页面就是模型,因为它是页面,所以它不是简单的 ViewModel。它依赖于 DI,并且期望执行 OnGetAsync()
以填充模型。这几乎是一个陷阱 22。
我还尝试通过 _razorViewEngine.FindView
获取视图而不是页面,但这也需要一个模型,所以我们回到 catch-22。
另一个问题。 debug/tweaking 页面的目的是轻松查看生成的内容。但是,如果我必须在 IndexModel
之外创建一个 Model
,那么它不再代表某处服务中实际生成的内容。
所有这些让我想知道我是否走在正确的道路上。还是我遗漏了什么?
请参考以下步骤将部分视图呈现为字符串:
将接口添加到名为 IRazorPartialToStringRenderer.cs 的服务文件夹。
public interface IRazorPartialToStringRenderer { Task<string> RenderPartialToStringAsync<TModel>(string partialName, TModel model); }
使用以下代码将 C# class 文件添加到名为 RazorPartialToStringRenderer.cs 的服务文件夹中:
using System; using System.IO; using System.Linq; using System.Threading.Tasks; 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.Rendering; using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Routing; namespace RazorPageSample.Services { public class RazorPartialToStringRenderer : IRazorPartialToStringRenderer { private IRazorViewEngine _viewEngine; private ITempDataProvider _tempDataProvider; private IServiceProvider _serviceProvider; public RazorPartialToStringRenderer( IRazorViewEngine viewEngine, ITempDataProvider tempDataProvider, IServiceProvider serviceProvider) { _viewEngine = viewEngine; _tempDataProvider = tempDataProvider; _serviceProvider = serviceProvider; } public async Task<string> RenderPartialToStringAsync<TModel>(string partialName, TModel model) { var actionContext = GetActionContext(); var partial = FindView(actionContext, partialName); using (var output = new StringWriter()) { var viewContext = new ViewContext( actionContext, partial, new ViewDataDictionary<TModel>( metadataProvider: new EmptyModelMetadataProvider(), modelState: new ModelStateDictionary()) { Model = model }, new TempDataDictionary( actionContext.HttpContext, _tempDataProvider), output, new HtmlHelperOptions() ); await partial.RenderAsync(viewContext); return output.ToString(); } } private IView FindView(ActionContext actionContext, string partialName) { var getPartialResult = _viewEngine.GetView(null, partialName, false); if (getPartialResult.Success) { return getPartialResult.View; } var findPartialResult = _viewEngine.FindView(actionContext, partialName, false); if (findPartialResult.Success) { return findPartialResult.View; } var searchedLocations = getPartialResult.SearchedLocations.Concat(findPartialResult.SearchedLocations); var errorMessage = string.Join( Environment.NewLine, new[] { $"Unable to find partial '{partialName}'. The following locations were searched:" }.Concat(searchedLocations)); ; throw new InvalidOperationException(errorMessage); } private ActionContext GetActionContext() { var httpContext = new DefaultHttpContext { RequestServices = _serviceProvider }; return new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); } } }
在
ConfigureServices
方法中注册服务Startup
class:public void ConfigureServices(IServiceCollection services) { services.AddRazorPages(); services.AddTransient<IRazorPartialToStringRenderer, RazorPartialToStringRenderer>(); }
使用 RenderPartialToStringAsync() 方法将 Razor 页面呈现为 HTML 字符串:
public class ContactModel : PageModel { private readonly IRazorPartialToStringRenderer _renderer; public ContactModel(IRazorPartialToStringRenderer renderer) { _renderer = renderer; } public void OnGet() { } [BindProperty] public ContactForm ContactForm { get; set; } [TempData] public string PostResult { get; set; } public async Task<IActionResult> OnPostAsync() { var body = await _renderer.RenderPartialToStringAsync("_ContactEmailPartial", ContactForm); //transfer model to the partial view, and then render the Partial view to string. PostResult = "Check your specified pickup directory"; return RedirectToPage(); } } public class ContactForm { public string Email { get; set; } public string Message { get; set; } public string Name { get; set; } public string Subject { get; set; } public Priority Priority { get; set; } } public enum Priority { Low, Medium, High }
调试截图如下:
更多详细步骤,请查看此博客Rendering A Partial View To A String。
我成功破解了!毕竟我走错了路……解决方案是使用 ViewComponent
。但它仍然很时髦!
感谢
- https://github.com/jakakonda/View2String/blob/master/View2String/Services/ViewRendererService2.cs
- https://forums.asp.net/t/2132316.aspx?Render+ViewComponent+as+string+from+controller
- 还有很多...
解决方案
将 PageModel 转换为 ViewComponent
namespace MyProject.ViewComponents
{
public class MyViewComponent : ViewComponent
{
private readonly MyDbContext _context;
public MyViewComponent(MyDbContext context)
{
_context = context;
}
public async Task<IViewComponentResult> InvokeAsync()
{
var count = await _context.Foos.CountAsync();
var message = $"Server time is { DateTime.Now } and the Foo count is { count }";
return View<string>(message);
}
}
}
并且视图放在Pages/Shared/Components/My/Default.cshtml
@model string
<h2>Test</h2>
<p>
@Model
</p>
服务
using System;
using System.IO;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing;
public class RenderViewComponentService
{
private readonly IServiceProvider _serviceProvider;
private readonly ITempDataProvider _tempDataProvider;
private readonly IViewComponentHelper _viewComponentHelper;
public RenderViewComponentService(
IServiceProvider serviceProvider,
ITempDataProvider tempDataProvider,
IViewComponentHelper viewComponentHelper
)
{
_serviceProvider = serviceProvider;
_tempDataProvider = tempDataProvider;
_viewComponentHelper = viewComponentHelper;
}
public async Task<string> RenderViewComponentToStringAsync<TViewComponent>(object args)
where TViewComponent : ViewComponent
{
var viewContext = GetFakeViewContext();
(_viewComponentHelper as IViewContextAware).Contextualize(viewContext);
var htmlContent = await _viewComponentHelper.InvokeAsync<TViewComponent>(args);
using var stringWriter = new StringWriter();
htmlContent.WriteTo(stringWriter, HtmlEncoder.Default);
var html = stringWriter.ToString();
return html;
}
private ViewContext GetFakeViewContext(ActionContext actionContext = null, TextWriter writer = null)
{
actionContext ??= GetFakeActionContext();
var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary());
var tempData = new TempDataDictionary(actionContext.HttpContext, _tempDataProvider);
var viewContext = new ViewContext(
actionContext,
NullView.Instance,
viewData,
tempData,
writer ?? TextWriter.Null,
new HtmlHelperOptions());
return viewContext;
}
private ActionContext GetFakeActionContext()
{
var httpContext = new DefaultHttpContext
{
RequestServices = _serviceProvider,
};
var routeData = new RouteData();
var actionDescriptor = new ActionDescriptor();
return new ActionContext(httpContext, routeData, actionDescriptor);
}
private 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;
}
}
}
用法
来自 Razor 页面(debug/tweaking 页面)
注意文件后面没有代码
@page
@using MyProject.ViewComponents
@await Component.InvokeAsync(typeof(MyViewComponent))
有路由数据
@page "{id}"
@using MyProject.ViewComponents
@await Component.InvokeAsync(typeof(MyViewComponent), RouteData.Values["id"])
来自控制器
[HttpGet]
public async Task<IActionResult> Get()
{
var html = await _renderViewComponentService
.RenderViewComponentToStringAsync<MyViewComponent>();
// do something with the html
return Ok(new { html });
}
使用 FromRoute
[HttpGet("{id}")]
public async Task<IActionResult> Get([FromRoute] int id)
{
var html = await _renderViewComponentService
.RenderViewComponentToStringAsync<MyViewComponent>(id);
// do something with the html
return Ok(new { html });
}
奇怪
非常不幸的是,注入的 IViewComponentHelper
不能开箱即用。
所以我们做了这个非常不直观的事情来让它工作。
(_viewComponentHelper as IViewContextAware).Contextualize(viewContext);
这会导致一连串奇怪的事情发生,例如假的 ActionContext
和 ViewContext
需要 TextWriter
但它没有用于任何事情!事实上 ViewContext
这个洞根本就没有用到。它只需要存在:(
另外 NullView
... 由于某种原因 Microsoft.AspNetCore.Mvc.ViewFeatures.NullView
是 Internal
所以我们基本上必须 copy/paste 它到我们自己的代码中。
以后可能会改进
无论如何:IMO 这比使用 IRazorViewEngine
更简单,它几乎在每个网络搜索中都会出现 :)