如何在Web表单中包含部分视图

82

我正在编写的某个网站同时使用了ASP.NET MVC和WebForms。

我有一个部分视图,想将其包含在一个WebForm中。该部分视图中有一些需要在服务器端处理的代码,因此使用Response.WriteFile无法正常工作。同时,需要确保在禁用JavaScript的情况下仍能正常使用。

我该怎么做呢?


我有同样的问题 - Html.RenderPartial 在WebForms上无法工作,但仍然应该有一种方法来解决这个问题。 - Keith
7个回答

101

我查看了MVC源代码,试图弄清楚如何做到这一点。控制器上下文、视图、视图数据、路由数据和HTML渲染方法之间似乎存在非常紧密的耦合关系。

基本上,为了实现这一点,您需要创建所有这些额外的元素。其中一些相对简单(例如视图数据),但有些则更加复杂——例如路由数据将考虑当前的WebForms页面被忽略。

最大的问题似乎是HttpContext——MVC页面依赖于HttpContextBase(而不是像WebForms那样使用HttpContext),虽然两者都实现了IServiceProvider,但它们并没有关联。MVC的设计者故意决定不改变传统的WebForms以使用新的上下文基类,但是他们提供了一个包装器。

这个包装器可以让您向WebForm添加一个部分视图:

public class WebFormController : Controller { }

public static class WebFormMVCUtil
{

    public static void RenderPartial( string partialName, object model )
    {
        //get a wrapper for the legacy WebForm context
        var httpCtx = new HttpContextWrapper( System.Web.HttpContext.Current );

        //create a mock route that points to the empty controller
        var rt = new RouteData();
        rt.Values.Add( "controller", "WebFormController" );

        //create a controller context for the route and http context
        var ctx = new ControllerContext( 
            new RequestContext( httpCtx, rt ), new WebFormController() );

        //find the partial view using the viewengine
        var view = ViewEngines.Engines.FindPartialView( ctx, partialName ).View;

        //create a view context and assign the model
        var vctx = new ViewContext( ctx, view, 
            new ViewDataDictionary { Model = model }, 
            new TempDataDictionary() );

        //render the partial view
        view.Render( vctx, System.Web.HttpContext.Current.Response.Output );
    }

}

然后在您的Web表单中,您可以这样做:

<% WebFormMVCUtil.RenderPartial( "ViewName", this.GetModel() ); %>

1
这在基本页面请求上可以工作,但是如果您在容器页面上进行任何后续操作,view.Render()将会出现“验证viewstate MAC失败...”异常。Keith,您能确认一下吗? - Kurt Schindler
如果您想知道为什么在MVC2及以上版本中无法编译,请参考Hilarius的答案。 - Krisztián Balla
@JennyO'Reilly 现在有更好的方法来完成这个任务了。WebForms现在支持与MVC合作,而2009年我写这篇文章时还缺乏这方面的支持。实际上,它只是一个用于MVC1和早期WebForms的垫片。 - Keith
1
也对使用新的更好的方法感兴趣。我正在使用这种方法在WebForms主页面中加载部分视图(耶,它起作用了!)当从主页面调用时,我无法获取控制器上下文,因此必须新建一个。 - Pat James
这个示例对我来说几乎可以直接使用。ViewContext()需要一个额外的参数,即httpCtx。 - Joel Hansen
显示剩余2条评论

