Jak dołączyć częściowy widok do formularza internetowego

80

Niektóre witryny, które programuję, używają zarówno ASP.NET MVC, jak i formularzy internetowych.

Mam częściowy widok i chcę to uwzględnić w formularzu internetowym. Widok częściowy zawiera kod, który musi zostać przetworzony na serwerze, więc użycie Response.WriteFile nie działa. Powinien działać przy wyłączonym javascript.

Jak mogę to zrobić?

eKek0
źródło
Mam ten sam problem - Html.RenderPartial nie może działać na WebForms, ale nadal powinien być sposób, aby to zrobić.
Keith

Odpowiedzi:

99

Rzuciłem okiem na źródło MVC, aby zobaczyć, czy mogę dowiedzieć się, jak to zrobić. Wydaje się, że istnieje bardzo ścisłe powiązanie między kontekstem kontrolera, widokami, danymi widoku, danymi routingu i metodami renderowania html.

Zasadniczo, aby to się stało, musisz stworzyć wszystkie te dodatkowe elementy. Niektóre z nich są stosunkowo proste (na przykład dane widoku), ale niektóre są nieco bardziej złożone - na przykład dane routingu będą uwzględniać ignorowanie bieżącej strony WebForms.

Wydaje się, że dużym problemem są strony HttpContext - MVC opierają się na HttpContextBase (a nie HttpContext, jak robią to WebForms) i chociaż oba implementują IServiceProvider, nie są powiązane. Projektanci MVC celowo podjęli decyzję, aby nie zmieniać starszych formularzy WebForm, aby korzystały z nowej bazy kontekstowej, jednak zapewnili opakowanie.

Działa to i umożliwia dodanie częściowego widoku do formularza internetowego:

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

}

Następnie w swoim formularzu internetowym możesz to zrobić:

<% WebFormMVCUtil.RenderPartial( "ViewName", this.GetModel() ); %>
Keith
źródło
1
Działa to jako podstawowe żądanie strony, ale view.Render () wyskakuje z wyjątkiem „Validation of viewstate MAC failed ...”, jeśli wykonasz jakiekolwiek posty na stronie kontenera. Możesz to potwierdzić, Keith?
Kurt Schindler
Nie widzę tego błędu stanu widoku - myślę jednak, że może się zdarzyć, że renderowany widok częściowy zawiera wszelkie kontrolki WebForm. Ta metoda RenderPartial jest uruchamiana podczas renderowania - po dowolnym stanie widoku. Formanty WebForm wewnątrz częściowego widoku będą uszkodzone i poza normalnym cyklem życia strony.
Keith
Właściwie to mam teraz - wydaje się, że występuje to w przypadku niektórych hierarchii formantów WebForms, a nie w przypadku innych. Co dziwne, błąd jest generowany z wnętrza metod renderowania MVC, tak jakby podstawowe wywołanie Page. Render oczekuje sprawdzenia poprawności adresu MAC strony i zdarzenia, co zawsze byłoby całkowicie błędne w MVC.
Keith
Zobacz odpowiedź Hilariusa, jeśli zastanawiasz się, dlaczego to nie kompiluje się pod MVC2 i nowszymi.
Krisztián Balla
1
Zainteresowany także nowymi i lepszymi sposobami osiągnięcia tego celu. Używam tego podejścia do ładowania częściowych widoków na stronie wzorcowej formularzy internetowych (tak, to działa!). Po wywołaniu ze strony wzorcowej nie mogłem uzyskać kontekstu kontrolera, więc musiałem przejść do nowego.
Pat James
40

Trochę to trwało, ale znalazłem świetne rozwiązanie. Rozwiązanie Keitha działa dla wielu ludzi, ale w pewnych sytuacjach nie jest najlepsze, ponieważ czasami chcesz, aby aplikacja przeszła przez proces kontrolera w celu renderowania widoku, a rozwiązanie Keitha po prostu renderuje widok za pomocą danego modelu I ' Przedstawiam tutaj nowe rozwiązanie, które będzie działać normalnie.

Ogólne kroki:

  1. Utwórz klasę Utility
  2. Utwórz wirtualny kontroler z fałszywym widokiem
  3. W swoim aspxlub master pagewywołaj metodę narzędziową, aby renderować częściowe przekazanie kontrolera, wyświetlić i, jeśli potrzebujesz, model do renderowania (jako obiekt),

Sprawdźmy to dokładnie w tym przykładzie

1) Utwórz klasę o nazwie MVCUtilityi utwórz następujące metody:

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

Utwórz klasę do przekazywania parametrów, zadzwonię tutaj RendeActionViewModel (możesz utworzyć w tym samym pliku klasy MvcUtility)

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

2) Teraz utwórz kontroler o nazwie DummyController

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

Utwórz widok Dummy o nazwie PartialRender.cshtml(widok brzytwy) DummyControllerz następującą zawartością, zwróć uwagę, że wykona on inną akcję renderowania za pomocą pomocnika HTML.

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

3) Teraz po prostu umieść to w swoim pliku MasterPagelub aspx, aby częściowo wyrenderować żądany widok. Pamiętaj, że jest to świetna odpowiedź, gdy masz wiele widoków maszynki do golenia, które chcesz połączyć ze swoimi MasterPagelub aspxstronami. (zakładając, że mamy widok PartialView o nazwie Logowanie do strony głównej kontrolera).

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

lub jeśli masz model do przejścia do Akcji

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

To rozwiązanie jest świetne, nie używa wywołania ajax , które nie spowoduje opóźnionego renderowania dla zagnieżdżonych widoków, nie tworzy nowego żądania WebRequest, więc nie przyniesie ci nowej sesji i przetworzy metodę pobierania ActionResult dla żądanego widoku, działa bez przekazywania żadnego modelu

Dzięki zastosowaniu MVC RenderAction w formularzu internetowym

Daniel
źródło
1
Wypróbowałem wszystkie inne rozwiązania w tym poście i ta odpowiedź jest zdecydowanie najlepsza. Poleciłbym każdemu, aby najpierw wypróbował to rozwiązanie.
Halcyon
Cześć Daniel. Możesz mi pomóc. Poszedłem za twoim rozwiązaniem, ale uderzyłem w miejsce. Podniosłem to pod stackoverflow.com/questions/38241661/…
Karthik Venkatraman
To zdecydowanie jedna z najlepszych odpowiedzi, jakie widziałem w SO. Wielkie dzięki.
FrenkyB,
Wydawało mi się to również świetnym rozwiązaniem i na pierwszy rzut oka wydaje się działać, wywoływany jest dummyController i widok oraz wywoływany jest mój kontroler i widok częściowy, ale następnie żądanie kończy się, gdy tylko <% MyApplication.MvcUtility.RenderAction ( „Strona główna”, „Zaloguj się”, nowe {}); %> jest przekazywana w moim aspx, więc reszta strony nie jest renderowana. Czy ktoś doświadczył tego zachowania i wie, jak go rozwiązać?
hsop
20

najbardziej oczywistym sposobem byłoby użycie AJAX

coś takiego (używając jQuery)

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

<script type="text/javascript">
$(document).load(function () {
    $.ajax(
    {    
        type: "GET",
        url : "urltoyourmvcaction",
        success : function (msg) { $("#mvcpartial").html(msg); }
    });
});
</script>
Alexander Taran
źródło
9
został dodany po mojej odpowiedzi) -:
Alexander Taran
11

To świetnie, dzięki!

Używam MVC 2 na .NET 4, który wymaga TextWriter jest przekazywany do ViewContext, więc musisz przekazać httpContextWrapper.Response.Output, jak pokazano poniżej.

    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
źródło
5

Oto podobne podejście, które działa w moim przypadku. Strategia polega na renderowaniu częściowego widoku w postaci ciągu znaków, a następnie wyświetlaniu go na stronie formularza internetowego.

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

W kodzie strony możesz to zrobić

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

    }
}

a na stronie będziesz mieć dostęp do renderowanej treści

<%= NavigationBarContent %>

Mam nadzieję, że to pomoże!

aarondcoleman
źródło
To jest naprawdę świetne, zwłaszcza gdy możesz gdzieś umieścić bloki skryptów!
jrizzo
3

To rozwiązanie ma inne podejście. Definiuje element, System.Web.UI.UserControlktóry można umieścić w dowolnym formularzu sieci Web i skonfigurować do wyświetlania zawartości z dowolnego adresu URL… w tym częściowego widoku MVC. To podejście jest podobne do wywołania AJAX dla HTML, w którym parametry (jeśli istnieją) są podawane za pośrednictwem ciągu zapytania URL.

Najpierw zdefiniuj kontrolkę użytkownika w 2 plikach:

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

Następnie dodaj kontrolkę użytkownika do strony formularza internetowego:

<%@ 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
źródło
Myślę, że to najlepsza odpowiedź, możesz ponownie użyć UserControl, jeśli zamierzasz używać tego więcej niż jeden raz, po prostu zmieniając contentUrl, po prostu radzę, aby bieżąca ścieżka requestPath nie pobierała portu, jeśli używasz inny port niż 80, spowoduje to błąd.
Daniel,
Znalazłem z tym problem, ta metoda generuje nową sesję dla żądania. To tak, jakby dwie witryny pracowały w tym samym miejscu.
Daniel,
Tak, jeśli używasz sesji po stronie serwera do utrzymywania stanu aplikacji, to rozwiązanie nie zadziała. Wolę jednak zachować stan na kliencie.
Bill Heitstuman
Na pierwszy rzut oka korzystanie z WebRequest wydaje się szybkim i łatwym rozwiązaniem. Jednak z mojego doświadczenia wynika, że ​​istnieje wiele ukrytych problemów, które mogą powodować problemy. Lepiej używać ViewEngine lub jakiegoś Ajaxa po stronie klienta, jak pokazano w innych odpowiedziach. Nie głosuj negatywnie, ponieważ jest to ważne rozwiązanie, ale nie takie, które polecałbym po wypróbowaniu.
Roberto
To renderuje kod widoku jako ciąg, podczas gdy myślę, że chodzi o renderowanie renderowanej zawartości widoku @Bill
nickornotto
1

FWIW, musiałem mieć możliwość dynamicznego renderowania częściowego widoku z istniejącego kodu formularzy internetowych i wstawienia go na górze danej kontrolki. Odkryłem, że odpowiedź Keitha może spowodować renderowanie częściowego widoku poza <html />tagiem.

Korzystając z odpowiedzi od Keitha i Hilariusa jako inspiracji, zamiast renderować bezpośrednio do HttpContext.Current.Response.Output, wyrenderowałem ciąg html i dodałem go jako LiteralControl do odpowiedniej kontrolki.

W statycznej klasie pomocniczej:

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

Na zajęciach:

    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);
    }
letni
źródło