web-dev-qa-db-fra.com

Manière idiomatique correcte d'utiliser des modèles d'éditeur personnalisés avec des modèles IEnumerable dans ASP.NET MVC

Cette question fait suite à Pourquoi mon DisplayFor ne passe-t-il pas en revue mon IEnumerable <DateTime>?


Un rafraîchissement rapide.

Quand:

  • le modèle a une propriété de type IEnumerable<T>
  • vous passez cette propriété à Html.EditorFor() en utilisant la surcharge qui accepte uniquement l'expression lambda
  • vous avez un modèle d'éditeur pour le type T sous Vues/Shared/EditorTemplates

alors le moteur MVC invoquera automatiquement le modèle d'éditeur pour chaque élément de la séquence énumérable, produisant une liste des résultats.

Par exemple, lorsqu'il existe une classe de modèle Order avec la propriété Lines:

public class Order
{
    public IEnumerable<OrderLine> Lines { get; set; }
}

public class OrderLine
{
    public string Prop1 { get; set; }
    public int Prop2 { get; set; }
}

Et il y a une vue Views/Shared/EditorTemplates/OrderLine.cshtml:

@model TestEditorFor.Models.OrderLine

@Html.EditorFor(m => m.Prop1)
@Html.EditorFor(m => m.Prop2)

Ensuite, lorsque vous appelez @Html.EditorFor(m => m.Lines) à partir de la vue de niveau supérieur, vous obtiendrez une page avec des zones de texte pour chaque ligne de commande, pas une seule.


Cependant, comme vous pouvez le voir dans la question liée, cela ne fonctionne que lorsque vous utilisez cette surcharge particulière de EditorFor. Si vous fournissez un nom de modèle (afin d'utiliser un modèle qui n'est pas nommé d'après la classe OrderLine), la gestion automatique de la séquence ne se produira pas et une erreur d'exécution se produira à la place.

À ce stade, vous devrez déclarer le modèle de votre modèle personnalisé comme IEnumebrable<OrderLine> Et itérer manuellement sur ses éléments d'une manière ou d'une autre pour les afficher tous, par exemple.

@foreach (var line in Model.Lines) {
    @Html.EditorFor(m => line)
}

Et c'est là que les problèmes commencent.

Les contrôles HTML générés de cette manière ont tous les mêmes identifiants et noms. Lorsque vous les POST eux plus tard, le classeur de modèle ne sera pas en mesure de construire un tableau de OrderLines, et l'objet de modèle que vous obtenez dans la méthode HttpPost dans le contrôleur sera null.
Cela a du sens si vous regardez l'expression lambda - elle ne relie pas vraiment l'objet en cours de construction à un endroit dans le modèle dont il provient.

J'ai essayé différentes manières d'itérer sur les éléments, et il semblerait que la seule façon soit de redéclarer le modèle du modèle comme IList<T> Et de l'énumérer avec for:

@model IList<OrderLine>

@for (int i = 0; i < Model.Count(); i++)
{ 
    @Html.EditorFor(m => m[i].Prop1)
    @Html.EditorFor(m => m[i].Prop2)
}

Ensuite, dans la vue de niveau supérieur:

@model TestEditorFor.Models.Order

@using (Html.BeginForm()) {
    @Html.EditorFor(m => m.Lines, "CustomTemplateName")
}

qui donne des contrôles HTML correctement nommés qui sont correctement reconnus par le classeur de modèle sur une soumission.


Bien que cela fonctionne, cela semble très mal.

Quelle est la manière idiomatique correcte d'utiliser un modèle d'éditeur personnalisé avec EditorFor, tout en préservant tous les liens logiques qui permettent au moteur de générer du code HTML adapté au classeur de modèle?

49
GSerg

Après discussion avec Erik Funkenbusch , ce qui a conduit à examiner le code source MVC , il semblerait qu'il existe deux façons plus agréables (correctes et idiomatiques?) De le faire.

Les deux impliquent de fournir le préfixe de nom html correct à l'assistant et génèrent du HTML identique à la sortie du EditorFor par défaut.

Je vais le laisser ici pour l'instant, je ferai plus de tests pour m'assurer qu'il fonctionne dans des scénarios profondément imbriqués.

Pour les exemples suivants, supposons que vous disposez déjà de deux modèles pour la classe OrderLine: OrderLine.cshtml et DifferentOrderLine.cshtml.


Méthode 1 - Utilisation d'un modèle intermédiaire pour IEnumerable<T>

Créez un modèle d'assistance, enregistrez-le sous n'importe quel nom (par exemple, "ManyDifferentOrderLines.cshtml"):

@model IEnumerable<OrderLine>

@{
    int i = 0;

    foreach (var line in Model)
    { 
        @Html.EditorFor(m => line, "DifferentOrderLine", "[" + i++ + "]")
    }
}

Appelez-le ensuite à partir du modèle de commande principal:

@model Order

@Html.EditorFor(m => m.Lines, "ManyDifferentOrderLines")

Méthode 2 - Sans modèle intermédiaire pour IEnumerable<T>

Dans le modèle de commande principal:

@model Order

@{
    int i = 0;

    foreach (var line in Model.Lines)
    {
        @Html.EditorFor(m => line, "DifferentOrderLine", "Lines[" + i++ + "]")
    }
}
32
GSerg

Il ne semble pas y avoir de moyen plus simple d'y parvenir que décrit dans la réponse de @GSerg . Étrange que l'équipe MVC n'ait pas trouvé une manière moins compliquée de le faire. J'ai créé cette méthode d'extension pour l'encapsuler au moins dans une certaine mesure:

public static MvcHtmlString EditorForEnumerable<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, IEnumerable<TValue>>> expression, string templateName)
{
    var fieldName = html.NameFor(expression).ToString();
    var items = expression.Compile()(html.ViewData.Model);
    return new MvcHtmlString(string.Concat(items.Select((item, i) => html.EditorFor(m => item, templateName, fieldName + '[' + i + ']'))));
}
3
Anders

