94 votes

Vue imbriquée foreach du modèle MVC Razor

Imaginez un scénario courant, ceci est une version plus simple de ce sur quoi je tombe. J'ai en réalité plusieurs niveaux d'imbrication supplémentaires dans le mien....

Mais voici le scénario

Thème contient une liste Catégorie contient une liste Produit contient une liste

Mon contrôleur fournit un thème entièrement peuplé, avec toutes les catégories pour ce thème, les produits dans ces catégories et leurs commandes.

La collection de commandes a une propriété appelée Quantité (parmi tant 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 au lieu de cela, je semble seulement obtenir une référence à l'objet Model principal, "Theme" et non à ceux dans la boucle foreach.

Est-ce que ce que j'essaie de faire 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 le 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 explicitement les arguments de type.

Merci.

1 votes

Ne devriez-vous pas avoir @ avant tous les foreach? 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 peux me tromper, mais pouvez-vous s'il vous plaît coller votre code réel? Je suis assez nouveau dans MVC, mais vous pouvez le résoudre assez facilement avec des vues partielles, ou des éditeurs (si c'est le nom?).

0 votes

category.nom Je suis sûr que c'est une chaîne de caractères et ...For ne prend pas en charge une chaîne de caractères en tant que premier paramètre

0 votes

Oui, j'ai juste manqué les @, maintenant ajoutés. Merci. Cependant, en ce qui concerne lambda, si je commence à taper @Html.TextBoxFor(m => m. alors je ne semble obtenir qu'une référence à l'objet Model principal, pas ceux à l'intérieur de la boucle foreach.

306voto

32bitkid Points 11851

La réponse rapide est d'utiliser un for() à la place de votre 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 ne tient pas compte pourquoi cela corrige le problème.

Il y a trois choses que vous devez comprendre au moins sommairement avant de pouvoir résoudre ce problème. Je dois admettre que je cargo-culte ce depuis longtemps quand j'ai commencé à travailler avec le framework. Et ça m'a pris un certain temps pour vraiment comprendre ce qui se passait.

Ces trois choses sont :

  • Comment les LabelFor et autres ...For Les aides fonctionnent en MVC ?
  • Qu'est-ce qu'un arbre d'expression ?
  • Comment fonctionne le Model Binder ?

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

Comment les LabelFor et autres ...For Les aides fonctionnent en MVC ?

Donc, vous avez utilisé le HtmlHelper<T> extensions pour LabelFor y TextBoxFor et autres, et vous avez probablement remarqué que lorsque vous les invoquez, vous leur passez un lambda et il par magie génère du html. Mais comment ?

La première chose à remarquer est donc la signature de ces aides. Regardons la surcharge la plus simple de TextBoxFor

public static MvcHtmlString TextBoxFor<TModel, TProperty>(
    this HtmlHelper<TModel> htmlHelper,
    Expression<Func<TModel, TProperty>> expression
) 

Tout d'abord, il s'agit d'une méthode d'extension pour un système fortement typé. HtmlHelper de type <TModel> . Donc, pour simplement ce qui se passe en coulisses, lorsque Razor rend cette vue, il génère une classe. A l'intérieur de cette classe se trouve une instance de HtmlHelper<TModel> (comme la propriété Html c'est pourquoi vous pouvez utiliser @Html... ), où TModel est le type défini dans votre @model déclaration. Donc dans votre cas, quand vous regardez cette vue TModel sera toujours du type ViewModels.MyViewModels.Theme .

Maintenant, l'argument suivant est un peu délicat. Regardons donc une invocation

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

On dirait que nous avons un petit lambda, et si l'on devait deviner la signature, on pourrait penser que le type de cet argument serait simplement un Func<TModel, TProperty> , donde 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 exact, si vous regardez le réel type 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 en MSIL, comme n'importe quel autre fichier de type fonction (c'est pourquoi vous pouvez utiliser les délégués, les groupes de méthodes et les lambdas de manière plus ou moins interchangeable, parce qu'il s'agit simplement de références de code). références de code).

Cependant, lorsque le compilateur voit que le type est un Expression<> il ne compile pas immédiatement le lambda en MSIL, mais génère un message de type arbre d'expression !

Qu'est-ce qu'un Arbre d'expression ?

Alors, que diable est un arbre d'expression. Eh bien, ce n'est pas compliqué, mais ce n'est pas non plus une promenade de santé. Pour citer Mme :

| 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 comme une collection d'"actions".

Dans le cas de model=>model.SomeProperty l'arbre d'expression contiendrait un nœud qui dirait : "Get 'Some Property' from a 'model'".

Cet arbre d'expression peut être compilé en une fonction qui peut être invoquée, mais tant que c'est un arbre d'expression, c'est juste une collection de nœuds.

Alors à quoi ça sert ?

Alors Func<> o Action<> une fois que vous les avez, ils sont pratiquement atomiques. Tout ce que vous pouvez faire, c'est Invoke() les, aka 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, a visité o compilé et invoqué.

Alors pourquoi tu me dis tout ça ?

Donc avec cette compréhension de ce qu'est un Expression<> est, nous pouvons retourner à Html.TextBoxFor . Lorsqu'il rend une zone de texte, il doit de générer quelques éléments à propos de la propriété que vous lui donnez. Des choses comme attributes sur la propriété pour validation, et notamment dans ce cas, elle doit trouver quoi faire pour nom le site <input> étiquette.

Pour ce faire, il "parcourt" l'arbre des expressions et construit un nom. Ainsi, pour une expression comme model=>model.SomeProperty , il marche l'expression en 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 il pourrait générer <input name="Foo.Bar.Baz.FooBar" value="[whatever FooBar is]" />

C'est logique ? Ce n'est pas seulement le travail que le Func<> le fait, mais comment La façon dont il effectue son travail est importante 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, dans ce cas une requête SQL).

