Отобразить страницу Razor в строку

Проблема:

Мне нужно сделать страницу Razor частичной для строки.

Почему я хочу это:

Я хочу создать действие контроллера, которое отвечает с помощью JSON, содержащего частичный вид и другие необязательные параметры.

Попытки:

Я знаком со следующим примером, который отображает представление в строку: https://github.com/aspnet/Entropy/blob/dev/samples/Mvc.RenderViewToString/RazorViewToStringRenderer.cs

Однако он несовместим с Pages, поскольку он ищет только в каталоге представлений, поэтому, даже если я дам ему абсолютный путь к части, он пытается найти мой _Layout.cshtml(чего он даже не должен делать! ) и не находит его.

Я попытался изменить его, чтобы он отображал страницы, но в конечном итоге я получаю исключение NullReferenceException для ViewData в частичном при попытке его визуализации. Я подозреваю, что это связано с NullView, но я понятия не имею, что туда помещать (конструктор для RazorView требует много объектов, которые я не знаю, как правильно).

Код:

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0: https://www.apache.org/licenses/LICENSE-2.0
// Modified by OronDF343: Uses pages instead of views.

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.ViewFeatures;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using Microsoft.AspNetCore.Routing;

namespace TestAspNetCore.Services
{
    public class RazorPageToStringRenderer
    {
        private readonly IRazorViewEngine _viewEngine;
        private readonly ITempDataProvider _tempDataProvider;
        private readonly IServiceProvider _serviceProvider;

        public RazorPageToStringRenderer(
            IRazorViewEngine viewEngine,
            ITempDataProvider tempDataProvider,
            IServiceProvider serviceProvider)
        {
            _viewEngine = viewEngine;
            _tempDataProvider = tempDataProvider;
            _serviceProvider = serviceProvider;
        }

        public async Task<string> RenderPageToStringAsync<TModel>(string viewName, TModel model)
        {
            var actionContext = GetActionContext();
            var page = FindPage(actionContext, viewName);

            using (var output = new StringWriter())
            {
                var viewContext = new ViewContext(actionContext,
                                                  new NullView(),
                                                  new ViewDataDictionary<TModel>(new EmptyModelMetadataProvider(),
                                                                                 new ModelStateDictionary())
                                                  {
                                                      Model = model
                                                  },
                                                  new TempDataDictionary(actionContext.HttpContext,
                                                                         _tempDataProvider),
                                                  output,
                                                  new HtmlHelperOptions());

                page.ViewContext = viewContext;
                await page.ExecuteAsync();

                return output.ToString();
            }
        }

        private IRazorPage FindPage(ActionContext actionContext, string pageName)
        {
            var getPageResult = _viewEngine.GetPage(executingFilePath: null, pagePath: pageName);
            if (getPageResult.Page != null)
            {
                return getPageResult.Page;
            }

            var findPageResult = _viewEngine.FindPage(actionContext, pageName);
            if (findPageResult.Page != null)
            {
                return findPageResult.Page;
            }

            var searchedLocations = getPageResult.SearchedLocations.Concat(findPageResult.SearchedLocations);
            var errorMessage = string.Join(
                Environment.NewLine,
                new[] { $"Unable to find page '{pageName}'. 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());
        }
    }
}

Ответ 1

Вот как я это сделал.

Как всегда регистрировать Сервис в Startup.cs

services.AddScoped<IViewRenderService, ViewRenderService>();

Служба определяется следующим образом:

public interface IViewRenderService
{
    Task<string> RenderToStringAsync<T>(string viewName, T model) where T : PageModel;
}

public class ViewRenderService : IViewRenderService
{
    private readonly IRazorViewEngine _razorViewEngine;
    private readonly ITempDataProvider _tempDataProvider;
    private readonly IServiceProvider _serviceProvider;
    private readonly IHttpContextAccessor _httpContext;
    private readonly IActionContextAccessor _actionContext;
    private readonly IRazorPageActivator _activator;


    public ViewRenderService(IRazorViewEngine razorViewEngine,
        ITempDataProvider tempDataProvider,
        IServiceProvider serviceProvider,
        IHttpContextAccessor httpContext,
        IRazorPageActivator activator,
        IActionContextAccessor actionContext)
    {
        _razorViewEngine = razorViewEngine;
        _tempDataProvider = tempDataProvider;
        _serviceProvider = serviceProvider;

        _httpContext = httpContext;
        _actionContext = actionContext;
        _activator = activator;

    }


    public async Task<string> RenderToStringAsync<T>(string pageName, T model) where T : PageModel
    {


        var actionContext =
            new ActionContext(
                _httpContext.HttpContext,
                _httpContext.HttpContext.GetRouteData(),
                _actionContext.ActionContext.ActionDescriptor
            );

        using (var sw = new StringWriter())
        {
            var result = _razorViewEngine.FindPage(actionContext, pageName);

            if (result.Page == null)
            {
                throw new ArgumentNullException($"The page {pageName} cannot be found.");
            }

            var view = new RazorView(_razorViewEngine,
                _activator,
                new List<IRazorPage>(),
                result.Page,
                HtmlEncoder.Default,
                new DiagnosticListener("ViewRenderService"));


            var viewContext = new ViewContext(
                actionContext,
                view,
                new ViewDataDictionary<T>(new EmptyModelMetadataProvider(), new ModelStateDictionary())
                {
                    Model = model
                },
                new TempDataDictionary(
                    _httpContext.HttpContext,
                    _tempDataProvider
                ),
                sw,
                new HtmlHelperOptions()
            );


            var page = ((Page)result.Page);

            page.PageContext = new Microsoft.AspNetCore.Mvc.RazorPages.PageContext
            {
                ViewData = viewContext.ViewData

            };

            page.ViewContext = viewContext;


            _activator.Activate(page, viewContext);

            await page.ExecuteAsync();


            return sw.ToString();
        }
    }



}

Я называю это так

