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

34

我在我的MVC 6项目中使用RazorEngine来解析模板,代码如下:

Engine.Razor.RunCompile(File.ReadAllText(fullTemplateFilePath), templateName, null, model);

在 beta 6 版本中运行良好,在升级至 beta 7 后出现以下错误:

MissingMethodException: Method not found: "Void Microsoft.AspNet.Razor.CodeGenerators.GeneratedClassContext.set_ResolveUrlMethodName(System.String)". in RazorEngine.Compilation.CompilerServiceBase.CreateHost(Type templateType, Type modelType, String className)

这是 global.json 文件内容:

{
  "projects": [ "src", "test" ],
  "sdk": {
    "version": "1.0.0-beta7",
    "runtime": "clr",
    "architecture": "x64"
  }
}

这是 project.json 文件:

...
"dependencies": {
    "EntityFramework.SqlServer": "7.0.0-beta7",
    "EntityFramework.Commands": "7.0.0-beta7",
    "Microsoft.AspNet.Mvc": "6.0.0-beta7",
    "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta7",
    "Microsoft.AspNet.Authentication.Cookies": "1.0.0-beta7",
    "Microsoft.AspNet.Authentication.Facebook": "1.0.0-beta7",
    "Microsoft.AspNet.Authentication.Google": "1.0.0-beta7",
    "Microsoft.AspNet.Authentication.MicrosoftAccount": "1.0.0-beta7",
    "Microsoft.AspNet.Authentication.Twitter": "1.0.0-beta7",
    "Microsoft.AspNet.Diagnostics": "1.0.0-beta7",
    "Microsoft.AspNet.Diagnostics.Entity": "7.0.0-beta7",
    "Microsoft.AspNet.Identity.EntityFramework": "3.0.0-beta7",
    "Microsoft.AspNet.Server.IIS": "1.0.0-beta7",
    "Microsoft.AspNet.Server.WebListener": "1.0.0-beta7",
    "Microsoft.AspNet.StaticFiles": "1.0.0-beta7",
    "Microsoft.AspNet.Tooling.Razor": "1.0.0-beta7",
    "Microsoft.Framework.Configuration.Abstractions": "1.0.0-beta7",
    "Microsoft.Framework.Configuration.Json": "1.0.0-beta7",
    "Microsoft.Framework.Configuration.UserSecrets": "1.0.0-beta7",
    "Microsoft.Framework.Logging": "1.0.0-beta7",
    "Microsoft.Framework.Logging.Console": "1.0.0-beta7",
    "Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.0.0-beta7",
    "RazorEngine": "4.2.2-beta1"
  },
...
  "frameworks": {
    "dnx451": { }
  },
...

我的模板是:

@model dynamic
@{
    Layout = null;
}

<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title>Registration</title>
</head>
<body>
<p>
    Hello, @Model
</p>
</body>
</html>

有人有类似的问题吗?MVC 6 中还有另一种解析模板的方式吗?


我没有看问题,但是为答案投票了 :) - Tân
8个回答

29

2016年7月更新

在以下版本1.0.0, RC2中工作正常


针对aspnetcore RC2的用户,这段代码可能会对你有所帮助:

  • 创建一个独立的服务,这样你就可以在非控制器环境下使用它,例如从命令行或队列运行器等...
  • Startup类中将此服务注册到您的IoC容器中

https://gist.github.com/ahmad-moussawi/1643d703c11699a6a4046e57247b4d09

用法

// using a Model
string html = view.Render("Emails/Test", new Product("Apple"));

// using a Dictionary<string, object>
var viewData = new Dictionary<string, object>();
viewData["Name"] = "123456";

string html = view.Render("Emails/Test", viewData);

注意事项

Razor中的链接以相对路径的形式呈现,因此在外部视图(如电子邮件等)上不起作用。

目前,我会在控制器上生成链接并通过ViewModel将其传递给视图。

来源

这个源码是从(感谢 @pholly)提取出来的:https://github.com/aspnet/Entropy/blob/dev/samples/Mvc.RenderViewToString/RazorViewToStringRenderer.cs)


6
要点是RazorViewToStringRenderer.cs代码与https://github.com/aspnet/Mvc/issues/3091有关,完全相同。 - Philip Holly
我在视图中使用链接标签助手时遇到了问题,无法使其正常工作:<a asp-controller="Whatever" asp-action="DoSomething" asp-route-thingy="ThingyValue">链接文本</a> - Ben Collins
@BenCollins,我猜测你得到的是错误的URL(相对URL),这在像电子邮件这样的外部链接中无法使用,是吗? - amd
@Ahmad 不是的,问题在于链接 taghelper 需要更多上下文才能正常工作。链接 taghelper 依赖于 UrlHelper,而 UrlHelper 需要真实的路由数据,否则会抛出异常。幸运的是,我正在 MVC 应用程序中进行此操作,因此我可以使用 IHttpContextAccessor 来构建具有真实值的 ActionContext - Ben Collins
是的,我认为这也是我的问题,因为我需要向IUrlHelper添加一个扩展方法AbsoluteAction,很高兴你找到了解决方案 :) - amd
如果有人遇到找不到视图的问题,请确保在project.json文件中包含以下代码:"copyToOutput": { "include": "Views" } - sunil

