MVC Razor view nested foreach’s model

Immagina uno scenario comune, questa è una versione più semplice di quello che sto incontrando. In realtà ho un paio di strati di ulteriore nidificazione sui miei ….

Ma questo è lo scenario

Tema contiene la categoria di elenco contiene la lista Il prodotto contiene la lista

Il mio controller fornisce un tema completamente popolato, con tutte le categorie per quel tema, i prodotti all’interno di queste categorie e i loro ordini.

La raccolta ordini ha una proprietà denominata Quantità (tra molte altre) che deve essere modificabile.

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

Se invece utilizzo lambda, mi sembra di ottenere solo un riferimento all’object Model top, “Theme” e non a quelli del ciclo foreach.

È ciò che sto cercando di fare anche lì o ho sopravvalutato o frainteso ciò che è ansible?

Con quanto sopra ho un errore su TextboxFor, EditorFor, ecc

CS0411: Gli argomenti di tipo per il metodo ‘System.Web.Mvc.Html.InputExtensions.TextBoxFor (System.Web.Mvc.HtmlHelper, System.Linq.Expressions.Expression>)’ non possono essere dedotti dall’utilizzo. Prova a specificare esplicitamente gli argomenti del tipo.

Grazie.

La risposta rapida è usare un ciclo for() al posto dei tuoi cicli foreach() . Qualcosa di simile a:

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

Ma questo spiega perché questo risolve il problema.

Ci sono tre cose che hai almeno una comprensione superficiale prima di poter risolvere questo problema. Devo ammettere che ho lavorato su questo carico per un lungo periodo quando ho iniziato a lavorare con il framework. E mi ci è voluto un po ‘per capire cosa stava succedendo.

Queste tre cose sono:

  • Come fanno LabelFor e altri ...For helper lavorare in MVC?
  • Cos’è un albero delle espressioni?
  • Come funziona Model Binder?

Tutti e tre questi concetti si collegano tra loro per ottenere una risposta.

Come fanno LabelFor e altri ...For helper lavorare in MVC?

Quindi, hai usato le estensioni HtmlHelper per LabelFor e TextBoxFor e altri, e probabilmente hai notato che quando li invochi, li passi a lambda e genera magicamente un po ‘di html. Ma come?

Quindi la prima cosa da notare è la firma di questi helper. Diamo un’occhiata al sovraccarico più semplice per TextBoxFor

 public static MvcHtmlString TextBoxFor( this HtmlHelper htmlHelper, Expression> expression ) 

Innanzitutto, si tratta di un metodo di estensione per un HtmlHelper fortemente tipizzato, di tipo . Quindi, per indicare semplicemente cosa succede dietro le quinte, quando il razor rende questa vista genera una class. All’interno di questa class è presente un’istanza di HtmlHelper (come proprietà Html , che è il motivo per cui è ansible utilizzare @Html... ), dove TModel è il tipo definito @model . Quindi nel tuo caso, quando guardi questa vista, TModel sarà sempre del tipo ViewModels.MyViewModels.Theme .

Ora, il prossimo argomento è un po ‘complicato. Quindi guardiamo un’invocazione

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

Sembra che abbiamo un piccolo lambda, E se uno dovesse indovinare la firma, si potrebbe pensare che il tipo per questo argomento sarebbe semplicemente un Func , dove TModel è il tipo del modello di vista e TProperty è dedotto come il tipo di proprietà.

Ma non è giusto, se si guarda il tipo effettivo dell’argomento la sua Expression> .

Quindi, quando generi normalmente un lambda, il compilatore prende il lambda e lo compila in MSIL, proprio come qualsiasi altra funzione (ecco perché puoi usare delegati, gruppi di metodi e lambda più o meno in modo intercambiabile, perché sono solo riferimenti al codice .)

Tuttavia, quando il compilatore vede che il tipo è Expression<> , non compila immediatamente il lambda giù in MSIL, invece genera un albero delle espressioni!

Cos’è un albero delle espressioni ?

