Modèle MVC Razor vue nestede de foreach

Imaginez un scénario commun, il s’agit d’une version plus simple de ce que je rencontre. J’ai en fait quelques couches de nidification sur la mine ….

Mais c’est le scénario

Thème contient Liste La liste contient la liste Le produit contient la liste

Mon contrôleur fournit un thème entièrement rempli, avec toutes les catégories pour ce thème, les produits de cette catégorie et leurs commandes.

La collection de commandes a une propriété appelée Quantity (parmi beaucoup d’autres) qui doit être modifiable.

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

Si j’utilise lambda à la place, alors je semble seulement avoir une référence au premier object Model, “Theme”, pas à ceux de la boucle foreach.

Est-ce que ce que j’essaye de faire là est même possible ou ai-je surestimé ou mal compris ce qui est possible?

Avec ce qui précède, j’obtiens une erreur sur TextboxFor, EditorFor, etc.

CS0411: Les arguments de type de la méthode ‘System.Web.Mvc.Html.InputExtensions.TextBoxFor (System.Web.Mvc.HtmlHelper, System.Linq.Expressions.Expression>)’ ne peuvent pas être déduits de l’utilisation. Essayez de spécifier les arguments de type explicitement.

Merci.

La réponse rapide est d’utiliser une boucle for() à la place de vos boucles foreach() . 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 explique pourquoi cela résout le problème.

Il y a trois choses que vous avez au moins une compréhension sommaire avant de pouvoir résoudre ce problème. Je dois admettre que cela m’a longtemps été difficile lorsque j’ai commencé à travailler avec le framework. Et ça m’a pris pas mal de temps pour vraiment comprendre ce qui se passait.

Ces trois choses sont:

  • Comment fonctionnent les LabelFor et autres ...For assistants dans MVC?
  • Qu’est-ce qu’un arbre d’expression?
  • Comment fonctionne le Model Binder?

Ces trois concepts sont liés pour obtenir une réponse.

Comment fonctionnent les LabelFor et autres ...For assistants dans MVC?

Vous avez donc utilisé les HtmlHelper pour LabelFor et TextBoxFor et d’autres, et vous avez probablement remarqué que lorsque vous les invoquez, vous leur transmettez un lambda et cela génère comme par magie du HTML. Mais comment?

Donc, la première chose à noter est la signature pour ces aides. Regardons la surcharge la plus simple pour TextBoxFor

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

Tout d’abord, il s’agit d’une méthode d’extension pour un HtmlHelper fortement typé, de type . Donc, pour simplement dire ce qui se passe en coulisse, quand le razor rend cette vue, il génère une classe. A l’intérieur de cette classe se trouve une instance de HtmlHelper (en tant que propriété Html , c’est pourquoi vous pouvez utiliser @Html... ), où TModel est le type défini dans votre instruction @model . 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 peu de lambda, et si on devinait la signature, on pourrait penser que le type de cet argument serait simplement un Func , 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> .

Donc, quand vous générez normalement un lambda, le compilateur prend le lambda et le comstack dans MSIL, comme toute autre fonction (c’est pourquoi vous pouvez utiliser des delegates, 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 une Expression<> , il ne comstack pas immédiatement le lambda vers MSIL, mais génère un arbre d’expression!

Qu’est-ce qu’un arbre d’expression ?

Alors, qu’est-ce que c’est qu’un arbre d’expression? Eh bien, ce n’est pas compliqué mais ce n’est pas une promenade dans le parc non plus. Pour citer ms:

| Les arbres d’expression représentent du 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 binary telle que x

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’arbre d’expression comporterait un nœud indiquant: “Obtenir une propriété” à partir d’un “modèle” ”

Cet arbre d’expression peut être compilé dans une fonction pouvant être appelée, mais tant qu’il s’agit d’un arbre d’expression, il ne s’agit que d’une collection de nœuds.

Alors à quoi ça sert?

Donc Func<> ou Action<> , une fois que vous les avez, ils sont plutôt atomiques. Tout ce que vous pouvez vraiment faire est de les Invoke() leur dire de faire le travail qu’ils sont censés faire.

Expression> , d’autre part, représente un ensemble d’actions pouvant être ajoutées, manipulées, visitées ou compilées et invoquées.

Alors pourquoi tu me dis tout ça?

Donc, avec cette compréhension de ce qu’est une Expression<> , nous pouvons revenir à Html.TextBoxFor . Lorsqu’il affiche une zone de texte, il doit générer quelques éléments sur la propriété que vous lui donnez. Des choses comme les atsortingbutes de la propriété pour la validation, et dans ce cas précis, il doit déterminer comment nommer la .

Cela se fait en “promenant” l’arbre d’expression et en créant un nom. Donc, pour une expression comme model=>model.SomeProperty , elle parcourt l’expression regroupant les propriétés que vous demandez et .

Pour un exemple plus compliqué, comme model=>model.Foo.Bar.Baz.FooBar , il pourrait générer

Avoir du sens? Ce n’est pas juste le travail que fait le Func<> , mais la façon dont cela fonctionne 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 ce cas est une requête SQL)

