以编程方式将 Razor 页面呈现为 HTML 字符串
Posted
技术标签:
【中文标题】以编程方式将 Razor 页面呈现为 HTML 字符串【英文标题】:Programmatically render Razor Page as HTML string 【发布时间】:2020-11-27 13:30:00 【问题描述】:目标
我想在后端生成一个 html 字符串 使用 HtmlToPDF 库将其转换为 PDF。 我还希望能够在浏览器中轻松查看生成的 HTML,以便进行调试/调整。该页面仅在IsDevelopment()
时才会公开。
我希望它尽可能简单。
我正在使用 ASP.NET Core 3.1
方法
剃刀页面
我想我会尝试 新的 Razor 页面,因为它们被宣传为超级简单。
@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
。但是 Page 是 Model,因为它是一个 Page,所以它不是一个简单的 ViewModel。它依赖于 DI,并期望执行 OnGetAsync()
以填充模型。这几乎是第 22 条规则。
我还尝试通过_razorViewEngine.FindView
获取视图而不是页面,但这也需要模型,所以我们回到了 catch-22。
另一个问题。调试/调整页面的目的是轻松查看生成的内容。但是,如果我必须在 IndexModel
之外创建一个 Model
,那么它就不再代表某处服务中实际生成的内容。
所有这一切都让我想知道我是否走在正确的道路上。还是我错过了什么?
【问题讨论】:
检查一下;github.com/Tyrrrz/MiniRazor 用于构建电子邮件模板应该很有趣,但也适用于任何 html 页面。我也需要创建一个 html 到 pdf 的工具,所以我很快就会遇到你的问题,需要找到一个好的解决方案。 【参考方案1】:请参考以下步骤将部分视图渲染为字符串:
向名为 IRazorPartialToStringRenderer.cs 的 Services 文件夹添加一个接口。
public interface IRazorPartialToStringRenderer
Task<string> RenderPartialToStringAsync<TModel>(string partialName, TModel model);
使用以下代码将 C# 类文件添加到名为 RazorPartialToStringRenderer.cs 的 Services 文件夹中:
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());
在Startup
类的ConfigureServices
方法中注册服务:
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。
【讨论】:
如何包含布局以便获得完整的页面 html 而不仅仅是部分?我正在做类似的事情,但它只返回页面中的 html 而没有布局代码。【参考方案2】:我成功破解了!毕竟我走错了路……解决方案是使用ViewComponent
。但它仍然很时髦!
感谢
Asp.Net Core render ViewComponent to variable https://github.com/jakakonda/View2String/blob/master/View2String/Services/ViewRendererService2.cs https://forums.asp.net/t/2132316.aspx?Render+ViewComponent+as+string+from+controller Render ViewComponent inside html string 还有更多...解决方案
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 页面(调试/调整页面)
注意文件后面没有代码
@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
所以我们基本上必须将它复制/粘贴到我们自己的代码中:(
也许以后会改进。
无论如何... IMO 这比使用IRazorViewEngine
更简单,几乎每个谷歌搜索都会出现:)
希望这可以帮助某人。
【讨论】:
这种方法比较繁琐。上一个答案为您提供了无限的广泛使用机会。假设你想从你在 get 方法中调用引擎的地方传递一个模型,你会怎么做?因为您肯定需要显示与当前请求相关的值,而不仅仅是存储在数据库中的一些数据 @TavershimaInvokeAsync()
可以接受任意数量的参数。所以你只需打电话给RenderViewComponentToStringAsync<MyViewComponent>(int arg1, string arg2, etc...);
很简单
这是一个使用 args docs.microsoft.com/en-us/aspnet/core/mvc/views/… 调用 InvokeAsync 的示例
我添加了从 RazorPage 和控制器传递路由参数的示例
您正在调用一个称为字符串渲染器的页面。您正在调用的页面仅适用于存储在数据库中的预先存在的数据。假设您从请求中获得了一个路由参数并且您需要对其进行处理,那么如何将这些数据中继到该页面以便调用使用它?我想在这里告诉你的是,对于这个简单的案例来说,工程量太大了以上是关于以编程方式将 Razor 页面呈现为 HTML 字符串的主要内容,如果未能解决你的问题,请参考以下文章