简体   繁体   English

以编程方式将 Razor 页面呈现为 HTML 字符串

[英]Programmatically render Razor Page as HTML string

Goal目标

  • I'm trying to generate a HTML string on the backend as I want to convert it to a PDF using a HtmlToPDF library.我正在尝试在后端生成 HTML 字符串,因为我想使用 HtmlToPDF 库将其转换为 PDF 。
  • I also want to be able to easily see the generated HTML in the browser, for debugging/tweaking purposes.我还希望能够在浏览器中轻松查看生成的 HTML,以进行调试/调整。 The page will only be public when IsDevelopment() .该页面仅在IsDevelopment()时才会公开。
  • I want it to be as simple as possible.我希望它尽可能简单。

I'm using ASP.NET Core 3.1我正在使用 ASP.NET 核心 3.1

Approach方法

Razor Page Razor 页码

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!这适用于浏览器 - 耶!

Render and get HTML string渲染并获取 HTML 字符串

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:(但这就是麻烦开始的地方:(

Problems问题

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找到页面时,它不知道如何填充ViewContextModel 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:请参考以下步骤将部分视图渲染为字符串:

  1. Add an interface to the Services folder named IRazorPartialToStringRenderer.cs.将接口添加到名为 IRazorPartialToStringRenderer.cs 的 Services 文件夹。

     public interface IRazorPartialToStringRenderer { Task<string> RenderPartialToStringAsync<TModel>(string partialName, TModel model); }
  2. 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()); } } }
  3. 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>(); }
  4. 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谢谢

The Solution解决方案

Converted PageModel into ViewComponent将 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);
        }
    }
}

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>

The Service服务

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;
        }
    }
}

Usage用法

From Razor Page (the debug/tweaking page)来自 Razor 页面(调试/调整页面)

Note there is no code behind file注意文件后面没有代码

@page
@using MyProject.ViewComponents

@await Component.InvokeAsync(typeof(MyViewComponent))

With RouteData使用 RouteData

@page "{id}"
@using MyProject.ViewComponents

@await Component.InvokeAsync(typeof(MyViewComponent), RouteData.Values["id"])

From Controller从 Controller

[HttpGet]
public async Task<IActionResult> Get()
{
    var html = await _renderViewComponentService
        .RenderViewComponentToStringAsync<MyViewComponent>();

    // do something with the html

    return Ok(new { html });
}

With FromRoute使用 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 });
}

The strange奇怪的

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的虚假ActionContextViewContext ,但它不用于任何事情! 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.NullViewInternal的,所以我们基本上必须将其复制/粘贴到我们自己的代码中。

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM