如何将ASP.NET MVC视图呈现为字符串?

512

我想输出两个不同的视图(一个作为将发送电子邮件的字符串,另一个作为显示给用户的页面)。

在ASP.NET MVC Beta中是否可能实现?

我尝试了多个示例:

1. 在ASP.NET MVC Beta中呈现 RenderPartial 为字符串

如果我使用此示例,则会收到“无法在HTTP标头已发送后重定向。”的错误信息。

2. MVC框架:捕获视图的输出

如果我使用此示例,则似乎无法执行 redirectToAction,因为它试图呈现可能不存在的视图。如果我返回视图,则完全混乱,并且看起来一点也不正确。

是否有人对我遇到的这些问题有任何想法或解决方案,或者有更好的建议?

非常感谢!

下面是一个示例。 我正在尝试创建 GetViewForEmail 方法:

public ActionResult OrderResult(string ref)
{
    //Get the order
    Order order = OrderService.GetOrder(ref);

    //The email helper would do the meat and veg by getting the view as a string
    //Pass the control name (OrderResultEmail) and the model (order)
    string emailView = GetViewForEmail("OrderResultEmail", order);

    //Email the order out
    EmailHelper(order, emailView);
    return View("OrderResult", order);
}

Tim Scott的被接受的答案(稍加修改和格式化):

public virtual string RenderViewToString(
    ControllerContext controllerContext,
    string viewPath,
    string masterPath,
    ViewDataDictionary viewData,
    TempDataDictionary tempData)
{
    Stream filter = null;
    ViewPage viewPage = new ViewPage();

    //Right, create our view
    viewPage.ViewContext = new ViewContext(controllerContext, new WebFormView(viewPath, masterPath), viewData, tempData);

    //Get the response context, flush it and get the response filter.
    var response = viewPage.ViewContext.HttpContext.Response;
    response.Flush();
    var oldFilter = response.Filter;

    try
    {
        //Put a new filter into the response
        filter = new MemoryStream();
        response.Filter = filter;

        //Now render the view into the memorystream and flush the response
        viewPage.ViewContext.View.Render(viewPage.ViewContext, viewPage.ViewContext.HttpContext.Response.Output);
        response.Flush();

        //Now read the rendered view.
        filter.Position = 0;
        var reader = new StreamReader(filter, response.ContentEncoding);
        return reader.ReadToEnd();
    }
    finally
    {
        //Clean up.
        if (filter != null)
        {
            filter.Dispose();
        }

        //Now replace the response filter
        response.Filter = oldFilter;
    }
}

使用示例

假设从控制器调用以获取订单确认电子邮件并传递Site.Master位置。

string myString = RenderViewToString(this.ControllerContext, "~/Views/Order/OrderResultEmail.aspx", "~/Views/Shared/Site.Master", this.ViewData, this.TempData);

2
你如何在强类型视图中使用它?也就是说,我如何将模型传递给页面? - Kjensen
无法在发送头部后设置内容类型(因为Flush会将其发送),因此不能在此处使用并创建JsonResult。 - Arnis Lapsa
因为没有一个单一的正确答案,我想。 :) 我创建了一个特定于我的问题,但我知道它也会被广泛询问。 - Dan Atkinson
2
建议的解决方案在MVC 3中不起作用。 - Kasper Holdum
1
@Qua:建议的解决方案已经超过两年了。我也不指望它能在MVC 3上运行!此外,现在有更好的方法来做这件事。 - Dan Atkinson
@BishopBarber 谢谢。已修复! - Dan Atkinson
16个回答

592

这是我想到的方法,经过测试已经在我的代码中顺利运行。我在控制器基类中添加了以下方法(当然你也可以将这些静态方法放在其他地方,并接受一个控制器作为参数)。

MVC2 .ascx 风格

protected string RenderViewToString<T>(string viewPath, T model) {
  ViewData.Model = model;
  using (var writer = new StringWriter()) {
    var view = new WebFormView(ControllerContext, viewPath);
    var vdd = new ViewDataDictionary<T>(model);
    var viewCxt = new ViewContext(ControllerContext, view, vdd,
                                new TempDataDictionary(), writer);
    viewCxt.View.Render(viewCxt, writer);
    return writer.ToString();
  }
}

Razor .cshtml样式