22
我找到了这个讨论它的帖子:https://github.com/aspnet/Mvc/issues/3091 帖子中有人创建了一个示例服务:https://github.com/aspnet/Entropy/blob/dev/samples/Mvc.RenderViewToString/RazorViewToStringRenderer.cs 经过试验,我成功地将服务缩减为仅需要有效的HttpContextViewEngine,并添加了一个不需要模型的重载。视图是相对于您的应用程序根目录的(它们不必位于Views文件夹中)。
您需要在Startup.cs中注册该服务,并注册HttpContextAccessor
//Startup.cs ConfigureServices()
services.AddTransient<ViewRenderService>();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using System;
using System.IO;

namespace LibraryApi.Services
{
    public class ViewRenderService
    {
        IRazorViewEngine _viewEngine;
        IHttpContextAccessor _httpContextAccessor;

        public ViewRenderService(IRazorViewEngine viewEngine, IHttpContextAccessor httpContextAccessor)
        {
            _viewEngine = viewEngine;
            _httpContextAccessor = httpContextAccessor;
        }

        public string Render(string viewPath)
        {
            return Render(viewPath, string.Empty);
        }

        public string Render<TModel>(string viewPath, TModel model)
        {
            var viewEngineResult = _viewEngine.GetView("~/", viewPath, false);

            if (!viewEngineResult.Success)
            {
                throw new InvalidOperationException($"Couldn't find view {viewPath}");
            }

            var view = viewEngineResult.View;

            using (var output = new StringWriter())
            {
                var viewContext = new ViewContext();
                viewContext.HttpContext = _httpContextAccessor.HttpContext;
                viewContext.ViewData = new ViewDataDictionary<TModel>(new EmptyModelMetadataProvider(), new ModelStateDictionary())
                { Model = model };
                viewContext.Writer = output;

                view.RenderAsync(viewContext).GetAwaiter().GetResult();

                return output.ToString();
            }
        }
    }
}

示例使用:

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using LibraryApi.Services;
using System.Dynamic;

namespace LibraryApi.Controllers
{
    [Route("api/[controller]")]
    public class ValuesController : Controller
    {
        ILogger<ValuesController> _logger;
        ViewRenderService _viewRender;
        public ValuesController(ILogger<ValuesController> logger, ViewRenderService viewRender)
        {
            _logger = logger;
            _viewRender = viewRender;
        }

        // GET api/values
        [HttpGet]
        public string Get()
        {
            //ViewModel is of type dynamic - just for testing
            dynamic x = new ExpandoObject();
            x.Test = "Yes";
            var viewWithViewModel = _viewRender.Render("eNotify/Confirm.cshtml", x);
            var viewWithoutViewModel = _viewRender.Render("MyFeature/Test.cshtml");
            return viewWithViewModel + viewWithoutViewModel;
        }
    }
}

1
谢谢 - 在ASP.NET Core 1.0.1中表现非常出色。喜欢通用/服务方法 :) - Richard Logwood
@TommyBaggett 我的代码是基于这个链接的:https://github.com/aspnet/Entropy/blob/dev/samples/Mvc.RenderViewToString/RazorViewToStringRenderer.cs 所以我不能获得功劳 :) 我没有意识到 _Layout.cshtml 和其他文件不会被使用 - 或许需要一个 ActionContext 来加载它们?试试我提供的服务,看看是否可行。它使用了一个 ActionContext - Philip Holly
1
使用ASP.NET Core 1.1.0时,我遇到了一个问题,当我开始使用这个版本时,问题就消失了:https://github.com/aspnet/Entropy/blob/dev/samples/Mvc.RenderViewToString/RazorViewToStringRenderer.cs。在采用示例中的方法之前,我的错误调用堆栈与此非常相似:https://github.com/aspnet/Mvc/issues/5505。 - Richard Logwood
谢谢Richard Logwood。我遇到了同样的问题——尝试使用我的视图作为电子邮件内容——并继续使用了您链接中的示例。 - Vasily Hall
你将 IHttpContextAccessor 注册为单例有什么特别的原因吗? - grokky
显示剩余3条评论

