Verschachtelte MVC Razor-Ansicht für jedes Modell

94

Stellen Sie sich ein häufiges Szenario vor. Dies ist eine einfachere Version dessen, was mir begegnet. Ich habe tatsächlich ein paar weitere Nistschichten auf meiner ...

Dies ist jedoch das Szenario

Thema enthält Liste Kategorie enthält Liste Produkt enthält Liste

Mein Controller bietet ein vollständig ausgefülltes Thema mit allen Kategorien für dieses Thema, den Produkten in diesen Kategorien und deren Bestellungen.

Die Auftragssammlung verfügt über eine Eigenschaft namens Menge (unter anderem), die bearbeitet werden muss.

@model ViewModels.MyViewModels.Theme

@Html.LabelFor(Model.Theme.name)
@foreach (var category in Model.Theme)
{
   @Html.LabelFor(category.name)
   @foreach(var product in theme.Products)
   {
      @Html.LabelFor(product.name)
      @foreach(var order in product.Orders)
      {
          @Html.TextBoxFor(order.Quantity)
          @Html.TextAreaFor(order.Note)
          @Html.EditorFor(order.DateRequestedDeliveryFor)
      }
   }
}

Wenn ich stattdessen Lambda verwende, bekomme ich anscheinend nur einen Verweis auf das oberste Modellobjekt, "Theme", nicht auf diejenigen innerhalb der foreach-Schleife.

Ist das, was ich dort versuche, überhaupt möglich oder habe ich überschätzt oder missverstanden, was möglich ist?

Mit dem oben genannten erhalte ich eine Fehlermeldung bei TextboxFor, EditorFor usw.

CS0411: Die Typargumente für die Methode 'System.Web.Mvc.Html.InputExtensions.TextBoxFor (System.Web.Mvc.HtmlHelper, System.Linq.Expressions.Expression>)' können aus der Verwendung nicht abgeleitet werden. Versuchen Sie, die Typargumente explizit anzugeben.

Vielen Dank.

David C.
quelle
1
Solltest du nicht @vor allem foreachs haben? Sollten Sie nicht auch Lambdas Html.EditorFor( Html.EditorFor(m => m.Note)zum Beispiel) und den Rest der Methoden haben? Ich kann mich irren, aber können Sie bitte Ihren tatsächlichen Code einfügen? Ich bin ziemlich neu in MVC, aber Sie können es ziemlich einfach mit Teilansichten oder Editoren lösen (wenn das der Name ist?).
Kobi
category.nameIch bin sicher, es ist ein stringund ...Forunterstützt keinen String als ersten Parameter
Balexandre
Ja, ich habe gerade die @ 's verpasst, jetzt hinzugefügt. Vielen Dank. Wenn ich jedoch bei Lambda anfange, @ Html.TextBoxFor (m => m.) Eingeben, erhalte ich anscheinend nur einen Verweis auf das oberste Modellobjekt, nicht auf diejenigen innerhalb der foreach-Schleife.
David C
@ DavidC - Ich weiß noch nicht genug von MVC 3, um zu antworten - aber ich vermute, dass dies Ihr Problem ist :).
Kobi
2
Ich bin im Zug, aber wenn dies nicht beantwortet wird, bis ich krank werde, schreibe eine Antwort. Die schnelle Antwort ist, ein reguläres for()anstelle eines zu verwenden foreach. Ich werde erklären, warum, weil es mich auch lange Zeit verdammt verwirrt hat.
J. Holmes

Antworten:

304

Die schnelle Antwort besteht darin, for()anstelle Ihrer foreach()Schleifen eine Schleife zu verwenden . Etwas wie:

@for(var themeIndex = 0; themeIndex < Model.Theme.Count(); themeIndex++)
{
   @Html.LabelFor(model => model.Theme[themeIndex])

   @for(var productIndex=0; productIndex < Model.Theme[themeIndex].Products.Count(); productIndex++)
   {
      @Html.LabelFor(model=>model.Theme[themeIndex].Products[productIndex].name)
      @for(var orderIndex=0; orderIndex < Model.Theme[themeIndex].Products[productIndex].Orders; orderIndex++)
      {
          @Html.TextBoxFor(model => model.Theme[themeIndex].Products[productIndex].Orders[orderIndex].Quantity)
          @Html.TextAreaFor(model => model.Theme[themeIndex].Products[productIndex].Orders[orderIndex].Note)
          @Html.EditorFor(model => model.Theme[themeIndex].Products[productIndex].Orders[orderIndex].DateRequestedDeliveryFor)
      }
   }
}