public string RenderRazorViewToString(string viewName, object model)
{
  ViewData.Model = model;
  using (var sw = new StringWriter())
  {
    var viewResult = ViewEngines.Engines.FindPartialView(ControllerContext,
                                                             viewName);
    var viewContext = new ViewContext(ControllerContext, viewResult.View,
                                 ViewData, TempData, sw);
    viewResult.View.Render(viewContext, sw);
    viewResult.ViewEngine.ReleaseView(ControllerContext, viewResult.View);
    return sw.GetStringBuilder().ToString();
  }
}

编辑:添加了Razor代码。


34
将视图渲染为字符串在整个路由概念中总是“不一致的”,因为它与路由无关。我不确定为什么一个可行的答案会被点踩。 - Ben Lesh
4
我认为你可能需要在 Razor 版本的方法声明中移除 "static",否则它无法找到 ControllerContext 等内容。 - Mike
3
你需要实现自己的方法来去除多余的空格。我能想到的最好的方法是将字符串加载到XmlDocument中,然后使用XmlWriter将其写回字符串中,就像我在上一条评论中留下的链接所述。希望这能帮到你。 - Ben Lesh
3
我该如何使用WebApi控制器来实现这个?如果您有任何建议,将不胜感激。 - Alexander
3
大家好,如果想要在所有控制器中使用 "Static" 关键字使其通用,您需要创建一个静态类,并在其中放置这个方法,并将 "this" 作为参数传递给 "ControllerContext"。您可以在 https://dev59.com/yXRB5IYBdhLWcg3w4a8o#18978036 上查看它。 - Dilip Langhanoja
显示剩余22条评论

72

这个答案与我的想法不符。它最初来自于https://dev59.com/yXRB5IYBdhLWcg3w4a8o#2759898,但在这里,我展示了如何使用 "Static" 关键字使其对所有控制器普遍适用。

为此,您需要在类文件中创建 static 类。(假设您的类文件名为 Utils.cs)

这个示例是针对 Razor 的。

Utils.cs

public static class RazorViewToString
{
    public static string RenderRazorViewToString(this Controller controller, string viewName, object model)
    {
        controller.ViewData.Model = model;
        using (var sw = new StringWriter())
        {
            var viewResult = ViewEngines.Engines.FindPartialView(controller.ControllerContext, viewName);
            var viewContext = new ViewContext(controller.ControllerContext, viewResult.View, controller.ViewData, controller.TempData, sw);
            viewResult.View.Render(viewContext, sw);
            viewResult.ViewEngine.ReleaseView(controller.ControllerContext, viewResult.View);
            return sw.GetStringBuilder().ToString();
        }
    }
}
现在,您可以通过以下方式将命名空间作为参数传递给控制器,在控制器文件中添加名称空间,并从控制器中调用此类。
现在你可以通过将命名空间加入到你的控制器文件里面,然后通过传递“this”作为参数给控制器来调用这个类。
string result = RazorViewToString.RenderRazorViewToString(this ,"ViewName", model);

根据@Sergey提供的建议,这个扩展方法也可以从控制器中调用,如下所示

string result = this.RenderRazorViewToString("ViewName", model);

我希望这对您有用,使代码变得清晰整洁。


1
好的解决方案!还有一件事,RenderRazorViewToString 实际上是扩展方法(因为你使用 this 关键字传递了控制器参数),因此可以通过以下方式调用该扩展方法:this.RenderRazorViewToString("视图名称", 模型)。 - Sergey
@Sergey 嗯...让我这样检查一下,如果没问题的话,我会更新我的答案。无论如何,感谢您的建议。 - Dilip Langhanoja
无法解析符号“ViewEngines”。您知道如何解决这个错误吗? - Auroratic
为什么在渲染之前,jQuery的更改没有应用到视图中? - MOH3N
1
@Sergey 谢谢您的建议,我已经根据您的建议更新了我的答案,这是完全正确的。 - Dilip Langhanoja
显示剩余4条评论

34

这对我有效:

public virtual string RenderView(ViewContext viewContext)
{
    var response = viewContext.HttpContext.Response;
    response.Flush();
    var oldFilter = response.Filter;
    Stream filter = null;
    try
    {
        filter = new MemoryStream();
        response.Filter = filter;
        viewContext.View.Render(viewContext, viewContext.HttpContext.Response.Output);
        response.Flush();
        filter.Position = 0;
        var reader = new StreamReader(filter, response.ContentEncoding);
        return reader.ReadToEnd();
    }
    finally
    {
        if (filter != null)
        {
            filter.Dispose();
        }
        response.Filter = oldFilter;
    }
}

