Cómo incluir una vista parcial dentro de un formulario web

80

Algún sitio que estoy programando usa ASP.NET MVC y WebForms.

Tengo una vista parcial y quiero incluir esto dentro de un formulario web. La vista parcial tiene algún código que debe procesarse en el servidor, por lo que el uso de Response.WriteFile no funciona. Debería funcionar con JavaScript desactivado.

¿Cómo puedo hacer esto?

eKek0
fuente
Tengo el mismo problema: Html.RenderPartial no puede funcionar en WebForms, pero aún debería haber una forma de hacerlo.
Keith

Respuestas:

99

Eché un vistazo a la fuente de MVC para ver si podía averiguar cómo hacer esto. Parece haber un acoplamiento muy estrecho entre el contexto del controlador, las vistas, los datos de la vista, los datos de enrutamiento y los métodos de renderización html.

Básicamente, para que esto suceda, debe crear todos estos elementos adicionales. Algunos de ellos son relativamente simples (como los datos de vista), pero algunos son un poco más complejos; por ejemplo, los datos de enrutamiento considerarán que la página de WebForms actual se ignorará.

El gran problema parece ser el HttpContext: las páginas MVC se basan en HttpContextBase (en lugar de HttpContext como lo hacen los WebForms) y aunque ambos implementan IServiceProvider no están relacionados. Los diseñadores de MVC tomaron la decisión deliberada de no cambiar los WebForms heredados para usar la nueva base de contexto, sin embargo, proporcionaron un contenedor.

Esto funciona y le permite agregar una vista parcial a un 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 );
    }

}

Luego, en su WebForm puede hacer esto:

<% WebFormMVCUtil.RenderPartial( "ViewName", this.GetModel() ); %>
Keith
fuente
1
Esto funciona en una solicitud de página básica, pero view.Render () explota con la excepción "Validación de la MAC de estado de vista fallida ..." si realiza alguna publicación en la página del contenedor. ¿Puedes confirmar lo mismo, Keith?
Kurt Schindler
No recibo ese error de estado de vista; sin embargo, creo que ocurriría si la vista parcial que está representando incluye cualquier control de WebForm. Este método RenderPartial se activa al renderizar, después de cualquier estado de visualización. Los controles de WebForm dentro de la vista parcial se romperán y estarán fuera del ciclo de vida normal de la página.
Keith
De hecho, ahora sí, parece ocurrir para algunas jerarquías de control de WebForms y no para otras. Extrañamente, el error se produce desde el interior de los métodos de representación de MVC, como si la llamada subyacente a Page. Render espera realizar una validación MAC de eventos y páginas, lo que siempre sería completamente incorrecto en MVC.
Keith
Vea la respuesta de Hilarius si se pregunta por qué esto no se compila en MVC2 y superior.
Krisztián Balla
1
También interesado en las nuevas y mejores formas de hacer esto. Estoy usando este enfoque para cargar vistas parciales en una página maestra de formularios web (¡sí, funciona!) Cuando se me llama desde la página maestra, no pude obtener un contexto de controlador, así que tuve que crear uno nuevo.
Pat James
40

Me tomó un tiempo, pero encontré una gran solución. La solución de Keith funciona para mucha gente, pero en ciertas situaciones no es la mejor, porque a veces quieres que tu aplicación pase por el proceso del controlador para renderizar la vista, y la solución de Keith simplemente renderiza la vista con un modelo dado I ' Presento aquí una nueva solución que ejecutará el proceso normal.

Pasos generales:

  1. Crear una clase de utilidad
  2. Cree un controlador ficticio con una vista ficticia
  3. En su aspxo master page, llame al método de utilidad para renderizar parcialmente pasando el Controlador, vea y si lo necesita, el modelo a renderizar (como un objeto),

Comprobémoslo de cerca en este ejemplo

1) Cree una clase llamada MVCUtilityy cree los siguientes métodos:

    //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 });
    }

Cree una clase para pasar los parámetros, llamaré aquí RendeActionViewModel (puede crear en el mismo archivo de la clase MvcUtility)

    public class RenderActionViewModel
    {
        public string ControllerName { get; set; }
        public string ActionName { get; set; }
        public object RouteValues { get; set; }
    }

2) Ahora crea un controlador llamado DummyController

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

Cree una vista ficticia llamada PartialRender.cshtml(vista de maquinilla de afeitar) para el DummyControllercon el siguiente contenido, tenga en cuenta que realizará otra acción de renderizado utilizando el ayudante Html.

@model Portal.MVC.MvcUtility.RenderActionViewModel
@{Html.RenderAction(Model.ActionName, Model.ControllerName, Model.RouteValues);}

3) Ahora simplemente ponga esto en su archivo MasterPageo aspx, para renderizar parcialmente la vista que desee. Tenga en cuenta que esta es una gran respuesta cuando tiene varias vistas de la maquinilla de afeitar que desea mezclar con sus páginas MasterPageo aspx. (suponiendo que tengamos un PartialView llamado Inicio de sesión para el controlador).

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

