La risposta rapida è usare un for()
loop al posto dei tuoi foreach()
loop. 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 sorvola sul motivo per cui questo risolve il problema.
Ci sono tre cose che hai almeno una comprensione superficiale prima di poter risolvere questo problema. Devo ammettere che l'ho coltivato per molto tempo quando ho iniziato a lavorare con il framework. E mi ci è voluto un bel po 'per capire davvero cosa stava succedendo.
Queste tre cose sono:
- Come funzionano gli helper
LabelFor
e gli altri ...For
in MVC?
- Cos'è un albero delle espressioni?
- Come funziona il Model Binder?
Tutti e tre questi concetti si collegano insieme per ottenere una risposta.
Come funzionano gli helper LabelFor
e gli altri ...For
in MVC?
Quindi, hai usato le HtmlHelper<T>
estensioni per LabelFor
e TextBoxFor
e altri, e probabilmente hai notato che quando le invochi, passi loro un lambda e questo genera magicamente dell'html. Ma come?
Quindi la prima cosa da notare è la firma di questi aiutanti. Vediamo il sovraccarico più semplice per
TextBoxFor
public static MvcHtmlString TextBoxFor<TModel, TProperty>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression
)
In primo luogo, si tratta di un metodo di estensione per un fortemente tipizzato HtmlHelper
, di tipo <TModel>
. Quindi, per indicare semplicemente cosa succede dietro le quinte, quando razor esegue il rendering di questa vista, genera una classe. All'interno di questa classe c'è un'istanza di HtmlHelper<TModel>
(come proprietà Html
, motivo per cui puoi usare @Html...
), dove TModel
è il tipo definito nella tua @model
istruzione. Quindi nel tuo caso, quando guardi questa visualizzazione TModel
sarà sempre del tipo ViewModels.MyViewModels.Theme
.
Ora, il prossimo argomento è un po 'complicato. Quindi diamo un'occhiata a un'invocazione
@Html.TextBoxFor(model=>model.SomeProperty);
Sembra che abbiamo un piccolo lambda, e se si dovesse indovinare la firma, si potrebbe pensare che il tipo per questo argomento sarebbe semplicemente a Func<TModel, TProperty>
, dove TModel
è il tipo del modello di visualizzazione e TProperty
viene dedotto come il tipo della proprietà.
Ma non è del tutto corretto, se si guarda al tipo effettivo di argomento è Expression<Func<TModel, TProperty>>
.
Quindi, quando normalmente generi un lambda, il compilatore prende lambda e lo compila in MSIL, proprio come qualsiasi altra funzione (motivo per cui puoi usare delegati, gruppi di metodi e lambda più o meno in modo intercambiabile, perché sono solo riferimenti a codice .)
Tuttavia, quando il compilatore vede che il tipo è an Expression<>
, non compila immediatamente il lambda fino a MSIL, ma genera invece un albero delle espressioni!
Quindi, cosa diavolo è un albero delle espressioni. Beh, non è complicato ma non è nemmeno una passeggiata nel parco. Per citare ms:
| Gli alberi delle espressioni rappresentano il codice in una struttura dati ad albero, in cui ogni nodo è un'espressione, ad esempio una chiamata a un metodo o un'operazione binaria come x <y.
In poche parole, un albero delle espressioni è una rappresentazione di una funzione come un insieme di "azioni".
Nel caso di model=>model.SomeProperty
, l'albero delle espressioni avrebbe un nodo al suo interno che dice: "Ottieni 'alcune proprietà' da un 'modello'"
Questo albero delle espressioni può essere compilato in una funzione che può essere invocata, ma fintanto che è un albero delle espressioni, è solo una raccolta di nodi.
Allora a cosa serve?
Quindi Func<>
o Action<>
, una volta che li hai, sono praticamente atomici. Tutto quello che puoi davvero fare è Invoke()
loro, ovvero dire loro di fare il lavoro che dovrebbero fare.
Expression<Func<>>
d'altra parte, rappresenta una raccolta 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<>
sia, possiamo tornare a Html.TextBoxFor
. Quando esegue il rendering di una casella di testo, deve generare alcune cose sulla proprietà che gli stai dando. Cose come attributes
sulla proprietà per la convalida, e in particolare in questo caso è necessario capire come denominare il <input>
tag.
Lo fa "percorrendo" l'albero delle espressioni e costruendo un nome. Quindi per un'espressione come model=>model.SomeProperty
, percorre l'espressione raccogliendo le proprietà che stai chiedendo e costruendo <input name='SomeProperty'>
.
Per un esempio più complicato, come model=>model.Foo.Bar.Baz.FooBar
, potrebbe generare<input name="Foo.Bar.Baz.FooBar" value="[whatever FooBar is]" />
Ha senso? Non è solo il lavoro che Func<>
fa, ma come fa il suo lavoro è importante qui.
(Nota che altri framework come LINQ to SQL fanno cose simili camminando su un albero delle espressioni e costruendo una grammatica diversa, in questo caso una query SQL)
Come funziona il Model Binder?
Quindi, una volta ottenuto, dobbiamo parlare brevemente del raccoglitore del modello. Quando il modulo viene pubblicato, è semplicemente come un appartamento
Dictionary<string, string>
, abbiamo perso la struttura gerarchica che poteva avere il nostro modello di visualizzazione annidata. È compito del raccoglitore di modelli prendere questa combinazione di coppia chiave-valore e tentare di reidratare un oggetto con alcune proprietà. Come lo fa? Hai indovinato, usando la "chiave" o il nome dell'ingresso che è stato pubblicato.
Quindi, se il modulo post assomiglia
Foo.Bar.Baz.FooBar = Hello
E stai postando su un modello chiamato SomeViewModel
, quindi fa il contrario di ciò che ha fatto l'aiutante in primo luogo. Cerca una proprietà chiamata "Foo". Quindi cerca una proprietà chiamata "Bar" fuori da "Foo", quindi cerca "Baz" ... e così via ...
Infine cerca di analizzare il valore nel tipo di "FooBar" e di assegnarlo a "FooBar".
PHEW !!!
E voilà, hai il tuo modello. L'istanza appena costruita dal Raccoglitore di modelli viene passata all'azione richiesta.
Quindi la tua soluzione non funziona perché gli Html.[Type]For()
helper hanno 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 utilizzare i parziali per il rendering. Ora, in teoria funzionerà, ma probabilmente non nel modo in cui ti aspetti. Quando si esegue il rendering di un parziale, si cambia 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ù breve. Significa anche che quando l'assistente genera il nome per la tua espressione, sarà superficiale. Verrà generato solo in base all'espressione fornita (non all'intero contesto).
Quindi diciamo che hai un parziale che ha appena reso "Baz" (dal nostro esempio prima). All'interno di quel parziale potresti semplicemente 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:
<input name="FooBar" />
Il che, se stai inviando questo modulo a un'azione che prevede un ViewModel grande e profondamente annidato, tenterà di idratare una proprietà FooBar
annullata TModel
. Che nel migliore dei casi non c'è, e nel peggiore è qualcosa di completamente diverso. Se stavi postando per un'azione specifica che accettava un Baz
, piuttosto che il modello radice, allora funzionerebbe benissimo! In effetti, i parziali sono un buon modo per cambiare il contesto di visualizzazione, ad esempio se avessi una pagina con più moduli che pubblicano tutti in azioni diverse, quindi rendere un parziale per ciascuno sarebbe un'ottima idea.
Ora, una volta ottenuto tutto questo, puoi iniziare a fare cose davvero interessanti con Expression<>
, estendendole programmaticamente e facendo altre cose pulite con esse. Non entrerò in niente di tutto ciò. Ma, si spera, questo ti darà una migliore comprensione di cosa sta succedendo dietro le quinte e perché le cose si stanno comportando come stanno.
@
prima di tuttoforeach
? Non dovresti avere anche lambda inHtml.EditorFor
(Html.EditorFor(m => m.Note)
, ad esempio) e nel resto dei metodi? Potrei sbagliarmi, ma puoi incollare il tuo codice effettivo? Sono abbastanza nuovo per MVC, ma puoi risolverlo abbastanza facilmente con visualizzazioni parziali o editor (se questo è il nome?).