Программно отображать страницу Razor как строку HTML - PullRequest
0 голосов
/ 07 августа 2020

Цель

  • Я пытаюсь сгенерировать строку HTML на бэкэнде, поскольку хочу преобразовать ее в PDF с помощью библиотеки HtmlToPDF.
  • Я также хочу чтобы иметь возможность легко увидеть сгенерированный HTML в браузере для целей отладки / настройки. Страница будет опубликована c только тогда, когда IsDevelopment().
  • Я хочу, чтобы она была как можно проще.

Я использую ASP. NET Core 3.1

Подход

Razor Page

Я решил попробовать 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 строку

Я нашел Визуализировать страницу Razor в строку который, кажется, делает то, что я хочу.

Но здесь начинаются проблемы: (

Проблемы

Во-первых, мне очень странно, что когда вы находите через _razorViewEngine.FindPage он не знает, как заполнить ViewContext или Model. Я думаю, что задача IndexModel заключалась в том, чтобы заполнить их. Я надеялся, что можно будет спросить ASP. NET для страницы IndexModel, и все будет так.

В любом случае ... следующая проблема. Чтобы отобразить страницу, мне нужно вручную создать ViewContext, и я должен предоставить его с Model. Но страница i s Модель, и, поскольку это страница, это не просто ViewModel. Он полагается на DI и ожидает выполнения OnGetAsync() для заполнения модели. Это в значительной степени уловка-22.

Я также пробовал получить представление вместо страницы через _razorViewEngine.FindView, но для этого также требуется модель, поэтому мы вернулись к уловке-22.

Другой вопрос. Целью страницы отладки / настройки было легко увидеть, что было сгенерировано. Но если мне нужно создать Model вне IndexModel, тогда он больше не будет отражать то, что на самом деле где-то создается в какой-либо службе.

Все это заставляет меня задуматься, на правильном ли я пути . Или мне что-то не хватает?

Ответы [ 2 ]

2 голосов
/ 07 августа 2020

Чтобы преобразовать частичное представление в строку, выполните следующие действия:

  1. Добавьте интерфейс в папку Services с именем IRazorPartialToStringRenderer.cs.

     public interface IRazorPartialToStringRenderer
     {
         Task<string> RenderPartialToStringAsync<TModel>(string partialName, TModel model);
     }
    
  2. Добавьте файл класса C# в папку Services с именем 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. Зарегистрируйте службы в ConfigureServices метод в классе Startup:

     public void ConfigureServices(IServiceCollection services)
     {
         services.AddRazorPages(); 
         services.AddTransient<IRazorPartialToStringRenderer, RazorPartialToStringRenderer>();
     }
    
  4. Использование метода RenderPartialToStringAsyn c () для визуализации страницы 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
     }
    

Снимок экрана отладки, как показано ниже:

enter image description here

More detail steps, please check this blog Отрисовка частичного представления в строку .

0 голосов
/ 07 августа 2020

Мне удалось его взломать! В конце концов, я был на неправильном пути ... решение - использовать 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);
        }
    }
}

и представление помещается в Pages / Shared / Components / My / Default.cs html

@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))

With RouteData

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

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

From Controller

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

    // do something with the html

    return Ok(new { html });
}

With 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, поэтому мы должны скопировать / вставить его в наш собственный код: (

Возможно, в будущем он будет улучшен.

В любом случае ... ИМО, это проще, чем использовать IRazorViewEngine, которое появляется практически в каждом поиске Google:)

Надеюсь на это может кому-то помочь.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...