Dies beschönigt jedoch, warum dies das Problem behebt.

Es gibt drei Dinge, die Sie zumindest flüchtig verstehen müssen, bevor Sie dieses Problem beheben können. Ich muss zugeben, dass ich dies lange Zeit mit Fracht kultiviert habe, als ich anfing, mit dem Framework zu arbeiten. Und ich habe eine ganze Weile gebraucht, um wirklich zu verstehen, was los war.

Diese drei Dinge sind:

  • Wie arbeiten die LabelForund andere ...ForHelfer in MVC?
  • Was ist ein Ausdrucksbaum?
  • Wie funktioniert der Model Binder?

Alle drei Konzepte verbinden sich, um eine Antwort zu erhalten.

Wie arbeiten die LabelForund andere ...ForHelfer in MVC?

Sie haben also die HtmlHelper<T>Erweiterungen für LabelForund TextBoxForund andere verwendet, und Sie haben wahrscheinlich bemerkt, dass Sie ihnen beim Aufrufen ein Lambda übergeben und auf magische Weise HTML- Code generieren. Aber wie?

Das erste, was zu bemerken ist, ist die Unterschrift für diese Helfer. Schauen wir uns die einfachste Überlastung an TextBoxFor

public static MvcHtmlString TextBoxFor<TModel, TProperty>(
    this HtmlHelper<TModel> htmlHelper,
    Expression<Func<TModel, TProperty>> expression
) 

Erstens ist dies eine Erweiterungsmethode für einen stark typisierten HtmlHelperTyp <TModel>. Um einfach zu sagen, was hinter den Kulissen passiert: Wenn Razor diese Ansicht rendert, wird eine Klasse generiert. Innerhalb dieser Klasse befindet sich eine Instanz von HtmlHelper<TModel>(als Eigenschaft Html, weshalb Sie sie verwenden können @Html...), wobei TModelder in Ihrer @modelAnweisung definierte Typ ist . Wenn Sie also in diesem Fall diese Ansicht betrachten, ist TModel sie immer vom Typ ViewModels.MyViewModels.Theme.

Das nächste Argument ist etwas knifflig. Schauen wir uns also einen Aufruf an

@Html.TextBoxFor(model=>model.SomeProperty);

Es sieht so aus, als hätten wir ein kleines Lambda. Und wenn man die Signatur erraten würde, könnte man denken, dass der Typ für dieses Argument einfach a ist Func<TModel, TProperty>, wobei TModelder Typ des Ansichtsmodells ist und TProperty als Typ der Eigenschaft abgeleitet wird.

Aber das ist nicht ganz richtig, wenn man sich den tatsächlichen Typ des Arguments ansieht Expression<Func<TModel, TProperty>>.

Wenn Sie also normalerweise ein Lambda generieren, nimmt der Compiler das Lambda und kompiliert es wie jede andere Funktion in MSIL (weshalb Sie Delegaten, Methodengruppen und Lambdas mehr oder weniger austauschbar verwenden können, da es sich nur um Code-Referenzen handelt .)

Wenn der Compiler Expression<>jedoch erkennt, dass es sich um einen Typ handelt , kompiliert er das Lambda nicht sofort in MSIL, sondern generiert einen Ausdrucksbaum!

Was ist ein Ausdrucksbaum ?

Also, was zum Teufel ist ein Ausdrucksbaum. Nun, es ist nicht kompliziert, aber es ist auch kein Spaziergang im Park. Um ms zu zitieren:

| Ausdrucksbäume stellen Code in einer baumartigen Datenstruktur dar, wobei jeder Knoten ein Ausdruck ist, beispielsweise ein Methodenaufruf oder eine binäre Operation wie x <y.

Einfach ausgedrückt ist ein Ausdrucksbaum eine Darstellung einer Funktion als Sammlung von "Aktionen".

Im Fall von model=>model.SomePropertywürde der Ausdrucksbaum einen Knoten enthalten, der besagt: "Holen Sie sich 'Some Property' von einem 'Modell'"

Dieser Ausdrucksbaum kann zu einer Funktion kompiliert werden, die aufgerufen werden kann. Solange es sich jedoch um einen Ausdrucksbaum handelt, handelt es sich nur um eine Sammlung von Knoten.

Wofür ist das gut?

