Imaginez un scénario courant, voici une version simplifiée de ce que je rencontre. En fait, j'ai plusieurs couches de nidification sur la mienne ...
Mais c'est le scénario
Thème contient la liste Catégorie contient la liste Produit contient la liste
Mon contrôleur fournit un thème entièrement rempli, avec toutes les catégories de ce thème, les produits de ces catégories et leurs commandes.
La collection d'ordres a une propriété appelée Quantité (parmi beaucoup d'autres) qui doit être éditable.
@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 alors que je ne reçoive qu'une référence à l'objet modèle supérieur, "Thème" 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 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 explicitement les arguments de type.
Merci.
La réponse rapide consiste à 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 masque pourquoi cela résout le problème.
Vous devez au moins comprendre sommairement trois choses avant de pouvoir résoudre ce problème. Je dois admettre que je cargo-culted cela depuis longtemps quand j'ai commencé à travailler avec le framework. Et il m'a fallu un bon bout de temps pour comprendre ce qui se passait.
Ces trois choses sont:
LabelFor
et les autres aides ...For
Fonctionnent-ils dans MVC?Ces trois concepts sont liés pour obtenir une réponse.
LabelFor
et les autres aides ...For
Fonctionnent-ils dans MVC?Donc, vous avez utilisé les extensions HtmlHelper<T>
Pour LabelFor
et TextBoxFor
et autres, et vous avez probablement remarqué que lorsque vous les appelez, vous leur transmettez un lambda et il comme par magie génère du HTML. Mais comment?
La première chose à noter 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, il s'agit d'une méthode d'extension pour un HtmlHelper
fortement typé, de type <TModel>
. Donc, pour simplement dire ce qui se passe dans les coulisses, lorsque rasoir 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
, raison pour laquelle vous pouvez utiliser @Html...
), Où TModel
correspond au type défini. dans votre déclaration @model
. Donc, dans votre cas, lorsque vous regardez cette vue, TModel
sera toujours du type ViewModels.MyViewModels.Theme
.
Maintenant, le prochain argument est un peu délicat. Voyons donc une invocation
@Html.TextBoxFor(model=>model.SomeProperty);
Il semble que nous ayons un petit lambda. Et si l’on devine la signature, on pourrait penser que le type de cet argument serait simplement un Func<TModel, TProperty>
, Où TModel
est le type du view model et TProperty
sont déduits du type de la propriété.
Mais ce n’est pas tout à fait correct, si vous regardez le type actuel de l’argument son Expression<Func<TModel, TProperty>>
.
Ainsi, lorsque vous générez normalement un lambda, le compilateur 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 un Expression<>
, Il ne compile pas immédiatement le lambda dans MSIL, il génère plutôt un arbre d'expressions!
Alors qu'est-ce que c'est que 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 le code dans une structure de données en forme d'arborescence, 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 sous la forme d'une collection "d'actions".
Dans le cas de model=>model.SomeProperty
, L'arbre des expressions comporterait un nœud indiquant: "Obtenir 'une propriété' à partir d'un 'modèle'"
Cet arbre d'expression peut être compilé en 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.
Donc Func<>
Ou Action<>
, Une fois que vous les avez, ils sont plus ou moins 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<>>
, En revanche, représente une collection d'actions pouvant être ajoutées, manipulées, visité , ou compilé et invoquées.
Donc, avec cette compréhension de ce qu'est un Expression<>
, Nous pouvons revenir à Html.TextBoxFor
. Lors du rendu d'une zone de texte, il doit générer quelques éléments relatifs à la propriété que vous lui attribuez. Des choses comme attributes
sur la propriété pour validation, et plus précisément dans ce cas, il faut déterminer ce qui doit nommer le <input>
tag.
Pour ce faire, il "marche" dans l'arbre d'expression et construit un nom. Donc, pour une expression comme model=>model.SomeProperty
, Elle parcourt 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]" />
Avoir un sens? Ce n'est pas seulement le travail que fait le Func<>
, Mais comment il fait que son travail est important ici.
(Notez que d'autres frameworks tels que LINQ to SQL font des choses similaires en parcourant un arbre d'expression et en construisant une grammaire différente, qu'il s'agisse ici d'une requête SQL)
Donc, une fois que vous avez compris cela, nous devons parler brièvement du classeur type. Lorsque le formulaire est posté, c'est simplement comme un appartement Dictionary<string, string>
, Nous avons perdu la structure hiérarchique que notre modèle de vue imbriquée pouvait avoir. Le classeur de modèles a pour tâche de prendre cette combinaison de paires clé-valeur et de tenter 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é postée.
Donc, si le post de formulaire ressemble à
Foo.Bar.Baz.FooBar = Hello
Et vous publiez sur un modèle appelé SomeViewModel
, alors il fait l'inverse de ce que l'aide a fait en premier lieu. Il cherche une propriété appelée "Foo". Ensuite, il recherche une propriété appelée "Bar" dans "Foo", puis "Baz" ... et ainsi de suite ...
Enfin, il essaie d'analyser la valeur dans le type de "FooBar" et de l'attribuer à "FooBar".
PHEW !!!
Et voila, vous avez votre modèle. L'instance que le modèle de classeur qui vient d'être construit est transmise à l'action demandée.
Votre solution ne fonctionne donc pas car les aides Html.[Type]For()
ont besoin d'une expression. Et vous leur donnez simplement une valeur. Il n'a aucune idée du contexte de cette valeur et ne sait pas quoi en faire.
Maintenant, certaines personnes ont suggéré d'utiliser des partiels pour le rendu. En théorie, cela fonctionnera, mais probablement pas comme vous le souhaitez. Lorsque vous rendez un partiel, vous modifiez le type de TModel
, car vous vous trouvez 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, celle-ci sera superficielle. Il ne générera que sur la base de l'expression donnée (et non du contexte entier).
Disons que vous avez un partiel qui vient de rendre "Baz" (d'après notre exemple précédent). À l'intérieur de ce partiel, vous pouvez 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" />
Si vous postez ce formulaire dans une action qui attend un ViewModel profondément imbriqué, il essaiera d'hydrater une propriété appelée FooBar
sur off de TModel
. Ce qui, au mieux, n’est pas là, et au pire, c’est autre chose. Si vous publiez une action spécifique qui accepte un Baz
, plutôt que le modèle racine, cela fonctionnerait très 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 qui publient tous des actions différentes, le rendu d'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 avec Expression<>
, En les développant par programme et en faisant d’autres choses intéressantes avec eux. Je ne vais pas entrer dans cela. Mais, espérons-le, cela vous permettra de mieux comprendre ce qui se passe dans les coulisses et pourquoi les choses agissent comme elles sont.
Vous pouvez simplement utiliser EditorTemplates pour cela. Vous devez créer un répertoire nommé "EditorTemplates" dans le dossier d'affichage de votre contrôleur et placer une vue distincte pour chacune de vos entités imbriquées (nommé 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 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. Ainsi, vous n'aurez plus de problème pour extraire l'entité Thème publiée dans son ensemble.
Vous pouvez ajouter une catégorie partielle et une partielle de produit. Chacune d’entre elles prendrait une plus petite partie du modèle principal car son propre modèle, c’est-à-dire que le type de modèle de la catégorie pourrait être un IEnumerable, vous lui transmettriez Model.Theme. Le produit partiel peut être un IEnumerable dans lequel vous transmettez Model.Products (à partir du produit partiel de la catégorie).
Je ne suis pas sûr que ce soit la bonne façon d'avancer, mais j'aimerais savoir.
EDIT
Depuis que j'ai posté cette réponse, j'ai utilisé EditorTemplates et je trouve que c'est le moyen le plus simple de gérer des éléments ou groupes répétitifs. Il gère automatiquement tous vos problèmes de message de validation et de soumission de formulaire/liaison de modèle.
Lorsque vous utilisez une boucle foreach dans la vue pour un 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>
}
}
C'est clair de l'erreur.
HtmlHelpers ajouté avec "For" attend l'expression lambda en tant que paramètre.
Si vous transmettez directement la valeur, utilisez plutôt la valeur Normal.
par exemple.
Au lieu de TextboxFor (....), utilisez Textbox ()
la syntaxe pour TextboxFor sera comme Html.TextBoxFor (m => m.Property)
Dans votre scénario, vous pouvez utiliser basic for for, car cela 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)
}
}
}