  emailView.Body = await this._viewRenderService.RenderToStringAsync("Email/ConfirmAccount", new Email.ConfirmAccountModel
                {
                    EmailView = emailView,
                });

"Email/ConfirmAccount" - это путь к моей странице Razor (под страницами). "ConfirmAccountModel" - это моя страница для этой страницы.

ViewData является нулевым, потому что ViewData для страницы задается, когда установлен параметр PageContext, поэтому, если это не установлено, ViewData имеет значение null.

Я также обнаружил, что мне нужно было позвонить

_activator.Activate(page, viewContext);

За все это работать. Это еще не полностью протестировано, поэтому может не работать для всех сценариев, но должно помочь вам приступить к работе.

Ответ 2

У меня была та же проблема.

Я заглянул в исходный код RazorViewEngine и узнал, что страница выполняется поиск с использованием данных маршрута "страница":

var routeData = new RouteData();
routeData.Values.Add("page", "/Folder/MyPage");

Он работает для меня с полным путем "/Folder/MyPage" в файле routeData и именем страницы "MyPage" в вызове GetPage.

Ответ 3

Если, как и я, вы не получите GetRouteData() из _httpContext.HttpContext и _actionContext будет нулевым, вы можете создать расширение:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text.Encodings.Web;
using System.Threading.Tasks;

namespace Utils
{
    public static class PageExtensions
    {
        public static async Task<string> RenderViewAsync(this PageModel pageModel, string pageName)
        {
            var actionContext = new ActionContext(
                pageModel.HttpContext,
                pageModel.RouteData,
                pageModel.PageContext.ActionDescriptor
            );

            using (var sw = new StringWriter())
            {
                IRazorViewEngine _razorViewEngine = pageModel.HttpContext.RequestServices.GetService(typeof(IRazorViewEngine)) as IRazorViewEngine;
                IRazorPageActivator _activator = pageModel.HttpContext.RequestServices.GetService(typeof(IRazorPageActivator)) as IRazorPageActivator;

                var result = _razorViewEngine.FindPage(actionContext, pageName);

                if (result.Page == null)
                {
                    throw new ArgumentNullException($"The page {pageName} cannot be found.");
                }

                var page = result.Page;

                var view = new RazorView(_razorViewEngine,
                    _activator,
                    new List<IRazorPage>(),
                    page,
                    HtmlEncoder.Default,
                    new DiagnosticListener("ViewRenderService"));


                var viewContext = new ViewContext(
                    actionContext,
                    view,
                    pageModel.ViewData,
                    pageModel.TempData,
                    sw,
                    new HtmlHelperOptions()
                );


                var pageNormal = ((Page)result.Page);

                pageNormal.PageContext = pageModel.PageContext;

                pageNormal.ViewContext = viewContext;


                _activator.Activate(pageNormal, viewContext);

                await page.ExecuteAsync();

                return sw.ToString();
            }
        }
    }
}

Примечание. Этот код отображает только вызываемую страницу и пропускает макет.

Вам просто нужно позвонить с вашего PageModel так:

var s = this.RenderViewAsync("sendEmail").Result;

"sendEmail" - это имя вашего PageModel view, а путь - /Pages/sendEmail.cshtml.

Ответ 4

Вот маршрут, по которому я пошел. Очень просто и доставляет удовольствие...

using System;
using System.IO;
using System.Net;

namespace gMIS.Rendering
{
    public static class RazorPage
    {
        public static string RenderToString(string url)
        {
            try
            {
                //Grab page
                WebRequest request = WebRequest.Create(url);
                WebResponse response = request.GetResponse();
                Stream data = response.GetResponseStream();
                string html = String.Empty;
                using (StreamReader sr = new StreamReader(data))
                {
                    html = sr.ReadToEnd();
                }
                return html;
            }
            catch (Exception err)
            {
                return {Handle as you see fit};
            }
        }
    }
}

Вызывается как таковой....

var msg = RazorPage.RenderToString(url);

Пример:

var pathToRazorPageFolder = request.PathToRazorPageFolder();

var msg = RazorPage.RenderToString($"{pathToRazorPageFolder}/Task_Summary?userGuid={userGuid}&taskId={task.Task_ID}&includelink=true&linkuserGuid={linkUserGuid}");

В приведенном выше примере используется это расширение, которое я создал, чтобы получить базовый путь к моему приложению.

namespace Microsoft.AspNetCore.Http
{
    public static class RequestExtension
    {
        public static string PathToRazorPageFolder(this HttpRequest request)
        {
            if (request != null) {
                var requestPath = request.Path.ToString();
                var returnPathToFolder = request.Scheme + "://" + request.Host + requestPath.Substring(0, requestPath.LastIndexOf("/")); ;
                return returnPathToFolder;
            } else
            {
                return "HttpRequest was null";
            }
        }
    }
}

Я знаю, что это не использует Dependency Injection, но человек это просто. И это просто работает. И работает с любой страницей, независимо от того, как она размещена. Будь эта страница находиться внутри или даже за пределами вашего приложения.