Vue MVC Razor imbriquée pour le modèle de foreach


94

Imaginez un scénario commun, c'est une version plus simple de ce que je rencontre. J'ai en fait quelques couches de nidification supplémentaire sur la mienne ....

Mais c'est le scénario

Le thème contient la liste La catégorie 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 ces catégories 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, il semble que je n'obtienne qu'une référence à l'objet Top Model, "Theme" et non à ceux de la boucle foreach.

Est-ce que ce que j'essaie de faire là-bas 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 pour 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.


1
Ne devriez-vous pas avoir @avant tout foreachs? Ne devriez-vous pas également avoir des lambdas dans Html.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?).
Kobi

category.nameJe suis sûr que c'est un stringet ...Forne prend pas en charge une chaîne comme premier paramètre
balexandre

oui, je viens de manquer les @, maintenant ajouté. Merci. Cependant, comme pour lambda, si je commence à taper @ Html.TextBoxFor (m => m. Alors je semble seulement obtenir une référence à l'objet Model supérieur, pas à ceux de la boucle foreach.
David C

@DavidC - Je ne connais pas encore assez MVC 3 pour répondre - mais je suppose que c'est votre problème :).
Kobi

2
Je suis dans le train, mais si ce n'est pas répondu au moment où je me mets au travail, affichez une réponse. La réponse rapide est d'utiliser un standard for()plutôt qu'un foreach. Je vais vous expliquer pourquoi, parce que cela m'a aussi troublé pendant longtemps.
J. Holmes

Réponses:


304

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 LabelForet les autres ...Fordans 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 LabelForet les autres ...Fordans MVC?

Donc, vous avez utilisé les HtmlHelper<T>extensions pour LabelForet TextBoxForet 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ù TModelest le type défini dans votre @modelinstruction. 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ù TModelest 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!

Qu'est-ce qu'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 attributessur 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 FooBarhors 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 Bazmodè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.


4
Réponse géniale. J'essaye actuellement de le digérer. :) Aussi coupable de Cargo Culting! Comme cette description.
David C

4
Merci pour cette réponse détaillée!
Kobi

14
Besoin de plus d'un vote pour cela. +3 (un pour chaque explication) et +1 pour les Cargo-Cultists Réponse absolument géniale!
Kyeotic

3
C'est pourquoi j'aime SO: réponse courte + explication en profondeur + lien génial (cargo-culte). J'aimerai montrer l'article sur le culte du fret à quiconque ne pense pas que la connaissance du fonctionnement interne des choses est extrêmement importante!
user1068352

18

Vous pouvez simplement utiliser EditorTemplates pour ce faire, vous devez créer un répertoire nommé "EditorTemplates" dans le dossier de vue de votre contrôleur et placer une vue séparée pour chacune de vos entités imbriquées (nommée comme nom de classe d'entité)

Vue principale :

@model ViewModels.MyViewModels.Theme

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

Affichage des catégories (/MyController/EditorTemplates/Category.cshtml):

@model ViewModels.MyViewModels.Category

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

Vue 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é Theme publiée dans son ensemble


1
Bien que la réponse acceptée soit très bonne (je l'ai également votée pour), cette réponse est l'option la plus facile à maintenir.
Aaron

4

Vous pouvez ajouter un partiel de catégorie et un partiel de produit, chacun prenant une partie plus petite du modèle principal comme son propre modèle, c'est-à-dire que le type de modèle de la catégorie peut être un IEnumerable, vous lui passerez Model.Theme. Le partiel du produit peut être un IEnumerable dans lequel vous passez Model.Products (à partir du partiel de catégorie).

Je ne sais pas si ce serait la bonne voie à suivre, mais je serais intéressé à savoir.

ÉDITER

Depuis la publication de cette réponse, j'ai utilisé EditorTemplates et je trouve que c'est le moyen le plus simple de gérer des groupes ou des éléments d'entrée répétitifs. Il gère automatiquement tous vos problèmes de message de validation et les problèmes de soumission de formulaire / de liaison de modèle.


Cela m'était venu à l'esprit, je ne savais tout simplement pas comment il le gérerait lorsque je le relis pour le mettre à jour.
David C

1
C'est proche, mais comme il s'agit d'un formulaire à publier en tant qu'unité, cela ne fonctionnera pas très bien. Une fois à l'intérieur du partiel, le contexte de la vue a changé et il n'a plus l'expression profondément imbriquée. La publication sur le Thememodèle ne serait pas hydratée correctement.
J. Holmes

C'est aussi ma préoccupation. Je ferais généralement ce qui précède comme une approche en lecture seule pour afficher les produits, puis je fournirais un lien sur chaque produit vers peut-être une méthode d'action / Product / Edit / 123 pour éditer chacun sur son propre formulaire. Je pense que vous pouvez devenir défait en essayant d'en faire trop sur une seule page dans MVC.
Adrian Thompson Phillips

@AdrianThompsonPhillips oui c'est très possible que je l'ai. Je viens d'un arrière-plan Forms, donc je ne peux toujours pas m'habituer à l'idée de devoir quitter la page pour faire une modification. :(
David C

2

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

Je suis surpris que le code ci-dessus compile même. @ Html.LabelFor nécessite une opération FUNC comme paramètre, le vôtre ne l'est pas
Jenna Leaf

Je ne sais pas si le code ci-dessus compile ou non, mais @foreach imbriqué fonctionne pour moi. MVC5.
antonio

0

Cela ressort clairement de l'erreur.

Le HtmlHelpers ajouté avec "For" attend une expression lambda comme paramètre.

Si vous passez la valeur directement, mieux vaut utiliser une 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 la boucle for de base, car elle vous donnera un index à utiliser.

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

0

Une autre possibilité beaucoup plus simple est que l'un de vos noms de propriété est erroné (probablement celui que vous venez de changer dans la classe). C'est ce que c'était pour moi dans RazorPages .NET Core 3.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.