[英]Programmatically render Razor Page as HTML string
IsDevelopment()
.IsDevelopment()
时才会公开。 I'm using ASP.NET Core 3.1我正在使用 ASP.NET 核心 3.1
I figured I'd try the new Razor Pages as they're advertised as super easy.我想我会尝试新的 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();
}
}
}
This works in the browser - yay!这适用于浏览器 - 耶!
I found Render a Razor Page to string which appears to do what I want.我发现Render a Razor Page to string这似乎可以满足我的要求。
But this is where the trouble start:(但这就是麻烦开始的地方:(
First off, I find it very odd that when you find the page via _razorViewEngine.FindPage
it doesn't know how to populate the ViewContext
or the Model
.首先,我觉得很奇怪,当您通过
_razorViewEngine.FindPage
找到页面时,它不知道如何填充ViewContext
或Model
。 I'd think the job of IndexModel
was to populate these.我认为
IndexModel
的工作是填充这些。 I was hoping it was possible to ask ASP.NET for the IndexModel
Page and that would be that.我希望可以向 ASP.NET 询问
IndexModel
页面,就是这样。
Anyway... the next problem.无论如何……下一个问题。 In order to render the Page I have to manually create a
ViewContext
and I have to supply it with a Model
.为了呈现页面,我必须手动创建
ViewContext
并且必须为其提供Model
。 But the Page is the Model, and since it's a Page it isn't a simple ViewModel.但是 Page 是 Model,因为它是一个 Page,所以它不是一个简单的 ViewModel。 It rely on DI and it expects
OnGetAsync()
to be executed in order to populate the Model.它依赖于 DI,并希望执行
OnGetAsync()
以填充 Model。 It's pretty much a catch-22.这几乎是第 22 条规则。
I also tried fetching the View instead of the Page via _razorViewEngine.FindView
but that also requires a Model, so we're back to catch-22.我还尝试通过
_razorViewEngine.FindView
获取视图而不是页面,但这也需要Model,所以我们回到catch-22。
Another issue.另一个问题。 The purpose of the debug/tweaking page was to easily see what was generated.
调试/调整页面的目的是轻松查看生成的内容。 But if I have to create a
Model
outside IndexModel
then it's no longer representative of what is actually being generated in a service somewhere.但是,如果我必须在
Model
IndexModel
那么它不再代表某处服务中实际生成的内容。
All this have me wondering if I'm even on the right path.这一切都让我想知道我是否走在正确的道路上。 Or am I missing something?
还是我错过了什么?
Please refer to the following steps to render A Partial View To A String:请参考以下步骤将部分视图渲染为字符串:
Add an interface to the Services folder named IRazorPartialToStringRenderer.cs.将接口添加到名为 IRazorPartialToStringRenderer.cs 的 Services 文件夹。
public interface IRazorPartialToStringRenderer { Task<string> RenderPartialToStringAsync<TModel>(string partialName, TModel model); }
Add a C# class file to the Services folder named RazorPartialToStringRenderer.cs with the following code:使用以下代码将 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()); } } }
Register the services in the ConfigureServices
method in the Startup
class:在
Startup
class 的ConfigureServices
方法中注册服务:
public void ConfigureServices(IServiceCollection services) { services.AddRazorPages(); services.AddTransient<IRazorPartialToStringRenderer, RazorPartialToStringRenderer>(); }
Using the RenderPartialToStringAsync() method to render Razor Page as HTML string:使用 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 }
The debug screenshot as below:调试截图如下:
More detail steps, please check this blog Rendering A Partial View To A String .更多详细步骤,请查看此博客Rendering A Partial View To A String 。
I managed to crack it.我设法破解它。 I was on the wrong path after all... the solution is to use a
ViewComponent
.毕竟我走错了路……解决方案是使用
ViewComponent
。 But it's still funky!但它仍然很时髦!
Thanks to谢谢
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);
}
}
}
and the view is placed in Pages/Shared/Components/My/Default.cshtml并且视图放置在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;
}
}
}
Note there is no code behind file注意文件后面没有代码
@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 });
}
[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 });
}
It's very unfortunate that the injected IViewComponentHelper
doesn't work out of the box.非常不幸的是,注入的
IViewComponentHelper
不能开箱即用。
So we have do this very unintuitive thing to make it work.所以我们做了这个非常不直观的事情来让它工作。
(_viewComponentHelper as IViewContextAware).Contextualize(viewContext);
which causes a cascade of odd things like the fake ActionContext
and ViewContext
which require a TextWriter
but it isn't used for ANYTHING!这会导致一系列奇怪的事情,例如需要
TextWriter
的虚假ActionContext
和ViewContext
,但它不用于任何事情! In fact the hole ViewContext
isn't used at all.事实上,根本没有使用
ViewContext
洞。 It just needs to exist:(它只需要存在:(
Also the NullView
... for some reason Microsoft.AspNetCore.Mvc.ViewFeatures.NullView
is Internal
so we basically have to copy/paste it into our own code.此外
NullView
...由于某种原因Microsoft.AspNetCore.Mvc.ViewFeatures.NullView
是Internal
的,所以我们基本上必须将其复制/粘贴到我们自己的代码中。
Perhaps it'll be improved in the future.也许将来会有所改进。
Anyway: IMO this is simpler then using IRazorViewEngine
which turns up in pretty much every web search:)无论如何:IMO这比使用
IRazorViewEngine
更简单,几乎每次web搜索都会出现:)
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.