Quindi, che diamine è un albero di espressione. Beh, non è complicato ma non è nemmeno una passeggiata nel parco. Per citare ms:

| Gli alberi di espressione rappresentano il codice in una struttura di dati ad albero, in cui ciascun nodo è un’espressione, ad esempio una chiamata di metodo o un’operazione binaria come x

In poche parole, un albero di espressioni è una rappresentazione di una funzione come una raccolta di “azioni”.

Nel caso di model=>model.SomeProperty , l’albero delle espressioni avrebbe un nodo in esso che dice: “Ottieni ‘Some Property’ da un ‘modello'”

Questo albero di espressioni può essere compilato in una funzione che può essere invocata, ma fintanto che si tratta di un albero di espressioni, è solo una raccolta di nodes.

Quindi a cosa serve?

Quindi Func<> o Action<> , una volta che li hai, sono praticamente atomici. Tutto quello che puoi fare è Invoke() , o dire loro di fare il lavoro che dovrebbero svolgere.

Expression> d’altra parte, rappresenta una collezione di azioni, che possono essere aggiunte, manipolate, visitate , o compilate e invocate.

Allora perché mi stai dicendo tutto questo?

Quindi, con questa comprensione di cosa è Expression<> , possiamo tornare a Html.TextBoxFor . Quando esegue il rendering di una casella di testo, ha bisogno di generare alcune cose sulla proprietà che le stai dando. Cose come gli attributes sulla convalida per la validazione e, in questo caso, in particolare, è necessario capire che nome dare al tag .

Lo fa “camminando” sull’albero delle espressioni e costruendo un nome. Quindi per un’espressione come model=>model.SomeProperty , cammina l’espressione raccogliendo le proprietà che stai richiedendo e crea .

Per un esempio più complicato, come model=>model.Foo.Bar.Baz.FooBar , potrebbe generare

Ha senso? Non è solo il lavoro che fa il Func<> , ma come funziona il suo lavoro qui è importante.

(Nota: altri framework come LINQ to SQL fanno cose simili camminando su un albero di espressioni e costruendo una grammatica diversa, che in questo caso è una query SQL)

Come funziona Model Binder?

Quindi una volta capito, dobbiamo parlare brevemente del legatore del modello. Quando il modulo viene pubblicato, è semplicemente come un Dictionary piatto Dictionary , abbiamo perso la struttura gerarchica che il nostro modello di visualizzazione annidato potrebbe aver avuto. È compito del legatore del modello prendere questa combo di coppie chiave-valore e tentare di reidratare un object con alcune proprietà. Come fa questo? Hai indovinato, usando la “chiave” o il nome dell’input che è stato pubblicato.

Quindi se il post del modulo è simile

 Foo.Bar.Baz.FooBar = Hello 

E stai postando su un modello chiamato SomeViewModel , poi fa il contrario di ciò che l’helper ha fatto in primo luogo. Cerca una proprietà chiamata “Foo”. Quindi cerca una proprietà chiamata “Bar” su “Foo”, quindi cerca “Baz” … e così via …

Alla fine cerca di analizzare il valore nel tipo di “FooBar” e assegnarlo a “FooBar”.

PHEW !!!

E voilà, hai il tuo modello. L’istanza in cui il Model Binder è stato appena costruito viene consegnata nell’azione richiesta.


Quindi la tua soluzione non funziona perché l’ Html.[Type]For() helper Html.[Type]For() bisogno di un’espressione. E stai solo dando loro un valore. Non ha idea di quale sia il contesto per quel valore e non sa cosa farne.

Ora alcune persone hanno suggerito di usare partial per renderizzare. Ora questo in teoria funzionerà, ma probabilmente non nel modo in cui ci si aspetta. Quando si TModel rendering di un partial, si modifica il tipo di TModel , perché ci si trova in un contesto di visualizzazione diverso. Ciò significa che puoi descrivere la tua proprietà con un’espressione più corta. Significa anche che quando l’aiutante genera il nome per la tua espressione, sarà superficiale. Genererà solo in base all’espressione fornita (non all’intero contesto).