谢谢您的评论,但是那不是用于视图内部渲染吗?我如何在我更新问题的上下文中使用它呢? - Dan Atkinson
抱歉,我还在思考去年的Silverlight,其第一个rc版本号为0. :) 今天我会尝试一下。(一旦我解决了视图路径的正确格式问题) - NikolaiDante
这个问题在RC1版本仍然会导致重定向出错。 - defeated
失败了:不,它不会。如果是这样的话,那么你肯定做错了什么。 - Dan Atkinson
将此与https://dev59.com/enRB5IYBdhLWcg3wxZ1K#521183合并,添加了ViewEnginesCollection的意识,尝试弹出partialview并获得了https://dev59.com/enRB5IYBdhLWcg3wxZ1K#521183。:E - Arnis Lapsa
不幸的是,这会在 .ascx 中使用 RenderPartial 时出现问题。 - Simon_Weaver

32

我找到了一种新的解决方案,可以将视图渲染为字符串,而无需处理当前HttpContext的响应流(这不允许您更改响应的ContentType或其他标头)。

基本上,您需要创建一个虚假的HttpContext来渲染视图本身:

/// <summary>Renders a view to string.</summary>
public static string RenderViewToString(this Controller controller,
                                        string viewName, object viewData) {
    //Create memory writer
    var sb = new StringBuilder();
    var memWriter = new StringWriter(sb);

    //Create fake http context to render the view
    var fakeResponse = new HttpResponse(memWriter);
    var fakeContext = new HttpContext(HttpContext.Current.Request, fakeResponse);
    var fakeControllerContext = new ControllerContext(
        new HttpContextWrapper(fakeContext),
        controller.ControllerContext.RouteData,
        controller.ControllerContext.Controller);

    var oldContext = HttpContext.Current;
    HttpContext.Current = fakeContext;

    //Use HtmlHelper to render partial view to fake context
    var html = new HtmlHelper(new ViewContext(fakeControllerContext,
        new FakeView(), new ViewDataDictionary(), new TempDataDictionary()),
        new ViewPage());
    html.RenderPartial(viewName, viewData);

    //Restore context
    HttpContext.Current = oldContext;    

    //Flush memory and return output
    memWriter.Flush();
    return sb.ToString();
}

/// <summary>Fake IView implementation used to instantiate an HtmlHelper.</summary>
public class FakeView : IView {
    #region IView Members

    public void Render(ViewContext viewContext, System.IO.TextWriter writer) {
        throw new NotImplementedException();
    }

    #endregion
}

对于ASP.NET MVC 1.0,与ContentResult、JsonResult等一起使用时有效(更改原始HttpResponse的Headers不会引发“服务器无法在发送HTTP标头后设置内容类型”异常)。

更新:在ASP.NET MVC 2.0 RC中,代码略有变化,因为我们必须将用于编写视图的StringWriter传递到ViewContext中:

//...

//Use HtmlHelper to render partial view to fake context
var html = new HtmlHelper(
    new ViewContext(fakeControllerContext, new FakeView(),
        new ViewDataDictionary(), new TempDataDictionary(), memWriter),
    new ViewPage());
html.RenderPartial(viewName, viewData);

//...

HtmlHelper 对象上没有 RenderPartial 方法。 这是不可能的 - html.RenderPartial(viewName, viewData); - MartinF
1
在ASP.NET MVC 1.0版本中,有几个RenderPartial扩展方法。我特别使用的是System.Web.Mvc.Html.RenderPartialExtensions.RenderPartial(this HtmlHelper, string, object)。我不知道这个方法是否已经添加到最新的MVC修订版中,也不确定它是否存在于早期版本中。 - LorenzCK
只需要在using声明中添加System.Web.Mvc.Html命名空间(否则html.RenderPartial(..)将无法访问:) - MartinF
有人在MVC2的RC版本中使用过这个吗?他们在ViewContext中添加了一个额外的Textwriter参数。我尝试了添加一个新的StringWriter(),但没有起作用。 - beckelmw
1
@beckelmw:我更新了响应。你必须传入原始的 StringWriter,用于写入 StringBuilder,而不是一个新实例或视图的输出将会丢失。 - LorenzCK