Also Func<>oder Action<>, sobald Sie sie haben, sind sie ziemlich atomar. Alles, was Sie wirklich tun können, sind Invoke()sie, auch bekannt als sagen Sie ihnen, dass sie die Arbeit tun sollen, die sie tun sollen.

Expression<Func<>>Auf der anderen Seite handelt es sich um eine Sammlung von Aktionen, die angehängt, bearbeitet, besucht oder kompiliert und aufgerufen werden können.

Warum erzählst du mir das alles?

Mit diesem Verständnis dessen, was ein Expression<>ist, können wir zurückkehren Html.TextBoxFor. Wenn ein Textfeld gerendert wird, müssen einige Dinge über die Eigenschaft generiert werden , die Sie ihm geben. Dinge wie attributesauf der Eigenschaft zur Validierung, und speziell in diesem Fall muss es herausfinden, wie das Tag zu benennen ist<input> .

Dies geschieht durch "Gehen" des Ausdrucksbaums und Erstellen eines Namens. Für einen Ausdruck wie model=>model.SomePropertygeht es also um den Ausdruck, der die Eigenschaften sammelt, nach denen Sie fragen, und erstellt <input name='SomeProperty'>.

Für ein komplizierteres Beispiel model=>model.Foo.Bar.Baz.FooBarkönnte es generiert werden<input name="Foo.Bar.Baz.FooBar" value="[whatever FooBar is]" />

Sinn ergeben? Es ist nicht nur die Arbeit, die das Func<>macht, sondern auch, wie es seine Arbeit macht, ist hier wichtig.

(Beachten Sie, dass andere Frameworks wie LINQ to SQL ähnliche Aktionen ausführen, indem sie einen Ausdrucksbaum durchlaufen und eine andere Grammatik erstellen, in diesem Fall eine SQL-Abfrage.)

Wie funktioniert der Model Binder?

Sobald Sie das bekommen, müssen wir kurz über den Modellbinder sprechen. Wenn das Formular veröffentlicht wird, ist es einfach wie eine Wohnung Dictionary<string, string>. Wir haben die hierarchische Struktur verloren, die unser verschachteltes Ansichtsmodell möglicherweise hatte. Es ist die Aufgabe des Modellbinders, diese Schlüssel-Wert-Paar-Kombination zu verwenden und zu versuchen, ein Objekt mit einigen Eigenschaften zu rehydrieren. Wie macht es das? Sie haben es erraten, indem Sie den "Schlüssel" oder den Namen der Eingabe verwendet haben, die veröffentlicht wurde.

Also, wenn der Formularbeitrag so aussieht

Foo.Bar.Baz.FooBar = Hello

Und Sie posten auf einem Modell namens SomeViewModel, dann macht es das Gegenteil von dem, was der Helfer zuerst getan hat. Es sucht nach einer Eigenschaft namens "Foo". Dann sucht es nach einer Eigenschaft namens "Bar" von "Foo", dann sucht es nach "Baz" ... und so weiter ...

Schließlich wird versucht, den Wert in den Typ "FooBar" zu analysieren und ihn "FooBar" zuzuweisen.

PUH!!!

Und voila, du hast dein Modell. Die Instanz, die der Modellbinder gerade erstellt hat, wird in die angeforderte Aktion übergeben.


Ihre Lösung funktioniert also nicht, weil die Html.[Type]For()Helfer einen Ausdruck benötigen. Und du gibst ihnen nur einen Wert. Es hat keine Ahnung, was der Kontext für diesen Wert ist, und es weiß nicht, was es damit anfangen soll.

Nun schlugen einige Leute vor, Partials zum Rendern zu verwenden. Nun wird dies theoretisch funktionieren, aber wahrscheinlich nicht so, wie Sie es erwarten. Wenn Sie einen Teil rendern, ändern Sie den Typ von TModel, da Sie sich in einem anderen Ansichtskontext befinden. Dies bedeutet, dass Sie Ihre Immobilie mit einem kürzeren Ausdruck beschreiben können. Dies bedeutet auch, dass der Helfer, wenn er den Namen für Ihren Ausdruck generiert, flach ist. Es wird nur basierend auf dem angegebenen Ausdruck generiert (nicht der gesamte Kontext).

Nehmen wir also an, Sie hatten einen Teil, der gerade "Baz" gerendert hat (aus unserem vorherigen Beispiel). In diesem Teil könnte man einfach sagen:

@Html.TextBoxFor(model=>model.FooBar)

Eher, als

@Html.TextBoxFor(model=>model.Foo.Bar.Baz.FooBar)

Das bedeutet, dass ein Eingabe-Tag wie folgt generiert wird:

<input name="FooBar" />

Welche, wenn Sie dieses Formular auf eine Aktion Mitteilung verfassen , die eine große tief verschachtelt Ansichtsmodell erwartet, dann wird es versuchen , eine Eigenschaft , um Hydrat genannt FooBarOff TModel. Was im besten Fall nicht da ist und im schlimmsten Fall etwas ganz anderes. Wenn Sie zu einer bestimmten Aktion posten würden, die ein Bazanstelle des Stammmodells akzeptiert , würde dies hervorragend funktionieren! Partials sind in der Tat eine gute Möglichkeit, Ihren Ansichtskontext zu ändern. Wenn Sie beispielsweise eine Seite mit mehreren Formularen haben, die alle unterschiedliche Aktionen ausführen, ist es eine gute Idee, für jedes eine Partial zu rendern.


Sobald Sie all dies erhalten haben, können Sie anfangen, wirklich interessante Dinge zu tun Expression<>, indem Sie sie programmgesteuert erweitern und andere nette Dinge mit ihnen tun. Darauf werde ich nicht eingehen. Aber hoffentlich erhalten Sie dadurch ein besseres Verständnis dafür, was sich hinter den Kulissen abspielt und warum sich die Dinge so verhalten, wie sie sind.

J. Holmes
quelle
4
Super Antwort. Ich versuche gerade, es zu verdauen. :) Auch des Cargo Culting schuldig! Wie diese Beschreibung.
David C
4
Vielen Dank für diese ausführliche Antwort!
Kobi
14
Benötigen Sie mehr als eine Gegenstimme dafür. +3 (eine für jede Erklärung) und +1 für Frachtkultisten. Absolut geniale Antwort!
Kyeotic
3
Deshalb liebe ich SO: kurze Antwort + ausführliche Erklärung + großartiger Link (Frachtkult). Ich möchte den Beitrag über den Frachtkult jedem zeigen, der nicht der Meinung ist, dass das Wissen über das Innenleben von Dingen extrem wichtig ist!
user1068352
18

Sie können dazu einfach EditorTemplates verwenden. Sie müssen ein Verzeichnis mit dem Namen "EditorTemplates" im Ansichtsordner Ihres Controllers erstellen und eine separate Ansicht für jede Ihrer verschachtelten Entitäten platzieren (als Entitätsklassenname bezeichnet).

Hauptansicht :

@model ViewModels.MyViewModels.Theme

@Html.LabelFor(Model.Theme.name)
@Html.EditorFor(Model.Theme.Categories)

Kategorieansicht (/MyController/EditorTemplates/Category.cshtml):

@model ViewModels.MyViewModels.Category

@Html.LabelFor(Model.Name)
@Html.EditorFor(Model.Products)

Produktansicht (/MyController/EditorTemplates/Product.cshtml):

@model ViewModels.MyViewModels.Product

@Html.LabelFor(Model.Name)
@Html.EditorFor(Model.Orders)

und so weiter

Auf diese Weise generiert der Html.EditorFor-Helfer die Namen der Elemente in geordneter Weise, sodass Sie keine weiteren Probleme beim Abrufen der veröffentlichten Theme-Entität als Ganzes haben

Alireza Sabouri
quelle
1
Während die akzeptierte Antwort sehr gut ist (ich habe sie auch positiv bewertet), ist diese Antwort die wartbarere Option.
Aaron
4

Sie können einen Kategorieteil und einen Produktteil hinzufügen, wobei jeder einen kleineren Teil des Hauptmodells als eigenes Modell übernimmt. Der Modelltyp der Kategorie kann also eine IE-Zahl sein, die Sie in Model.Theme übergeben. Der Teil des Produkts kann ein IEnumerable sein, an den Sie Model.Products übergeben (aus dem Teil der Kategorie heraus).

Ich bin mir nicht sicher, ob das der richtige Weg wäre, würde mich aber dafür interessieren.

BEARBEITEN

Seit ich diese Antwort gepostet habe, habe ich EditorTemplates verwendet und finde, dass dies der einfachste Weg ist, um sich wiederholende Eingabegruppen oder Elemente zu verarbeiten. Es behandelt alle Probleme mit Validierungsnachrichten und Probleme mit der Übermittlung von Formularen / Modellbindungen automatisch.

