将 Razor 页面渲染为字符串

Posted

技术标签:

【中文标题】将 Razor 页面渲染为字符串【英文标题】:Render a Razor Page to string 【发布时间】:2018-03-14 09:37:14 【问题描述】:

问题:

我需要将 Razor 页面呈现为字符串的一部分。

为什么我想要这个:

我想创建一个控制器动作,它使用包含部分视图和其他可选参数的 JSON 进行响应。

尝试:

我熟悉以下将 View 呈现为字符串的示例:https://github.com/aspnet/Entropy/blob/dev/samples/Mvc.RenderViewToString/RazorViewToStringRenderer.cs

但是,它与 Pages 不兼容,因为它只在 Views 目录中搜索,所以即使我给它一个绝对路径,它也会试图找到我的 _Layout.cshtml(它甚至不应该这样做! ) 却找不到。

我尝试对其进行修改以使其呈现页面,但在尝试呈现它时,我最终在我的部分中获得了 ViewData 的 NullReferenceException。我怀疑它与 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());
        
    

【问题讨论】:

确保 Razor 页面中没有“@page”指令,然后重试。 如果您正在编译控制台应用程序。视图 .cshtml 必须在物理上与 exe 位于同一目录中。我遇到了另一个问题,即全局 .net 程序集未注册。 【参考方案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 为 null,因为在设置 PageContext 时设置了 Page 的 ViewData,因此如果未设置,则 ViewData 为 null。

我也发现我不得不打电话

_activator.Activate(page, viewContext);

为了一切顺利。这尚未经过全面测试,因此可能不适用于所有场景,但应该可以帮助您入门。

【讨论】:

干得好,谢谢。呈现的 HTML 仅包含部分视图,似乎缺少 _ViewStart,因此结果中没有 Layout。我正在努力让 _ViewStart 正常工作。 (参见 PageContext.ViewStartFactories 属性) 如果有人使用 net-core 2.2 并希望使用这种方法添加到您的 startup.cs services.TryAddSingleton() @Sven 您是否设法让 _ViewStart 工作并包含布​​局字符串? 这工作除了在页面上我有这个:输入参考并且它使用href的空白值呈现,而如果它在我在浏览器中查看的页面上它工作正常..知道如何解决这个问题吗?【参考方案2】:

如果你像我一样没有从_httpContext.HttpContext 得到GetRouteData() 并且_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

【讨论】:

没有额外 DI 的好方法。适用于net-core 2.2 如何呈现整个页面,而不仅仅是视图?【参考方案3】:

我遇到了同样的问题。

我查看了 RazorViewEngine 源代码,发现页面 使用“页面”路由数据搜索:

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

它在 routeData 中使用完整路径“/Folder/MyPage”,在 GetPage 调用中使用页面名称“MyPage”。

【讨论】:

【参考方案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";
            
        
    

我知道这不使用依赖注入,但是很简单。它只是工作。并且适用于任何页面,无论它是如何托管的。该页面是否在您的应用程序内部甚至外部。

【讨论】:

这只是从网页 url 中提取 html。您无需调用您的班级剃须刀页面。问题是如何在自己的代码中从 razor 模板生成 html。 是的,它可以是任何页面,但碰巧我正在使用 Razor 页面。不过,这是一个很好的观点。可以比剃须刀页面更广泛地使用。

以上是关于将 Razor 页面渲染为字符串的主要内容,如果未能解决你的问题,请参考以下文章

将渲染的 Razor 视图另存为 HTML 字符串

在 ASP.NET Core 中将 Razor 视图渲染为字符串

首次渲染时 Blazor 组件引用为空

Razor 中的@rendersection

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

ASP.NET MVC Razor 无编码渲染