15
过去,我曾在类库中使用 RazorEngine,因为我的目标是从这个类库中呈现模板。
据我所知,您似乎在 MVC 6.0 项目中,那么为什么不使用 RenderPartialViewToString() 方法,而不必添加对 RazorEngine 的依赖呢?
请记住,我只是出于好奇才问的。
例如,为了演示目的,在 VS2015 中,我创建了一个新的 ASP.NET Web 应用程序,并从 ASP.NET 5 预览模板中选择了 Web 应用程序模板。
ViewModels 文件夹内,我创建了一个 PersonViewModel
public class PersonViewModel
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string FullName
    {
        get
        {
            return string.Format("{0} {1}", this.FirstName, this.LastName);
        }
    } 
}

我随后创建了一个新的BaseController并添加了一个RenderPartialViewToString()方法:
public string RenderPartialViewToString(string viewName, object model)
{
    if (string.IsNullOrEmpty(viewName))
        viewName = ActionContext.ActionDescriptor.Name;

    ViewData.Model = model;

    using (StringWriter sw = new StringWriter())
    {
        var engine = Resolver.GetService(typeof(ICompositeViewEngine)) as ICompositeViewEngine;
        ViewEngineResult viewResult = engine.FindPartialView(ActionContext, viewName);

        ViewContext viewContext = new ViewContext(ActionContext, viewResult.View, ViewData, TempData, sw,new HtmlHelperOptions());

        var t = viewResult.View.RenderAsync(viewContext);
        t.Wait();

        return sw.GetStringBuilder().ToString();
    }
}

感谢 @DavidG 提供了 method 的方法

Views-->Shared 文件夹中,我创建了一个新的 Templates 文件夹,在其中添加了一个简单的 RegistrationTemplate.cshtml 视图,该视图是强类型的,与我的 PersonViewModel 相关联:

@model MyWebProject.ViewModels.PersonViewModel
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title>Registration</title>
</head>
<body>
    <p>
        Hello, @Model.FullName
    </p>
</body>
</html>

最后一步是让我的Controller继承自我的BaseController
public class MyController : BaseController

并创建类似于:

public IActionResult Index()
{
    var model = new PersonViewModel();
    model.FirstName = "Frank";
    model.LastName = "Underwood";
    var emailbody = base.RenderPartialViewToString("Templates/RegistrationTemplate", model);

    return View();
}

当然,上面的示例是无用的,因为我没有对变量emailbody做任何操作,但是这个示例的目的是展示它的使用方法。
此时,我可以(例如)调用一个EmailService并传递emailbody
_emailService.SendEmailAsync("test@test.com", "registration", emailbody);

我不确定这是否是你当前任务的合适替代方案。

如何在类库中执行相同操作?那里没有ActionContext... - Skorunka František
上面的例子展示了如何将一个视图渲染成字符串。该例子位于一个asp.net MVC应用程序中,其中所有的依赖项都已经存在。如果您计划在类库中使用类似的东西,则上述方法可能不是最好的,因为您将没有所有必要的依赖项等等...为此,您可能需要尝试另一种方法,例如:http://www.strathweb.com/2016/01/re-using-external-view-components-in-asp-net-5-asp-net-mvc-6/ - Vlince

13

今天我完成了一个可以解决你问题的库。你可以在ASP.NET之外使用它,因为它不依赖于它。

例如:

string content = "Hello @Model.Name. Welcome to @Model.Title repository";

var model = new
{
  Name = "John Doe",
  Title = "RazorLight"
};

var engine = new RazorLightEngine();
string result = engine.ParseString(content, model);

//Output: Hello John Doe, Welcome to RazorLight repository

更多: https://github.com/toddams/RazorLight


2
感谢您分享RazorLight库。这里提供的一些其他解决方案只适用于解析单个视图文件。当我需要支持考虑_ViewStart和_Layout文件时,我转向了您的库。您的库非常适合我的需求。 - Tommy Baggett
1
我曾经尝试了其他的解决方案,但都没有奏效。但这个方法非常好用!! - JCPhlux
@toddams,你能帮我解决我提出的问题吗?我们正在尝试使用生成动态模板的库来处理动态模型的问题。问题链接:https://github.com/toddams/RazorLight/issues/158 - inan
很棒的解决方案。干杯。 - Greg R Taylor

4
为了改进@vlince的答案(它对我来说无法直接使用),这是我所做的:
1- 创建一个基本控制器,你的其他控制器将继承该控制器
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.Rendering;
using System.IO;

namespace YourNameSpace
{
    public class BaseController : Controller
    {
        protected ICompositeViewEngine viewEngine;

        public BaseController(ICompositeViewEngine viewEngine)
        {
            this.viewEngine = viewEngine;
        }

        protected string RenderViewAsString(object model, string viewName = null)
        {
            viewName = viewName ?? ControllerContext.ActionDescriptor.ActionName;
            ViewData.Model = model;

            using (StringWriter sw = new StringWriter())
            {
                IView view = viewEngine.FindView(ControllerContext, viewName, true).View;
                ViewContext viewContext = new ViewContext(ControllerContext, view, ViewData, TempData, sw, new HtmlHelperOptions());

                view.RenderAsync(viewContext).Wait();

                return sw.GetStringBuilder().ToString();
            }
        }
    }
}