Il existe plusieurs façons de résoudre ce problème. Il n'y a aucun moyen d'obtenir la prise en charge IEnumerable par défaut dans les modèles d'éditeur lorsque vous spécifiez un nom de modèle dans EditorFor. Tout d'abord, je suggérerais que si vous avez plusieurs modèles pour le même type dans le même contrôleur, votre contrôleur a probablement trop de responsabilités et vous devriez envisager de le refactoriser.

Cela dit, la solution la plus simple est un DataType personnalisé . MVC utilise des DataTypes en plus des UIHints et des noms de type. Voir:

EditorTemplate personnalisé non utilisé dans MVC4 pour DataType.Date

Donc, il suffit de dire:

[DataType("MyCustomType")]
public IEnumerable<MyOtherType> {get;set;}

Ensuite, vous pouvez utiliser MyCustomType.cshtml dans vos modèles d'éditeur. Contrairement à UIHint, cela ne souffre pas du manque de support IEnuerable . Si votre utilisation prend en charge un type par défaut (par exemple, Téléphone ou E-mail, alors préférez utiliser l'énumération de type existante à la place). Vous pouvez également dériver votre propre attribut DataType et utiliser DataType.Custom comme base.

Vous pouvez également simplement envelopper votre type dans un autre type pour créer un modèle différent. Par exemple:

public class MyType {...}
public class MyType2 : MyType {}

Ensuite, vous pouvez créer un MyType.cshtml et MyType2.cshtml assez facilement, et vous pouvez toujours traiter un MyType2 comme un MyType dans la plupart des cas.

Si c'est trop "hackish" pour vous, vous pouvez toujours construire votre modèle pour un rendu différent en fonction des paramètres passés via le paramètre "additionalViewData" du modèle d'éditeur.

Une autre option serait d'utiliser la version où vous passez le nom du modèle pour effectuer la "configuration" du type, comme créer des balises de table ou d'autres types de mise en forme, puis utiliser la version de type plus générique pour afficher uniquement les éléments de ligne dans un forme plus générique de l'intérieur du modèle nommé.

Cela vous permet d'avoir un modèle CreateMyType et un modèle EditMyType qui sont différents à l'exception des éléments de campagne individuels (que vous pouvez combiner avec la suggestion précédente).

Une autre option est, si vous n'utilisez pas de modèles d'affichage pour ce type, vous pouvez utiliser DisplayTempates pour votre modèle alternatif (lors de la création d'un modèle personnalisé, ce n'est qu'une convention .. lors de l'utilisation du modèle intégré, il créera simplement versions d'affichage). Certes, cela est contre-intuitif mais cela résout le problème si vous n'avez que deux modèles pour le même type que vous devez utiliser, sans modèle d'affichage correspondant.

Bien sûr, vous pouvez toujours simplement convertir l'IEnumerable en un tableau dans le modèle, ce qui ne nécessite pas de redéclarer le type de modèle.

@model IEnumerable<MyType>
@{ var model = Model.ToArray();}
@for(int i = 0; i < model.Length; i++)
{
    <p>@Html.TextBoxFor(x => model[i].MyProperty)</p>
}

Je pourrais probablement penser à une douzaine d'autres façons de résoudre ce problème, mais en réalité, chaque fois que je l'ai eu, j'ai trouvé que si j'y pense, je peux simplement repenser mon modèle ou mes vues dans un tel de manière à ne plus l'exiger.

En d'autres termes, je considère que ce problème est une "odeur de code" et un signe que je fais probablement quelque chose de mal, et repenser le processus donne généralement une meilleure conception qui n'a pas le problème.

Donc pour répondre à votre question. La manière idiomatique correcte serait de reconcevoir vos contrôleurs et vues afin que ce problème n'existe pas. à moins que, choisissez le "hack" le moins offensant pour réaliser ce que vous voulez .

3
Erik Funkenbusch