La réponse rapide est d'utiliser une for()
boucle à la place de vos foreach()
boucles. Quelque chose comme:
@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)
}
}
}
Mais cela passe sous silence pourquoi cela résout le problème.
Il y a trois choses que vous avez au moins une compréhension superficielle avant de pouvoir résoudre ce problème. Je dois admettre que j'ai cultivé cela pendant longtemps lorsque j'ai commencé à travailler avec le framework. Et il m'a fallu un certain temps pour vraiment comprendre ce qui se passait.
Ces trois choses sont:
- Comment fonctionnent les assistants
LabelFor
et les autres ...For
dans MVC?
- Qu'est-ce qu'un arbre d'expression?
- Comment fonctionne le classeur de modèles?
Ces trois concepts sont liés pour obtenir une réponse.
Comment fonctionnent les assistants LabelFor
et les autres ...For
dans MVC?
Donc, vous avez utilisé les HtmlHelper<T>
extensions pour LabelFor
et TextBoxFor
et d'autres, et vous avez probablement remarqué que lorsque vous les appelez, vous leur passez un lambda et cela génère comme par magie du html. Mais comment?
La première chose à remarquer est donc la signature de ces assistants. Regardons la surcharge la plus simple pour
TextBoxFor
public static MvcHtmlString TextBoxFor<TModel, TProperty>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression
)
Tout d' abord, cela est une méthode d'extension pour un fortement typé HtmlHelper
, de type <TModel>
. Donc, pour indiquer simplement ce qui se passe dans les coulisses, lorsque Razor rend cette vue, il génère une classe. À l'intérieur de cette classe se trouve une instance de HtmlHelper<TModel>
(en tant que propriété Html
, c'est pourquoi vous pouvez utiliser @Html...
), où TModel
est le type défini dans votre @model
instruction. Donc, dans votre cas, lorsque vous regardez cette vue TModel
sera toujours du type ViewModels.MyViewModels.Theme
.
Maintenant, l'argument suivant est un peu délicat. Alors regardons une invocation
@Html.TextBoxFor(model=>model.SomeProperty);
On dirait que nous avons un petit lambda, et si l'on devinait la signature, on pourrait penser que le type de cet argument serait simplement a Func<TModel, TProperty>
, où TModel
est le type du modèle de vue et TProperty
est déduit comme le type de la propriété.
Mais ce n'est pas tout à fait vrai, si vous regardez le type réel de l'argument son Expression<Func<TModel, TProperty>>
.
Ainsi, lorsque vous générez normalement un lambda, le compilateur prend le lambda et le compile dans MSIL, comme toute autre fonction (c'est pourquoi vous pouvez utiliser des délégués, des groupes de méthodes et des lambdas de manière plus ou moins interchangeable, car ce ne sont que des références de code .)
Cependant, lorsque le compilateur voit que le type est an Expression<>
, il ne compile pas immédiatement le lambda vers MSIL, il génère plutôt un arbre d'expression!
Alors, qu'est-ce que diable est un arbre d'expression. Eh bien, ce n'est pas compliqué mais ce n'est pas non plus une promenade dans le parc. Pour citer ms:
| Les arbres d'expression représentent le code dans une structure de données arborescente, où chaque nœud est une expression, par exemple, un appel de méthode ou une opération binaire telle que x <y.
En termes simples, un arbre d'expression est une représentation d'une fonction en tant que collection "d'actions".
Dans le cas de model=>model.SomeProperty
, l'arborescence des expressions contiendrait un nœud qui dit: "Obtenir 'une propriété' à partir d'un 'modèle'"
Cet arbre d'expression peut être compilé en une fonction qui peut être appelée, mais tant qu'il s'agit d'un arbre d'expression, ce n'est qu'une collection de nœuds.
Alors, à quoi ça sert?
Donc Func<>
ou Action<>
, une fois que vous les avez, ils sont à peu près atomiques. Tout ce que vous pouvez vraiment faire, c'est Invoke()
leur dire de faire le travail qu'ils sont censés faire.
Expression<Func<>>
d'autre part, représente une collection d'actions, qui peuvent être ajoutées, manipulées, visitées ou compilées et appelées.
Alors pourquoi tu me dis tout ça?
Donc, avec cette compréhension de ce qu'est un Expression<>
, nous pouvons revenir à Html.TextBoxFor
. Lorsqu'il rend une zone de texte, il doit générer quelques éléments sur la propriété que vous lui attribuez. Des choses comme attributes
sur la propriété pour la validation, et en particulier dans ce cas, il doit déterminer comment nommer la <input>
balise.
Il le fait en "parcourant" l'arborescence des expressions et en créant un nom. Donc, pour une expression comme model=>model.SomeProperty
, elle parcourt l'expression rassemblant les propriétés que vous demandez et construit <input name='SomeProperty'>
.
Pour un exemple plus compliqué, comme model=>model.Foo.Bar.Baz.FooBar
, cela pourrait générer<input name="Foo.Bar.Baz.FooBar" value="[whatever FooBar is]" />
Ça a du sens? Ce n'est pas seulement le travail que le Func<>
fait, mais la manière dont il fait son travail est important ici.
(Notez que d'autres frameworks comme LINQ to SQL font des choses similaires en parcourant un arbre d'expression et en construisant une grammaire différente, que dans ce cas une requête SQL)
Comment fonctionne le classeur de modèles?
Donc, une fois que vous avez compris, nous devons parler brièvement du classeur modèle. Lorsque le formulaire est publié, c'est tout simplement comme un appartement
Dictionary<string, string>
, nous avons perdu la structure hiérarchique que notre modèle de vue imbriquée pouvait avoir. C'est le travail du classeur de modèles de prendre cette combinaison de paires clé-valeur et d'essayer de réhydrater un objet avec certaines propriétés. Comment fait-il cela? Vous l'avez deviné, en utilisant la "clé" ou le nom de l'entrée qui a été publiée.
Donc, si le message du formulaire ressemble à
Foo.Bar.Baz.FooBar = Hello
Et vous publiez sur un modèle appelé SomeViewModel
, puis il fait l'inverse de ce que l'assistant a fait en premier lieu. Il recherche une propriété appelée "Foo". Ensuite, il cherche une propriété appelée "Bar" hors de "Foo", puis il cherche "Baz" ... et ainsi de suite ...
Enfin, il essaie d'analyser la valeur dans le type de "FooBar" et de l'affecter à "FooBar".
PHEW!!!
Et voila, vous avez votre modèle. L'instance que le Model Binder vient de construire est transmise à l'action demandée.
Votre solution ne fonctionne donc pas car les Html.[Type]For()
assistants ont besoin d'une expression. Et vous leur donnez simplement une valeur. Il n'a aucune idée du contexte pour cette valeur, et il ne sait pas quoi en faire.
Maintenant, certaines personnes ont suggéré d'utiliser des partiels pour le rendu. Maintenant, cela fonctionnera en théorie, mais probablement pas de la manière que vous attendez. Lorsque vous effectuez le rendu d'un partiel, vous modifiez le type de TModel
, car vous êtes dans un contexte de vue différent. Cela signifie que vous pouvez décrire votre propriété avec une expression plus courte. Cela signifie également que lorsque l'assistant génère le nom de votre expression, il sera peu profond. Il ne sera généré qu'en fonction de l'expression donnée (pas du contexte entier).
Disons donc que vous avez eu un partiel qui vient de rendre "Baz" (de notre exemple avant). À l'intérieur de ce partiel, vous pourriez simplement dire:
@Html.TextBoxFor(model=>model.FooBar)
Plutôt que
@Html.TextBoxFor(model=>model.Foo.Bar.Baz.FooBar)
Cela signifie qu'il générera une balise d'entrée comme celle-ci:
<input name="FooBar" />
Ce qui, si vous envoyez des messages ce formulaire à une action qui attend un grand ViewModel, il essayera profondément imbriquées pour hydrater une propriété appelée FooBar
hors de TModel
. Ce qui, au mieux, n'existe pas, et au pire, c'est tout autre chose. Si vous publiez sur une action spécifique qui acceptait un Baz
modèle, plutôt que le modèle racine, cela fonctionnerait très bien! En fait, les partiels sont un bon moyen de modifier votre contexte d'affichage, par exemple si vous aviez une page avec plusieurs formulaires qui publient tous des actions différentes, alors rendre un partiel pour chacun serait une excellente idée.
Maintenant, une fois que vous avez tout cela, vous pouvez commencer à faire des choses vraiment intéressantes Expression<>
, en les étendant par programme et en faisant d'autres choses intéressantes avec eux. Je n'entrerai pas dans tout cela. Mais, espérons-le, cela vous permettra de mieux comprendre ce qui se passe dans les coulisses et pourquoi les choses se comportent comme elles le sont.
@
avant toutforeach
s? Ne devriez-vous pas également avoir des lambdas dansHtml.EditorFor
(Html.EditorFor(m => m.Note)
par exemple) et le reste des méthodes? Je me trompe peut-être, mais pouvez-vous coller votre code actuel? Je suis assez nouveau dans MVC, mais vous pouvez le résoudre assez facilement avec des vues partielles ou des éditeurs (si tel est le nom?).