14

本文介绍了如何在不同场景下将视图渲染为字符串:

  1. MVC控制器调用其自己的ActionMethod之一
  2. MVC控制器调用另一个MVC控制器的ActionMethod
  3. WebAPI控制器调用MVC控制器的ActionMethod

解决方案/代码以名为ViewRenderer的类提供。它是Rick Stahl在GitHub上的WestwindToolkit的一部分。

用法(3. - WebAPI示例):

string html = ViewRenderer.RenderView("~/Areas/ReportDetail/Views/ReportDetail/Index.cshtml", ReportVM.Create(id));

5
此外,作为NuGet包,West Wind Web MVC Utilities(https://www.nuget.org/packages/Westwind.Web.Mvc/)还可以充当视图渲染器,不仅可以渲染部分视图,还可以渲染包含布局的整个视图。附带代码的博客文章请参见:https://weblog.west-wind.com/posts/2012/May/30/Rendering-ASPNET-MVC-Views-to-String - Jeroen K
如果能将其分成更小的软件包就太好了。Nuget软件包会对web.config文件进行一系列修改并向您的项目添加js文件,但在卸载软件包时这些文件并不会被清除 :/ - Josh Noe
唉,所有这些都是针对有主机的情况... 对于像单元测试这样想要呈现视图(使用来自预定义模型的数据)而没有任何IIS或其他主机、浏览器或其他上下文的情况不合适。 - David V. Corbin

10
如果您想完全放弃MVC,从而避免所有HttpContext的混乱...
using RazorEngine;
using RazorEngine.Templating; // For extension methods.

string razorText = System.IO.File.ReadAllText(razorTemplateFileLocation);
string emailBody = Engine.Razor.RunCompile(razorText, "templateKey", typeof(Model), model);

这里使用了很棒的开源 Razor 引擎: https://github.com/Antaris/RazorEngine

不错!你知道是否有类似的解析引擎适用于WebForms语法吗?我仍然有一些旧的WebForms视图,暂时无法转移到Razor。 - Dan Atkinson
嗨,我在使用Razor引擎时遇到了很多问题,而且错误报告也不太友好。我认为Url助手也不受支持。 - Layinka
@Layinka 不确定这是否有帮助,但大多数错误信息都在异常的“CompilerErrors”属性中。 - Josh Noe

8

ASP NET CORE 的附加提示:

接口:

public interface IViewRenderer
{
  Task<string> RenderAsync<TModel>(Controller controller, string name, TModel model);
}

实现:

public class ViewRenderer : IViewRenderer
{
  private readonly IRazorViewEngine viewEngine;

  public ViewRenderer(IRazorViewEngine viewEngine) => this.viewEngine = viewEngine;

  public async Task<string> RenderAsync<TModel>(Controller controller, string name, TModel model)
  {
    ViewEngineResult viewEngineResult = this.viewEngine.FindView(controller.ControllerContext, name, false);

    if (!viewEngineResult.Success)
    {
      throw new InvalidOperationException(string.Format("Could not find view: {0}", name));
    }

    IView view = viewEngineResult.View;
    controller.ViewData.Model = model;

    await using var writer = new StringWriter();
    var viewContext = new ViewContext(
       controller.ControllerContext,
       view,
       controller.ViewData,
       controller.TempData,
       writer,
       new HtmlHelperOptions());

       await view.RenderAsync(viewContext);

       return writer.ToString();
  }
}

Startup.cs 中的注册

...
 services.AddSingleton<IViewRenderer, ViewRenderer>();
...

在控制器中的使用:

public MyController: Controller
{
  private readonly IViewRenderer renderer;
  public MyController(IViewRendere renderer) => this.renderer = renderer;
  public async Task<IActionResult> MyViewTest
  {
    var view = await this.renderer.RenderAsync(this, "MyView", model);
    return new OkObjectResult(view);
  }
}

巫术!我可能会提出另一个关于asp.net core的问题,所以这可以成为未来的答案(至少在.NET 12之前)。在.NET 6 Core MVC中完美运行。 - nitewulf50
太好了!我只需要将isMainPage更改为true,否则_ViewStart.cshtml不会被执行,代码如下: ViewEngineResult viewEngineResult = this.viewEngine.FindView(controller.ControllerContext, name, true); - Paolo Sanchi

5
您可以使用以下方法将视图转换为字符串:

使用以下方式可以获取字符串中的视图

protected string RenderPartialViewToString(string viewName, object model)
{
    if (string.IsNullOrEmpty(viewName))
        viewName = ControllerContext.RouteData.GetRequiredString("action");

    if (model != null)
        ViewData.Model = model;

    using (StringWriter sw = new StringWriter())
    {
        ViewEngineResult viewResult = ViewEngines.Engines.FindPartialView(ControllerContext, viewName);
        ViewContext viewContext = new ViewContext(ControllerContext, viewResult.View, ViewData, TempData, sw);
        viewResult.View.Render(viewContext, sw);

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

我们可以用两种方式调用这个方法。
string strView = RenderPartialViewToString("~/Views/Shared/_Header.cshtml", null)

或者

var model = new Person()
string strView = RenderPartialViewToString("~/Views/Shared/_Header.cshtml", model)

5
在服务层中将视图渲染为字符串,而无需传递ControllerContext,可以参考Rick Strahl的文章http://www.codemag.com/Article/1312081,该文章创建了一个通用控制器。以下是代码摘要:
// Some Static Class
public static string RenderViewToString(ControllerContext context, string viewPath, object model = null, bool partial = false)
{
    // first find the ViewEngine for this view
    ViewEngineResult viewEngineResult = null;
    if (partial)
        viewEngineResult = ViewEngines.Engines.FindPartialView(context, viewPath);
    else
        viewEngineResult = ViewEngines.Engines.FindView(context, viewPath, null);

    if (viewEngineResult == null)
        throw new FileNotFoundException("View cannot be found.");

    // get the view and attach the model to view data
    var view = viewEngineResult.View;
    context.Controller.ViewData.Model = model;

    string result = null;

    using (var sw = new StringWriter())
    {
        var ctx = new ViewContext(context, view, context.Controller.ViewData, context.Controller.TempData, sw);
        view.Render(ctx, sw);
        result = sw.ToString();
    }

    return result;
}

// In the Service Class
public class GenericController : Controller
{ }

public static T CreateController<T>(RouteData routeData = null) where T : Controller, new()
{
    // create a disconnected controller instance
    T controller = new T();

    // get context wrapper from HttpContext if available
    HttpContextBase wrapper;
    if (System.Web.HttpContext.Current != null)
        wrapper = new HttpContextWrapper(System.Web.HttpContext.Current);
    else
        throw new InvalidOperationException("Cannot create Controller Context if no active HttpContext instance is available.");

    if (routeData == null)
        routeData = new RouteData();

    // add the controller routing if not existing
    if (!routeData.Values.ContainsKey("controller") &&
        !routeData.Values.ContainsKey("Controller"))
        routeData.Values.Add("controller", controller.GetType().Name.ToLower().Replace("controller", ""));

    controller.ControllerContext = new ControllerContext(wrapper, routeData, controller);
    return controller;
}

然后在Service类中渲染View:

var stringView = RenderViewToString(CreateController<GenericController>().ControllerContext, "~/Path/To/View/Location/_viewName.cshtml", theViewModel, true);

ViewEngines在.NET 5.0中似乎不再正确。 - David V. Corbin
此代码针对的是.NET Framework,而不是Core。 - Oliver

3

我正在使用MVC 1.0 RTM,但以上解决方案都不适用于我。但是以下解决方案适用:

Public Function RenderView(ByVal viewContext As ViewContext) As String

    Dim html As String = ""

    Dim response As HttpResponse = HttpContext.Current.Response

    Using tempWriter As New System.IO.StringWriter()

        Dim privateMethod As MethodInfo = response.GetType().GetMethod("SwitchWriter", BindingFlags.NonPublic Or BindingFlags.Instance)

        Dim currentWriter As Object = privateMethod.Invoke(response, BindingFlags.NonPublic Or BindingFlags.Instance Or BindingFlags.InvokeMethod, Nothing, New Object() {tempWriter}, Nothing)

        Try
            viewContext.View.Render(viewContext, Nothing)
            html = tempWriter.ToString()
        Finally
            privateMethod.Invoke(response, BindingFlags.NonPublic Or BindingFlags.Instance Or BindingFlags.InvokeMethod, Nothing, New Object() {currentWriter}, Nothing)
        End Try

    End Using

    Return html

End Function

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