Quindi diciamo che hai avuto un partial che ha appena reso “Baz” (dal nostro esempio precedente). Dentro quel parziale si potrebbe solo dire:

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

Piuttosto che

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

Ciò significa che genererà un tag di input come questo:

  

Che, se stai postando questo modulo a un’azione che si aspetta un ViewModel grande e profondamente annidato, tenterà di idratare una proprietà chiamata FooBar su TModel . Che nel migliore dei casi non c’è, e nel peggiore dei casi è qualcos’altro. Se stavi postando un’azione specifica che accettava un Baz , piuttosto che il modello root, allora funzionerebbe alla grande! In effetti, i partial sono un buon modo per cambiare il contesto della tua vista, ad esempio se tu avessi una pagina con più moduli che tutti postano a diverse azioni, quindi rendere un partial per ciascuna sarebbe una grande idea.


Ora, una volta ottenuto tutto questo, puoi iniziare a fare cose davvero interessanti con Expression<> , estendendoli in modo programmatico e facendo altre cose belle con loro. Non entrerò in nessuna di queste cose. Ma, si spera, questo ti darà una migliore comprensione di ciò che accade dietro le quinte e del perché le cose si comportano come sono.

Puoi semplicemente utilizzare EditorTemplates per farlo, devi creare una directory denominata “EditorTemplates” nella cartella di visualizzazione del controller e posizionare una vista separata per ciascuna delle entity framework nidificate (denominata come nome della class di quadro)

Vista principale:

 @model ViewModels.MyViewModels.Theme @Html.LabelFor(Model.Theme.name) @Html.EditorFor(Model.Theme.Categories) 

Vista categoria (/MyController/EditorTemplates/Category.cshtml):

 @model ViewModels.MyViewModels.Category @Html.LabelFor(Model.Name) @Html.EditorFor(Model.Products) 

Vista del prodotto (/MyController/EditorTemplates/Product.cshtml):

 @model ViewModels.MyViewModels.Product @Html.LabelFor(Model.Name) @Html.EditorFor(Model.Orders) 

e così via

in questo modo l’helper Html.EditorFor genererà i nomi dell’elemento in modo ordinato e pertanto non avrai ulteriori problemi per recuperare l’ quadro Tema pubblicata nel suo complesso

Potresti aggiungere una partial parziale e una partial di prodotto, ognuna delle quali prenderà una parte più piccola del modello principale come se fosse un suo modello, ovvero il tipo di modello di una categoria potrebbe essere un object IEnumerable, che passerebbe a Model.Theme. Il partial del prodotto potrebbe essere un object IEnumerable che passi a Model.Products in (dall’interno del partial di categoria).

Non sono sicuro che sarebbe la strada giusta, ma sarei interessato a conoscere.

MODIFICARE

Da quando ho postato questa risposta, ho utilizzato EditorTemplates e ho trovato questo il modo più semplice per gestire gruppi di input o elementi ripetuti. Gestisce automaticamente tutti i problemi relativi ai messaggi di convalida e forma l’invio / il modello.

Quando si utilizza il ciclo foreach in vista per il modello binded … Il modello dovrebbe essere nel formato elencato.

vale a dire

 @model IEnumerable @{ 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{ No Theam avaiable } } 

È chiaro dall’errore.

HtmlHelpers aggiunto a “For” si aspetta un’espressione lambda come parametro.

Se stai passando il valore direttamente, meglio usare quello normale.

per esempio

Invece di TextboxFor (….) usa Textbox ()

la syntax per TextboxFor sarà come Html.TextBoxFor (m => m.Property)

Nel tuo scenario puoi usare il ciclo di base, dato che ti darà l’indice da usare.

 @for(int i=0;im.Theme[i].name) @for(int j=0;jm.Theme[i].Products[j].name) @for(int k=0;kModel.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) } } }