41
经过一段时间的探索,我找到了一个很好的解决方案。Keith的解决方案适用于许多人,但在某些情况下并不是最佳选择,因为有时候您希望应用程序通过控制器的流程来渲染视图,而Keith的解决方案只是使用给定的模型呈现视图。我在这里提供一个新的解决方案,它将运行正常的流程。
通用步骤:
1. 创建一个实用类。 2. 创建一个虚拟控制器和一个虚拟视图。 3. 在您的aspx或master page中调用实用方法以部分渲染传递Controller、View和(如果需要)要呈现的模型(作为对象)。
让我们在这个例子中仔细看一下:
1. 创建一个名为MVCUtility的类,并创建以下方法:
    //Render a partial view, like Keith's solution
    private static void RenderPartial(string partialViewName, object model)
    {
        HttpContextBase httpContextBase = new HttpContextWrapper(HttpContext.Current);
        RouteData routeData = new RouteData();
        routeData.Values.Add("controller", "Dummy");
        ControllerContext controllerContext = new ControllerContext(new RequestContext(httpContextBase, routeData), new DummyController());
        IView view = FindPartialView(controllerContext, partialViewName);
        ViewContext viewContext = new ViewContext(controllerContext, view, new ViewDataDictionary { Model = model }, new TempDataDictionary(), httpContextBase.Response.Output);
        view.Render(viewContext, httpContextBase.Response.Output);
    }

    //Find the view, if not throw an exception
    private static IView FindPartialView(ControllerContext controllerContext, string partialViewName)
    {
        ViewEngineResult result = ViewEngines.Engines.FindPartialView(controllerContext, partialViewName);
        if (result.View != null)
        {
            return result.View;
        }
        StringBuilder locationsText = new StringBuilder();
        foreach (string location in result.SearchedLocations)
        {
            locationsText.AppendLine();
            locationsText.Append(location);
        }
        throw new InvalidOperationException(String.Format("Partial view {0} not found. Locations Searched: {1}", partialViewName, locationsText));
    }       

    //Here the method that will be called from MasterPage or Aspx
    public static void RenderAction(string controllerName, string actionName, object routeValues)
    {
        RenderPartial("PartialRender", new RenderActionViewModel() { ControllerName = controllerName, ActionName = actionName, RouteValues = routeValues });
    }

创建一个参数传递类,我会在这里调用它 RendeActionViewModel(可以在 MvcUtility Class 的同一文件中创建)
    public class RenderActionViewModel
    {
        public string ControllerName { get; set; }
        public string ActionName { get; set; }
        public object RouteValues { get; set; }
    }

2) 现在创建一个名为 DummyController 的控制器。

    //Here the Dummy controller with Dummy view
    public class DummyController : Controller
    {
      public ActionResult PartialRender()
      {
          return PartialView();
      }
    }

创建一个名为 PartialRender.cshtml 的虚拟视图(Razor视图),并将其添加到 DummyController 中,内容如下。请注意,它将使用Html帮助程序执行另一个Render操作。
(Note: 源文中的"Dummy view"指的是“虚拟视图”,而非“假视图”或“无用视图”)
@model Portal.MVC.MvcUtility.RenderActionViewModel
@{Html.RenderAction(Model.ActionName, Model.ControllerName, Model.RouteValues);}

3) 现在把这个内容放入您的MasterPageaspx文件中,来部分渲染您需要的视图。请注意,当您想将多个razor视图与您的MasterPageaspx页面混合时,这是一个很好的答案(假设我们有一个名为“Login”的PartialView用于Home控制器)。

    <% MyApplication.MvcUtility.RenderAction("Home", "Login", new { }); %>

如果您有一个可以传递到操作中的模型

    <% MyApplication.MvcUtility.RenderAction("Home", "Login", new { Name="Daniel", Age = 30 }); %>

这个解决方案很好,不使用ajax调用,这样不会造成嵌套视图的延迟渲染不会创建新的WebRequest,因此不会创建新的会话,并且将处理检索所需ActionResult的方法以获取你想要的视图,无需传递任何模型感谢在Web表单中使用MVC RenderAction

1
我尝试了这篇帖子中的所有其他解决方案,而这个答案是迄今为止最好的。我建议任何人首先尝试这个解决方案。 - Halcyon
嗨,丹尼尔。你能帮我吗?我按照你的解决方案进行了操作,但卡在了一个地方。我已经在http://stackoverflow.com/questions/38241661/showing-a-razor-view-in-an-web-form-iframe下提出了问题。 - Karthik Venkatraman
这绝对是我在SO上见过的最好的答案之一。非常感谢。 - FrenkyB
这对我来说似乎是一个很好的解决方案,乍一看它似乎可以工作,dummyController和视图被调用,我的控制器和部分视图也被调用,但是当在我的aspx中传递<% MyApplication.MvcUtility.RenderAction("Home", "Login", new { }); %>行时,请求就会立即结束,因此页面的其余部分不会呈现。有人经历过这种行为并知道如何解决吗? - hsop