2- 继承基本控制器并调用方法

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewEngines;

namespace YourNameSpace
{
    public class YourController : BaseController
    {
        public YourController(ICompositeViewEngine viewEngine) : base(viewEngine) { }

        public string Index(int? id)
        {
            var model = new MyModel { Name = "My Name" };

            return RenderViewAsString(model);
        }
    }
}

谢谢 - 在ASP.NET Core 1.0.1中运行得非常好。 - Richard Logwood
在这个例子中,TutorialController实际上应该是YourController吗?否则,非常感谢您提供的这个想法。我已经从我的旧ASP.NET表单时代遗忘了这种技术,或多或少地使用了BaseForm :) - Newclique

3

ResolveUrlMethodName已被移除。因此,在您的CreateHost这里,您正在尝试设置一个不存在的属性:)。

我们决定将~/处理从核心Razor移动到Microsoft.AspNet.Mvc.Razor程序集中实现的TagHelper。这是删除该方法的提交的位。

希望这可以帮到您。


感谢详细的解释!将来我会查看它。 - hcp

2
这里提供了一种仅使用ASP.NET Core、无需外部库和反射的备选解决方案,与之相关的IT技术。请参考此链接:https://weblogs.asp.net/ricardoperes/getting-html-for-a-viewresult-in-asp-net-core。你只需要一个ViewResult和一个HttpContext
其思路是获取ViewResult并调用某个方法(例如ToHtml)来获取呈现的输出结果。这个方法可以像这样:
public static class ViewResultExtensions {
    public static string ToHtml(this ViewResult result, HttpContext httpContext) {            
        var feature = httpContext.Features.Get<IRoutingFeature>();
        var routeData = feature.RouteData;
        var viewName = result.ViewName ?? routeData.Values["action"] as string;
        var actionContext = new ActionContext(httpContext, routeData, new ControllerActionDescriptor());
        var options = httpContext.RequestServices.GetRequiredService<IOptions<MvcViewOptions>>();
        var htmlHelperOptions = options.Value.HtmlHelperOptions;
        var viewEngineResult = result.ViewEngine?.FindView(actionContext, viewName, true) ?? options.Value.ViewEngines.Select(x => x.FindView(actionContext, viewName, true)).FirstOrDefault(x => x != null);
        var view = viewEngineResult.View;
        var builder = new StringBuilder();

        using (var output = new StringWriter(builder)) {
            var viewContext = new ViewContext(actionContext, view, result.ViewData, result.TempData, output, htmlHelperOptions);

            view
                .RenderAsync(viewContext)
                .GetAwaiter()
                .GetResult();
        }
        return builder.ToString();
    }
}

使用它只需执行以下操作:

var view = this.View(“ViewName”);
var html = view.ToHtml();

2

将部分视图转换为字符串响应的扩展方法。

public static class PartialViewToString
{
    public static async Task<string> ToString(this PartialViewResult partialView, ActionContext actionContext)
    {
        using(var writer = new StringWriter())
        {
            var services = actionContext.HttpContext.RequestServices;
            var executor = services.GetRequiredService<PartialViewResultExecutor>();
            var view = executor.FindView(actionContext, partialView).View;
            var viewContext = new ViewContext(actionContext, view, partialView.ViewData, partialView.TempData, writer, new HtmlHelperOptions());
            await view.RenderAsync(viewContext);
            return writer.ToString();
        }
    }
}

在您的控制器操作中使用。

public async Task<IActionResult> Index()
{
    return await PartialView().ToString(ControllerContext)
}

.NET 5 实现

public static async Task<string> ViewToString(this PartialViewResult partialView, Controller controller)
    {
        using (var writer = new StringWriter())
        {
            var services = controller.ControllerContext.HttpContext.RequestServices;
            var viewEngine = services.GetService(typeof(ICompositeViewEngine)) as ICompositeViewEngine;
            var viewName = partialView.ViewName ?? controller.ControllerContext.ActionDescriptor.ActionName;
            var view = viewEngine.FindView(controller.ControllerContext, viewName, false).View;
            var viewContext = new ViewContext(controller.ControllerContext, view, partialView.ViewData, partialView.TempData, writer, new HtmlHelperOptions());
            await view.RenderAsync(viewContext);
            return writer.ToString();
        }
    }

这个在.NET 5上好像不再起作用了。我无法弄清楚如何注册PartialViewResultExecutor :( “未注册类型为'Microsoft.AspNetCore.Mvc.ViewFeatures.PartialViewResultExecutor'的服务。” - Fabrício Murta

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接