Adrian Thompson Phillips
quelle
Das war mir eingefallen, ich war mir nur nicht sicher, wie es damit umgehen würde, wenn ich es zurück las, um es zu aktualisieren.
David C
1
Es ist nah dran, aber da dies ein Formular ist, das als Einheit veröffentlicht werden soll, funktioniert es nicht ganz richtig. Sobald Sie sich im Teil befinden, hat sich der Ansichtskontext geändert und der tief verschachtelte Ausdruck ist nicht mehr vorhanden. Das Zurückschicken auf das ThemeModell würde nicht richtig hydratisiert.
J. Holmes
Das ist auch mein Anliegen. Normalerweise würde ich das oben Gesagte als schreibgeschützten Ansatz für die Anzeige der Produkte verwenden und dann einen Link zu jedem Produkt zu einer / Product / Edit / 123-Aktionsmethode bereitstellen, um jedes in einem eigenen Formular zu bearbeiten. Ich denke, Sie können rückgängig machen, wenn Sie versuchen, auf einer Seite in MVC zu viel zu tun.
Adrian Thompson Phillips
@AdrianThompsonPhillips ja es ist sehr wahrscheinlich, dass ich habe. Ich stamme aus einem Formularhintergrund, daher kann ich mich immer noch nicht an die Idee gewöhnen, die Seite verlassen zu müssen, um eine Bearbeitung vorzunehmen. :(
David C
2

Wenn Sie eine foreach-Schleife in der Ansicht für ein gebundenes Modell verwenden ... Ihr Modell sollte im aufgelisteten Format vorliegen.

dh

@model IEnumerable<ViewModels.MyViewModels>


        @{
            if (Model.Count() > 0)
            {            

                @Html.DisplayFor(modelItem => Model.Theme.FirstOrDefault().name)
                @foreach (var theme in Model.Theme)
                {
                   @Html.DisplayFor(modelItem => theme.name)
                   @foreach(var product in theme.Products)
                   {
                      @Html.DisplayFor(modelItem => product.name)
                      @foreach(var order in product.Orders)
                      {
                          @Html.TextBoxFor(modelItem => order.Quantity)
                         @Html.TextAreaFor(modelItem => order.Note)
                          @Html.EditorFor(modelItem => order.DateRequestedDeliveryFor)
                      }
                  }
                }
            }else{
                   <span>No Theam avaiable</span>
            }
        }
Pranav Labhe
quelle
Ich bin überrascht, dass der obige Code sogar kompiliert wird. @ Html.LabelFor erfordert eine FUNC-Operation als Parameter, Ihre ist nicht
Jenna Leaf
Ich weiß nicht, ob der obige Code kompiliert wird oder nicht, aber verschachteltes @foreach funktioniert für mich. MVC5.
Antonio
0

Es ist aus dem Fehler klar.

Die mit "For" angehängten HtmlHelpers erwarten einen Lambda-Ausdruck als Parameter.

Wenn Sie den Wert direkt übergeben, verwenden Sie besser Normal.

z.B

Verwenden Sie anstelle von TextboxFor (....) Textbox ()

Die Syntax für TextboxFor lautet wie folgt: Html.TextBoxFor (m => m.Property)

In Ihrem Szenario können Sie basic for loop verwenden, da Sie dadurch einen Index zur Verwendung erhalten.

@for(int i=0;i<Model.Theme.Count;i++)
 {
   @Html.LabelFor(m=>m.Theme[i].name)
   @for(int j=0;j<Model.Theme[i].Products.Count;j++) )
     {
      @Html.LabelFor(m=>m.Theme[i].Products[j].name)
      @for(int k=0;k<Model.Theme[i].Products[j].Orders.Count;k++)
          {
           @Html.TextBoxFor(m=>Model.Theme[i].Products[j].Orders[k].Quantity)
           @Html.TextAreaFor(m=>Model.Theme[i].Products[j].Orders[k].Note)
           @Html.EditorFor(m=>Model.Theme[i].Products[j].Orders[k].DateRequestedDeliveryFor)
      }
   }
}
Manas
quelle
0

Eine andere viel einfachere Möglichkeit ist, dass einer Ihrer Eigenschaftsnamen falsch ist (wahrscheinlich einer, den Sie gerade in der Klasse geändert haben). Das war es für mich in RazorPages .NET Core 3.

Erste Division
quelle