Comment fonctionne le Model Binder ?

Une fois que vous avez compris cela, nous devons parler brièvement du modèle de liant. Lorsque le formulaire est posté, c'est simplement comme un plat 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 de prendre cette combinaison de paires clé-valeur et de tenter de réhydrater un objet avec certaines propriétés. Comment s'y prend-il ? cela ? Vous l'avez deviné, en utilisant la "clé" ou le nom de l'entrée qui a été postée.

Donc, si le message du formulaire ressemble à

Foo.Bar.Baz.FooBar = Hello

Et vous vous connectez à un modèle appelé SomeViewModel alors il fait l'inverse de ce que l'assistant a fait en premier lieu. Il recherche une propriété appelée "Foo". Puis il cherche une propriété appelée "Bar" à partir de "Foo", puis il cherche "Baz"... et ainsi de suite...

Enfin, il essaie d'analyser 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() Les aides ont besoin d'une expression. Et vous ne faites que leur donner une valeur. Il n'a aucune idée du contexte de cette valeur, et il ne sait pas quoi en faire.

Certaines personnes ont suggéré d'utiliser des partiels pour le rendu. En théorie, cela fonctionne, mais probablement pas de la façon dont vous l'attendez. Lorsque vous effectuez le rendu d'un partial, vous changez le type de TModel parce que 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. Elle générera uniquement le nom basé sur l'expression qui lui est donnée (et non sur le contexte entier).

Disons que vous avez un partiel qui a juste rendu "Baz" (de notre exemple précédent). A l'intérieur de ce partiel, vous pourriez simplement dire :

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

Plutôt que de

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

Cela signifie qu'il va générer une balise d'entrée comme celle-ci :

<input name="FooBar" />

Si vous envoyez ce formulaire à une action qui attend un ViewModel important et profondément imbriqué, il essaiera d'hydrater une propriété. appelée FooBar de TModel . Ce qui, au mieux, n'existe pas, et au pire, est quelque chose d'entièrement différent. Si vous envoyez un message à une action spécifique qui accepte un message de type Baz plutôt que le modèle Root, alors cela fonctionnera parfaitement ! En fait, les partiels sont un bon moyen de modifier le contexte d'affichage, par exemple si vous avez une page avec plusieurs formulaires qui renvoient tous à des actions différentes, le rendu d'un partiel pour chacun d'eux serait une bonne idée.


Maintenant, une fois que vous avez tout ça, vous pouvez commencer à faire des choses intéressantes avec Expression<> en les étendant par programme et en faisant d'autres choses intéressantes avec eux. Je ne vais pas m'étendre sur ce sujet. Mais, avec un peu de chance, cela vous donner une meilleure compréhension de ce qui se passe dans les coulisses et pourquoi les choses agissent comme elles le font.

4 votes

Super réponse. J'essaie actuellement de la digérer. :) Aussi coupable de Culte du Cargo! J'aime cette description.

4 votes

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

14 votes

Besoin de plus d'un upvote pour cela. +3 (un pour chaque explication), et +1 pour les adeptes du culte de la cargaison. Réponse absolument brillante!

19voto

sos00 Points 725

Vous pouvez simplement utiliser EditorTemplates pour faire cela, 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 le nom de la classe d'entité)

Vue principale :

@model ViewModels.MyViewModels.Theme

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

Vue de la 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)

et ainsi de suite

de cette manière, l'aide Html.EditorFor générera les noms des éléments de manière ordonnée et vous n'aurez donc aucun problème supplémentaire pour récupérer l'entité Theme postée dans son ensemble

1 votes

Alors que la réponse acceptée est très bonne (j'ai aussi voté pour elle), cette réponse est l'option la plus maintenable.

4voto

Vous pourriez ajouter un modèle de catégorie et un modèle de produit, chacun prendrait une petite partie du modèle principal comme son propre modèle, c'est-à-dire que le type de modèle de catégorie pourrait être un IEnumerable, vous lui transmettriez Model.Theme. Le modèle partiel de produit pourrait être un IEnumerable que vous transmettriez à Model.Products (depuis le modèle de catégorie).

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

MODIFIER

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

0 votes

Cela m'avait traversé l'esprit, je n'étais simplement pas sûr de comment cela allait être géré lorsque je le relirais pour le mettre à jour.

1 votes

C'est proche, mais puisque c'est un formulaire à poster en tant qu'unité, cela ne fonctionnera pas tout à fait correctement. Une fois à l'intérieur de la vue partielle, le contexte de vue a changé et l'expression imbriquée profondément n'est plus présente. Poster de nouveau vers le modèle Theme ne serait pas correctement hydraté.

0 votes

C'est également ma préoccupation. Je ferais habituellement ce qui précède en tant qu'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 modifier chacun sur son propre formulaire. Je pense que vous risquez de vous perdre en essayant d'en faire trop sur une seule page en MVC.

2voto

Pranav Labhe Points 51

Lorsque vous utilisez une boucle foreach dans une vue pour un modèle lié ... Votre modèle est censé être au format listé.

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{
                   Aucun thème disponible
            }
        }

0 votes

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

0 votes

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

0voto

FirstDivision Points 259

Une autre possibilité beaucoup plus simple est qu'un de vos noms de propriété est incorrect (probablement celui que vous venez de modifier dans la classe). C'est ce qui s'est passé pour moi dans RazorPages .NET Core 3.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X