20

最明显的方法是通过 AJAX。

可以像这样实现(使用 jQuery):

<div id="mvcpartial"></div>

<script type="text/javascript">
$(document).load(function () {
    $.ajax(
    {    
        type: "GET",
        url : "urltoyourmvcaction",
        success : function (msg) { $("#mvcpartial").html(msg); }
    });
});
</script>

9
我的回复后面加了个“)-:” - Alexander Taran

11

太好了,谢谢!

我正在使用.NET 4上的MVC 2,它要求将TextWriter传递到ViewContext中,因此您必须像下面所示传递httpContextWrapper.Response.Output。

    public static void RenderPartial(String partialName, Object model)
    {
        // get a wrapper for the legacy WebForm context
        var httpContextWrapper = new HttpContextWrapper(HttpContext.Current);

        // create a mock route that points to the empty controller
        var routeData = new RouteData();
        routeData.Values.Add(_controller, _webFormController);

        // create a controller context for the route and http context
        var controllerContext = new ControllerContext(new RequestContext(httpContextWrapper, routeData), new WebFormController());

        // find the partial view using the viewengine
        var view = ViewEngines.Engines.FindPartialView(controllerContext, partialName).View as WebFormView;

        // create a view context and assign the model
        var viewContext = new ViewContext(controllerContext, view, new ViewDataDictionary { Model = model }, new TempDataDictionary(), httpContextWrapper.Response.Output);

        // render the partial view
        view.Render(viewContext, httpContextWrapper.Response.Output);
    }

6

这里有一种和我使用的类似的方法。策略是将局部视图渲染为字符串,然后在WebForm页面中输出该字符串。

 public class TemplateHelper
{
    /// <summary>
    /// Render a Partial View (MVC User Control, .ascx) to a string using the given ViewData.
    /// http://www.joeyb.org/blog/2010/01/23/aspnet-mvc-2-render-template-to-string
    /// </summary>
    /// <param name="controlName"></param>
    /// <param name="viewData"></param>
    /// <returns></returns>
    public static string RenderPartialToString(string controlName, object viewData)
    {
        ViewDataDictionary vd = new ViewDataDictionary(viewData);
        ViewPage vp = new ViewPage { ViewData = vd};
        Control control = vp.LoadControl(controlName);

        vp.Controls.Add(control);

        StringBuilder sb = new StringBuilder();
        using (StringWriter sw = new StringWriter(sb))
        {
            using (HtmlTextWriter tw = new HtmlTextWriter(sw))
            {
                vp.RenderControl(tw);
            }
        }

        return sb.ToString();
    }
}

在页面代码后台,你可以做以下事情

public partial class TestPartial : System.Web.UI.Page
{
    public string NavigationBarContent
    {
        get;
        set;
    }

    protected void Page_Load(object sender, EventArgs e)
    {
        NavigationVM oVM = new NavigationVM();

        NavigationBarContent = TemplateHelper.RenderPartialToString("~/Views/Shared/NavigationBar.ascx", oVM);

    }
}

在页面中,您将可以访问已呈现的内容。
<%= NavigationBarContent %>

希望这能帮到您!

这实际上非常棒,特别是当你可以在某个地方放置脚本块时! - jrizzo

3
这个解决方案采用了不同的方法。它定义了一个System.Web.UI.UserControl,可以放置在任何Web表单中,并配置为显示来自任何URL(包括MVC部分视图)的内容。这种方法类似于针对HTML的AJAX调用,因为参数(如果有)是通过URL查询字符串给出的。
首先,在2个文件中定义一个用户控件:
/controls/PartialViewControl.ascx 文件
<%@ Control Language="C#" 
AutoEventWireup="true" 
CodeFile="PartialViewControl.ascx.cs" 
Inherits="PartialViewControl" %>

/controls/PartialViewControl.ascx.cs:

public partial class PartialViewControl : System.Web.UI.UserControl {
    [Browsable(true),
    Category("Configutation"),
    Description("Specifies an absolute or relative path to the content to display.")]
    public string contentUrl { get; set; }

    protected override void Render(HtmlTextWriter writer) {
        string requestPath = (contentUrl.StartsWith("http") ? contentUrl : "http://" + Request.Url.DnsSafeHost + Page.ResolveUrl(contentUrl));
        WebRequest request = WebRequest.Create(requestPath);
        WebResponse response = request.GetResponse();
        Stream responseStream = response.GetResponseStream();
        var responseStreamReader = new StreamReader(responseStream);
        var buffer = new char[32768];
        int read;
        while ((read = responseStreamReader.Read(buffer, 0, buffer.Length)) > 0) {
            writer.Write(buffer, 0, read);
        }
    }
}

然后将用户控件添加到您的Web表单页面中:
<%@ Page Language="C#" %>
<%@ Register Src="~/controls/PartialViewControl.ascx" TagPrefix="mcs" TagName="PartialViewControl" %>
<h1>My MVC Partial View</h1>
<p>Below is the content from by MVC partial view (or any other URL).</p>
<mcs:PartialViewControl runat="server" contentUrl="/MyMVCView/"  />

我认为这是最好的答案,如果您要多次使用此UserControl,只需更改contentUrl即可重用它。我建议当前的requestPath不要获取端口,以防您使用的端口与80不同,这将导致错误。 - Daniel
我发现了一个问题,这个方法为请求生成了一个新的会话。因此就像在同一位置有两个站点在工作。 - Daniel
是的,如果您正在使用服务器端会话来保存应用程序状态,则此解决方案将无法工作。然而,我更喜欢在客户端上维护状态。 - Bill Heitstuman
乍一看,使用 WebRequest 似乎是一个快速简便的解决方案。然而,根据我的经验,有许多潜在的问题可能会导致问题。最好使用 ViewEngine 或客户端上的某些 ajax,如其他答案所示。这不是我尝试后会推荐的解决方案,但也不会给出负面评价,因为它仍然是有效的解决方案。 - Yogi
这将视图代码呈现为字符串,但我猜想的想法是呈现已呈现的视图内容 @Bill - nickornotto

1

顺便说一下,我需要能够从现有的Webforms代码动态地呈现部分视图,并将其插入到给定控件的顶部。我发现Keith的答案可能会导致部分视图在<html />标签之外呈现。

受Keith和Hilarius答案的启发,我没有直接呈现到HttpContext.Current.Response.Output,而是呈现了HTML字符串并将其作为LiteralControl添加到相关控件中。

在静态帮助类中:

    public static string RenderPartial(string partialName, object model)
    {
        //get a wrapper for the legacy WebForm context
        var httpCtx = new HttpContextWrapper(HttpContext.Current);

        //create a mock route that points to the empty controller
        var rt = new RouteData();
        rt.Values.Add("controller", "WebFormController");

        //create a controller context for the route and http context
        var ctx = new ControllerContext(new RequestContext(httpCtx, rt), new WebFormController());

        //find the partial view using the viewengine
        var view = ViewEngines.Engines.FindPartialView(ctx, partialName).View;

        //create a view context and assign the model
        var vctx = new ViewContext(ctx, view, new ViewDataDictionary { Model = model }, new TempDataDictionary(), new StringWriter());

        // This will render the partial view direct to the output, but be careful as it may end up outside of the <html /> tag
        //view.Render(vctx, HttpContext.Current.Response.Output);

        // Better to render like this and create a literal control to add to the parent
        var html = new StringWriter();
        view.Render(vctx, html);
        return html.GetStringBuilder().ToString();
    }

在调用类中:
    internal void AddPartialViewToControl(HtmlGenericControl ctrl, int? insertAt = null, object model)
    {
        var lit = new LiteralControl { Text = MvcHelper.RenderPartial("~/Views/Shared/_MySharedView.cshtml", model};
        if (insertAt == null)
        {
            ctrl.Controls.Add(lit);
            return;
        }
        ctrl.Controls.AddAt(insertAt.Value, lit);
    }

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