Comment fonctionne le Model Binder?

Donc, une fois que vous obtenez cela, nous devons parler brièvement du modèle de classeur. Lorsque le formulaire est publié, il s’agit simplement d’un Dictionary plat Dictionary , nous avons perdu la structure hiérarchique que notre modèle de vue nested peut avoir. C’est le travail du classeur modèle de prendre cette combinaison clé-valeur et de tenter de réhydrater un object 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 post de la forme ressemble

 Foo.Bar.Baz.FooBar = Hello 

Et vous SomeViewModel sur un modèle appelé SomeViewModel , puis il fait l’inverse de ce que l’assistant a fait en premier lieu. Il cherche une propriété appelée “Foo”. Ensuite, il cherche une propriété appelée “Bar” de “Foo”, puis elle cherche “Baz” … et ainsi de suite …

Enfin, il essaie d’parsingr la valeur dans le type “FooBar” et de l’affecter à “FooBar”.

PHEW!!!

Et voilà, vous avez votre modèle. L’instance que le Model Binder vient de construire est transmise à l’action demandée.


Donc, votre solution ne fonctionne pas parce que le Html.[Type]For() aides de Html.[Type]For() besoin d’une expression. Et vous leur donnez juste 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 les rendre. Maintenant, cela fonctionnera en théorie, mais probablement pas comme prévu. Lorsque vous effectuez un rendu partiel, vous modifiez le type de TModel , car vous vous trouvez dans un autre contexte d’affichage. Cela signifie que vous pouvez décrire votre propriété avec une expression plus courte. Cela signifie également que lorsque l’aide génère le nom de votre expression, elle sera superficielle. Il ne générera que sur la base de l’expression donnée (et non sur l’ensemble du contexte).

Alors disons que vous avez eu un partiel qui a juste rendu “Baz” (de notre exemple avant). Dans cette partie, vous pourriez simplement dire:

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

Plutôt que

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

Cela signifie qu’il va générer une balise d’entrée comme ceci:

  

Qui, si vous publiez ce formulaire dans une action qui attend un grand ViewModel profondément nested, essaiera alors d’hydrater une propriété appelée FooBar de TModel . Ce qui au mieux n’est pas là, et au pire, c’est autre chose. Si vous postiez une action spécifique acceptant un Baz plutôt que le modèle racine, cela fonctionnerait bien! En fait, les partiels sont un bon moyen de changer le contexte de votre vue, par exemple si vous avez une page avec plusieurs formulaires que tous publient sur différentes actions, le rendu partiel de chacun serait une bonne idée.


Maintenant, une fois que vous obtenez tout cela, vous pouvez commencer à faire des choses vraiment intéressantes avec Expression<> , en les étendant par programme et en faisant d’autres choses intéressantes avec eux. Je ne vais pas entrer dans tout ça. Mais, espérons-le, cela vous permettra de mieux comprendre ce qui se passe en coulisse et pourquoi les choses se passent comme elles sont.

Vous pouvez simplement utiliser EditorTemplates pour ce faire, vous devez créer un répertoire nommé “EditorTemplates” dans le dossier d’affichage de votre contrôleur et placer une vue séparée pour chacune de vos entités nestedes (nommée en tant que nom de classe d’entité).

Vue principale :

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

Vue par catégorie (/MyController/EditorTemplates/Category.cshtml):

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

Vue du produit (/MyController/EditorTemplates/Product.cshtml):

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

etc

De cette façon, Html.EditorFor helper générera les noms des éléments de manière ordonnée et vous n’aurez donc plus de problème pour récupérer l’entité Thème envoyée dans son ensemble

Vous pourriez append une catégorie partielle et un produit partiel, chacun prenant une partie plus petite du modèle principal en tant que son propre modèle, c.-à-d. Le partiel du produit peut être un IEnumerable que vous transmettez à Model.Products (à partir de la catégorie partielle).

Je ne suis pas sûr que ce soit la bonne voie à suivre, mais je serais intéressé à le savoir.

MODIFIER

Depuis l’affichage de cette réponse, j’ai utilisé EditorTemplates et j’ai trouvé que c’était le moyen le plus simple de gérer des groupes d’entrées ou des éléments répétés. Il traite tous vos problèmes de message de validation et soumet automatiquement les soumissions et les modèles de liaison.

Lorsque vous utilisez la boucle foreach dans la vue pour le modèle lié … Votre modèle est censé être au format répertorié.

c’est à 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 } } 

C’est clair de l’erreur.

Le HtmlHelpers ajouté avec “For” attend l’expression lambda comme paramètre.

Si vous transmettez directement la valeur, mieux vaut utiliser la valeur normale.

par exemple

Au lieu de TextboxFor (….) utilisez Textbox ()

la syntaxe de TextboxFor sera comme Html.TextBoxFor (m => m.Property)

Dans votre scénario, vous pouvez utiliser basic for loop, comme cela vous donnera un index à utiliser.

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