o si tienes un modelo para pasar a la Acción

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

Esta solución es excelente, no usa una llamada ajax , lo que no causará un renderizado retrasado para las vistas anidadas, no crea una nueva WebRequest por lo que no le traerá una nueva sesión y procesará el método para recuperar el ActionResult para la vista que desea, funciona sin pasar ningún modelo

Gracias al uso de MVC RenderAction dentro de un formulario web

Daniel
fuente
1
Probé todas las otras soluciones en esta publicación y esta respuesta es, con mucho, la mejor. Recomendaría a cualquier otra persona que pruebe esta solución primero.
Halcyon
Hola Daniel. Podrías ayudarme. Seguí tu solución pero acerté en un lugar. Lo he planteado en stackoverflow.com/questions/38241661/…
Karthik Venkatraman
Esta es definitivamente una de las mejores respuestas que he visto en SO. Muchas gracias.
FrenkyB
Esto también me pareció una gran solución, y a primera vista parece funcionar, se llama al dummyController y la vista y se llama a mi controlador y vista parcial, pero luego la solicitud finaliza tan pronto como <% MyApplication.MvcUtility.RenderAction ( "Inicio", "Iniciar sesión", nuevo {}); La línea%> se pasa en mi aspx, por lo que el resto de la página no se muestra. ¿Alguien ha experimentado este comportamiento y sabe cómo solucionarlo?
hsop
20

la forma más obvia sería a través de AJAX

algo como esto (usando jQuery)

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

<script type="text/javascript">
$(document).load(function () {
    $.ajax(
    {    
        type: "GET",
        url : "urltoyourmvcaction",
        success : function (msg) { $("#mvcpartial").html(msg); }
    });
});
</script>
Alejandro Taran
fuente
9
fue agregado después de mi respuesta) -:
Alexander Taran
11

¡Esto es genial, gracias!

Estoy usando MVC 2 en .NET 4, que requiere que un TextWriter se pase a ViewContext, por lo que debe pasar httpContextWrapper.Response.Output como se muestra a continuación.

    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);
    }
Dr. C. Hilarius
fuente
5

Aquí hay un enfoque similar que me ha funcionado. La estrategia es convertir la vista parcial en una cadena y luego generarla en la página 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();
    }
}

En el código de página subyacente, puede hacer

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);

    }
}

y en la página tendrás acceso al contenido renderizado

<%= NavigationBarContent %>

¡Espero que ayude!

aarondcoleman
fuente
¡Esto es realmente genial, especialmente cuando puedes poner bloques de script en algún lugar!
jrizzo
3

Esta solución tiene un enfoque diferente. Define un archivo System.Web.UI.UserControlque puede colocarse en cualquier formulario web y configurarse para mostrar el contenido de cualquier URL ... incluida una vista parcial MVC. Este enfoque es similar a una llamada AJAX para HTML en que los parámetros (si los hay) se proporcionan a través de la cadena de consulta URL.

Primero, defina un control de usuario en 2 archivos:

/controls/PartialViewControl.ascx archivo

<%@ 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);
        }
    }
}

Luego agregue el control de usuario a su página de formulario 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/"  />
Bill Heitstuman
fuente
Creo que esta es la mejor respuesta, puede reutilizar UserControl si va a usar esto más de una vez, simplemente cambiando el contentUrl, solo le aconsejo que la requestPath actual no obtenga el puerto, en caso de que esté usando un puerto diferente al 80, va a generar un error.
Daniel
Encontré un problema con él, este método genera una nueva sesión para la solicitud. Entonces, es como tener dos sitios trabajando en el mismo lugar.
Daniel
Sí, si está utilizando sesiones del lado del servidor para mantener el estado de su aplicación, esta solución no funcionaría. Sin embargo, prefiero mantener el estado del cliente.
Bill Heitstuman
A primera vista, utilizar WebRequest parece una solución rápida y sencilla. Sin embargo, según mi experiencia, hay muchos problemas ocultos que pueden causar problemas. Es mejor usar ViewEngine o algún ajax en el lado del cliente como se muestra en otras respuestas. No hay voto negativo ya que esta es una solución válida, pero no una que recomendaría después de probarla.
Roberto
Esto representa el código de la vista como una cadena, mientras que supongo que la idea es representar el contenido de la vista representada @Bill
nickornotto
1

FWIW, necesitaba poder representar una vista parcial dinámicamente desde el código de formularios web existente e insertarla en la parte superior de un control dado. Descubrí que la respuesta de Keith puede hacer que la vista parcial se represente fuera de la <html />etiqueta.

Usando las respuestas de Keith e Hilarius como inspiración, en lugar de renderizar directamente a HttpContext.Current.Response.Output, rendericé la cadena html y la agregué como un LiteralControl al control relevante.

En la clase de ayuda estática:

    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();
    }

Al llamar a la clase:

    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);
    }
lukep
fuente