Partager via


ASP.NET MVC

Les fonctionnalités et problèmes de la liaison de modèle ASP.NET MVC

Jess Chadwick

La liaison de modèle ASP.NET MVC simplifie les actions de contrôleur en introduisant une couche d'abstraction qui remplit automatiquement les paramètres d'action du contrôleur, en se chargeant du mappage traditionnel des propriétés et du code de conversion de type généralement impliqués dans l'utilisation des données de requête ASP.NET. Bien que la liaison de modèle semble simple, il s'agit en fait d'une infrastructure relativement complexe composée d'un certain nombre de parties qui collaborent de façon à créer et remplir les objets requis par les actions de contrôleur.

Cet article vous emmène au cœur du sous-système de la liaison de modèle ASP.NET MVC et vous permet de découvrir chaque couche de l'infrastructure de la liaison de modèle, ainsi que les différentes façons dont vous pouvez étendre la logique de la liaison de modèle afin de répondre aux besoins de votre application. Il présente également quelques techniques de liaison de modèle souvent oubliées, ainsi que les méthodes permettant d'éviter certaines des erreurs de liaison de modèle les plus courantes.

Notions de base sur la liaison de modèle

Pour comprendre ce qu'est la liaison de modèle, commençons par examiner une méthode type permettant de remplir un objet à partir des valeurs de requête dans une application ASP.NET, comme illustré à la figure 1.

Figure 1 Extraction des valeurs directement depuis la requête

public ActionResult Create()
{
  var product = new Product() {
    AvailabilityDate = DateTime.Parse(Request["availabilityDate"]),
    CategoryId = Int32.Parse(Request["categoryId"]),
    Description = Request["description"],
    Kind = (ProductKind)Enum.Parse(typeof(ProductKind), 
                                   Request["kind"]),
    Name = Request["name"],
    UnitPrice = Decimal.Parse(Request["unitPrice"]),
    UnitsInStock = Int32.Parse(Request["unitsInStock"]),
 };
 // ...
}

Comparons ensuite l'action de la figure 1 à celle de la figure 2, dans laquelle la liaison de modèle donne le même résultat.

Figure 2 Liaison de modèle aux valeurs primitives

public ActionResult Create(
  DateTime availabilityDate, int categoryId,
    string description, ProductKind kind, string name,
    decimal unitPrice, int unitsInStock
  )
{
  var product = new Product() {
    AvailabilityDate = availabilityDate,
    CategoryId = categoryId,
    Description = description,
    Kind = kind,
    Name = name,
    UnitPrice = unitPrice,
    UnitsInStock = unitsInStock,
 };
 
 // ...
}

Bien que les deux exemples donnent le même résultat, c'est-à-dire une instance Product remplie, la figure 2 repose sur ASP.NET MVC pour convertir les valeurs de la requête en valeurs fortement typées. Avec la liaison de modèle, les actions de contrôleur peuvent être concentrées sur l'apport d'une valeur ajoutée et permettent d'éviter de perdre du temps avec l'analyse et le mappage traditionnels des requêtes.

Liaison à des objets complexes

Bien que la liaison de modèle à des modèles mêmes simples et primitifs puisse avoir un impact relativement important, la plupart des actions de contrôleur reposent sur plus de deux paramètres. Fort heureusement, ASP.NET MVC gère les types complexes aussi bien que les types primitifs.

Le code suivant requiert un passage de plus lors de l'action Create. Les valeurs primitives sont ainsi ignorées et la liaison se fait directement avec la classe Product :

public ActionResult Create(Product product)
{
  // ...
}

Là encore, ce code donne le même résultat que les actions de la figure 1 et de la figure 2, mais cette fois aucun code n'a été impliqué : la liaison de modèle ASP.NET MVC complexe a éliminé tout le code réutilisable nécessaire pour la création et le remplissage d'une nouvelle instance Product. Ce code illustre la véritable puissance de la liaison de modèle.

Décomposition de la liaison de modèle

Maintenant que vous avez vu la liaison de modèle en action, il est temps de décomposer les différentes parties qui constituent l'infrastructure de la liaison de modèle.

La liaison de modèle est composée de deux étapes distinctes : la collecte des valeurs de la requête et le remplissage des modèles avec ces valeurs. Ces étapes sont réalisées respectivement par des fournisseurs de valeurs et des binders de modèles.

Fournisseurs de valeurs

ASP.NET MVC comprend des implémentations de fournisseurs de valeurs qui couvrent la plupart des sources courantes de valeurs de requête, telles que les paramètres de la querystring, les champs de formulaire et les données de routage. Lors de l'exécution, ASP.NET MVC utilise les fournisseurs de valeurs enregistrés dans la classe ValueProviderFactories afin d'évaluer les valeurs de requête que les binders de modèles peuvent utiliser.

Par défaut, la collection de fournisseurs de valeurs évalue les valeurs provenant de diverses sources dans l'ordre suivant :

  1. Paramètres d'action liés précédemment, lorsqu'il s'agit d'une action enfant
  2. Champs de formulaire (Request.Form)
  3. Valeurs de propriété dans le corps de la requête JSON (Request.InputStream), mais uniquement lorsqu'il s'agit d'une requête AJAX
  4. Données de routage (RouteData.Values)
  5. Paramètres de la querystring (Request.QueryString)
  6. Fichiers publiés (Request.Files)

La collection des fournisseurs de valeurs, comme l'objet Request, n'est en fait qu'un dictionnaire idéalisé, une couche d'abstraction des paires clé/valeur que les binders de modèles peuvent utiliser sans avoir besoin de connaître l'origine des données. Toutefois, l'infrastructure du fournisseur de valeurs améliore cette abstraction au-delà du dictionnaire Request et vous donne ainsi un contrôle total sur la façon dont et l'endroit où l'infrastructure de liaison de modèle obtient ses données. Vous pouvez même créer vos propres fournisseurs de valeurs personnalisés.

Fournisseurs de valeurs personnalisés

La condition minimum de création d'un fournisseur de valeurs personnalisé est relativement simple : Créez une nouvelle classe qui implémente l'interface System.Web.Mvc.ValueProviderFactory.

Par exemple, la figure 3 illustre le cas d'un fournisseur de valeurs personnalisé qui extrait les valeurs des cookies de l'utilisateur.

Figure 3 Fabrique de fournisseurs de valeurs personnalisés qui inspecte les valeurs de cookies

public class CookieValueProviderFactory : ValueProviderFactory
{
  public override IValueProvider GetValueProvider
  (
    ControllerContext controllerContext
  )
  {
    var cookies = controllerContext.HttpContext.Request.Cookies;
 
    var cookieValues = new NameValueCollection();
    foreach (var key in cookies.AllKeys)
    {
      cookieValues.Add(key, cookies[key].Value);
    }
 
    return new NameValueCollectionValueProvider(
      cookieValues, CultureInfo.CurrentCulture);
  }
}

Remarquez la simplicité de CookieValueProviderFactory. Au lieu de créer totalement un nouveau fournisseur de valeurs, CookieValueProviderFactory se contente d'extraire les cookies de l'utilisateur et d'utiliser NameValueCollectionValueProvider pour exposer ces valeurs dans l'infrastructure de liaison de modèle.

Une fois que vous avez créé un fournisseur de valeurs personnalisé, vous devez l'ajouter à la liste des fournisseurs de valeurs via la collection ValueProviderFactories.Factories :

var factory = new CookieValueProviderFactory();
ValueProviderFactories.Factories.Add(factory);

Bien qu'il soit relativement facile de créer des fournisseurs de valeurs personnalisés, soyez attentif lors de cette opération. L'ensemble de fournisseurs de valeurs fournis par ASP.NET MVC expose relativement bien la plupart des données disponibles dans HttpRequest (peut-être à l'exception des cookies) et propose généralement suffisamment de données pour répondre à la majorité des scénarios.

Afin de déterminer si la création d'un nouveau fournisseur de valeurs est appropriée dans le cas de votre scénario, posez-vous la question suivante : l'ensemble d'informations proposé par les fournisseurs de valeurs existants contient-il toutes les données dont j'ai besoin (même si elles n'ont peut-être pas le format adéquat) ?

Si la réponse est non, l'ajout d'un fournisseur de valeurs personnalisé est probablement la bonne solution pour résoudre le problème de valeur nulle. Toutefois, si la réponse est oui, comme c'est souvent le cas, réfléchissez à la façon dont vous pouvez apporter les pièces manquantes en personnalisant le comportement de la liaison de modèle afin d'accéder aux données fournies par les fournisseurs de valeurs. Le reste de cet article vous montre comment effectuer cette opération.

Le composant principal de l'infrastructure de liaison de modèle ASP.NET MVC chargé de créer et de remplir les modèles à l'aide de valeurs fournies par les fournisseurs de valeurs est qualifié de binder de modèle.

Binder de modèle par défaut

L'infrastructure ASP.NET MVC comprend l'implémentation du binder de modèle par défaut nommé DefaultModelBinder, qui est conçu pour lier efficacement la plupart des types de modèles. Pour cela, elle a recours à une logique récursive relativement simple pour chaque propriété du modèle cible :

  1. Examinez les fournisseurs de services afin de voir si la propriété a été découverte comme type simple ou complexe en vérifiant si le nom de la propriété est enregistré comme préfixe. Les préfixes ne sont que la « notation par points »du nom de champ de formulaire HTML utilisée pour représenter si une valeur est la propriété d'un objet complexe. Le modèle de préfixe est [ParentProperty].[Property]. Par exemple, le champ de formulaire portant le nom UnitPrice.Amount contient la valeur du champ Amount de la propriété UnitPrice.
  2. Obtenez ValueProviderResult auprès des fournisseurs de valeurs enregistrés pour le nom de la propriété.
  3. Si la valeur est un type simple, tentez de la convertir vers le type cible. La logique de conversion par défaut utilise le TypeConverter de la propriété pour convertir depuis la valeur source de type string vers le type cible.
  4. Sinon, la propriété est un type complexe, effectuez alors une liaison récursive.

Liaison de modèle récursive

La liaison de modèle récursive redémarre efficacement la totalité du processus de liaison de modèle, mais utilise le nom de la propriété cible comme nouveau préfixe. À l'aide de cette approche, le DefaultModelBinder est capable de traverser des graphiques d'objets complexes complets et de remplir jusqu'aux valeurs de propriété profondément imbriquées.

Pour voir la liaison récursive en action, remplacez le type décimal simple de Product.UnitPrice par le type personnalisé Currency. La figure 4 illustre les deux classes.

Figure 4 Classe Product avec la propriété complexe Unitprice

public class Product
{
  public DateTime AvailabilityDate { get; set; }
  public int CategoryId { get; set; }
  public string Description { get; set; }
  public ProductKind Kind { get; set; }
  public string Name { get; set; }
  public Currency UnitPrice { get; set; }
  public int UnitsInStock { get; set; }
}
 
public class Currency
{
  public float Amount { get; set; }
  public string Code { get; set; }
}

Une fois cette mise à jour implémentée, le binder de modèle recherche les valeurs nommées UnitPrice.Amount et UnitPrice.Code pour remplir la propriété complexe Product.UnitPrice.

La logique de la liaison récursive du DefaultModelBinder peut remplir efficacement même les graphiques d'objets les plus complexes. Jusqu'à présent, vous avez vu un objet complexe qui se trouvait à un premier niveau de la hiérarchie d'objets et le DefaultModelBinder l'a traité avec facilité. Pour illustrer la véritable puissance de la liaison de modèle récursive, ajoutez une nouvelle propriété nommée Child à Product avec le même type, Product :

public class Product {
  public Product Child { get; set; }
  // ...
}

Ajoutez ensuite un champ au formulaire puis, en appliquant la notation par points pour indiquer chaque niveau, créez autant de niveaux que vous le souhaitez. Par exemple :

<input type="text" name="Child.Child.Child.Child.Child.Child.Name"/>

Ce champ de formulaire va créer six niveaux de Products. Pour chaque niveau, le DefaultModelBinder crée consciencieusement une nouvelle instance Product et commence directement à lier ses valeurs. Une fois que le binder a complètement terminé, il a créé un graphique d'objet qui ressemble à la figure 5.

Figure 5 Un graphique d'objet créé par la liaison de modèle récursive

new Product {
  Child = new Product { 
    Child = new Product {
      Child = new Product {
        Child = new Product {
          Child = new Product {
            Child = new Product {
              Name = "MADNESS!"
            }
          }
        }
      }
    }
  }
}

Bien que cet exemple fictif définisse la valeur d'une seule propriété, il illustre parfaitement la façon dont la fonctionnalité de liaison de modèle récursive de DefaultModelBinder permet à ce dernier de prendre en charge directement certains graphiques d'objets très complexes. Avec la liaison de modèle récursive, si vous pouvez créer un nom de champ de formulaire pour représenter la valeur à remplir, peu importe l'emplacement où se trouve cette valeur dans la hiérarchie d'objets, le binder de modèle la trouvera et la liera.

Où le modèle de liaison semble s'écrouler

C'est vrai : le DefaultModelBinder ne pourra tout simplement pas lier certains modèles. Toutefois, il existe un certain nombre de scénarios dans lesquels la logique de liaison du modèle par défaut ne semble pas fonctionner alors qu'elle fonctionne en fait très bien tant que vous l'utilisez de façon appropriée.

Voici certains des scénarios les plus courants que les développeurs ne pensent généralement pas pouvoir gérer avec le DefaultModelBinder. Vous allez découvrir comment vous pouvez les implémenter à l'aide du DefaultModelBinder et rien d'autre.

Collections complexes Les fournisseurs de valeurs ASP.NET MVC prédéfinis traitent tous les noms des champs de requête comme s'il s'agissait de valeurs de publication de formulaire. Prenons par exemple une collection de valeurs primitives dans une publication de formulaire dans laquelle chaque valeur requiert son propre index unique (espace ajouté pour faciliter la lecture) :

MyCollection[0]=one &
MyCollection[1]=two &
MyCollection[2]=three

La même approche peut également être appliquée aux collections d'objets complexes. Pour illustrer ce point, mettez à jour la classe Product de façon à prendre en charge plusieurs devises en remplaçant la propriété UnitPrice par une collection d'objets Currency :

public class Product : IProduct
{
  public IEnumerable<Currency> UnitPrice { get; set; }
 
  // ...
}

Avec cette modification, les paramètres de requête suivants sont obligatoires pour remplir la propriété UnitPrice mise à jour :

UnitPrice[0].Code=USD &
UnitPrice[0].Amount=100.00 &

UnitPrice[1].Code=EUR &
UnitPrice[1].Amount=73.64

Prêtez attention à la syntaxe d'affectation des noms des paramètres de la requête nécessaires pour lier les collections d'objets complexes. Remarquez les indexeurs utilisés pour identifier chaque élément de la zone. Notez également que chaque propriété de chaque instance doit contenir la référence indexée complète vers cette instance. Gardez à l'esprit que le binder de modèle s'attend à ce que les noms de propriété suivent la syntaxe d'affectation des noms de la publication de formulaire, que la requête soit un GET ou un POST.

Bien que cela soit quelque peu surprenant, les requêtes JSON ont les mêmes exigences : elles doivent également respecter la syntaxe d'affectation des noms de publication de formulaire. Prenons l'exemple de la charge utile JSON pour la collection UnitPrice précédente. La syntaxe de tableau JSON pure pour ces données serait représentée de la façon suivante :

[ 
  { "Code": "USD", "Amount": 100.00 },
  { "Code": "EUR", "Amount": 73.64 }
]

Toutefois, les fournisseurs de valeurs et les binders de modèles par défaut requièrent la représentation des données sous forme de publication de formulaire JSON :

{
  "UnitPrice[0].Code": "USD",
  "UnitPrice[0].Amount": 100.00,

  "UnitPrice[1].Code": "EUR",
  "UnitPrice[1].Amount": 73.64
}

Le scénario de collection d'objets complexe est peut-être celui qui pose le plus de problèmes aux développeurs dans la mesure où la syntaxe n'est pas nécessairement évidente pour tous les développeurs. Toutefois, une fois que vous avez appris la syntaxe relativement simple de publication des collections complexes, ces scénarios deviennent nettement plus simples à gérer.

Binders de modèles personnalisés génériques Bien que le DefaultModelBinder soit suffisamment puissant pour gérer presque tout ce que vous lui envoyez, il arrive qu'il ne fasse tout simplement pas ce dont vous avez besoin. Lorsque ces scénarios se produisent, la plupart des développeurs sautent sur l'occasion pour profiter du modèle d'extensibilité de l'infrastructure de liaison de modèle et créer leur propre binder de modèle personnalisé.

Par exemple, bien que Microsoft .NET Framework offre une excellente prise en charge des principes orientés objet, le DefaultModelBinder ne propose aucune prise en charge de la liaison à des classes de base abstraites et à des interfaces. Pour illustrer cet inconvénient, refactorisez la classe Product de façon à ce qu'elle soit dérivée d'une interface, nommée IProduct, composée de propriétés en lecture seule. De la même manière, mettez à jour l'action de contrôleur Create afin qu'elle accepte la nouvelle interface IProduct au lieu de l'implémentation concrète de Product, comme illustré à la figure 6.

Figure 6 Liaison à une interface

public interface IProduct
{
  DateTime AvailabilityDate { get; }
  int CategoryId { get; }
  string Description { get; }
  ProductKind Kind { get; }
  string Name { get; }
  decimal UnitPrice { get; }
  int UnitsInStock { get; }
}
 
public ActionResult Create(IProduct product)
{
  // ...
}

L'action Create mise à jour illustrée à la figure 6 entraîne la levée d'une exception par le DefaultModelBinder, bien qu'il s'agisse d'un code en C# parfaitement légitime : « Cannot create an instance of an interface ». Il est tout à fait compréhensible que le binder de modèle lance cette exception, si l'on considère que le DefaultModelBinder n'a aucun moyen de savoir quel type concret d'IProduct créer.

La méthode la plus simple pour résoudre ce problème consiste à créer un binder de modèle personnalisé qui implémente l'interface IModelBinder. La figure 7 illustre l'exemple de ProductModelBinder, un binder de modèle personnalisé qui sait créer et lier une instance de l'interface IProduct.

Figure 7 ProductModelBinder - Un binder de modèle personnalisé fortement couplé

public class ProductModelBinder : IModelBinder
{
  public object BindModel
    (
      ControllerContext controllerContext,
      ModelBindingContext bindingContext
    )
  {
    var product = new Product() {
      Description = GetValue(bindingContext, "Description"),
      Name = GetValue(bindingContext, "Name"),
  }; 
 
    string availabilityDateValue = 
      GetValue(bindingContext, "AvailabilityDate");

    if(availabilityDateValue != null)
    {
      DateTime date;
      if (DateTime.TryParse(availabilityDateValue, out date))
      product.AvailabilityDate = date;
    }
 
    string categoryIdValue = 
      GetValue(bindingContext, "CategoryId");

    if (categoryIdValue != null)
    {
      int categoryId;
      if (Int32.TryParse(categoryIdValue, out categoryId))
      product.CategoryId = categoryId;
    }
 
    // Repeat custom binding code for every property
    // ...
 
    return product;
  }
 
  private string GetValue(
    ModelBindingContext bindingContext, string key)
  {
    var result = bindingContext.ValueProvider.GetValue(key);
    return (result == null) ? null : result.AttemptedValue;
  }
}

L'inconvénient de la création de binders de modèles personnalisés qui implémentent directement l'interface IModelBinder est que ces binders dupliquent souvent une grande partie du DefaultModelBinder uniquement pour modifier quelques zones de logique. Il est également courant que ces binders personnalisés se concentrent sur des classes de modèles spécifiques. Ils créent ainsi un couplage fort entre l'infrastructure et la couche métier, et limitent la réutilisation afin de prendre en charge d'autres types de modèles.

Pour éviter tous ces problèmes dans vos binders de modèles personnalisés, pensez à dériver à partir du DefaultModelBinder et à remplacer des comportements spécifiques afin de répondre à vos besoins. Cette approche permet souvent de bénéficier du meilleur des deux mondes.

Binder de modèle abstrait Le seul problème lorsque l'on tente d'appliquer la liaison de modèle à une interface avec le DefaultModelBinder est qu'elle ignore comment déterminer le type de modèle concret. Envisagez un but de niveau plus élevé : la capacité de développer des actions de contrôleur par rapport à un type non concret et de déterminer dynamiquement le type concret pour chaque requête.

En dérivant depuis le DefaultModelBinder et en remplaçant uniquement la logique qui détermine le type de modèle cible, vous pouvez non seulement traiter le scénario IProduct, mais également créer un binder de modèle général capable de gérer aussi la plupart des autres hiérarchies d'interface. La figure 8 illustre un exemple de binder de modèle abstrait général.

Figure 8 Un binder de modèle abstrait général

public class AbstractModelBinder : DefaultModelBinder
{
  private readonly string _typeNameKey;

  public AbstractModelBinder(string typeNameKey = null)
  {
    _typeNameKey = typeNameKey ?? "__type__";
  }

  public override object BindModel
  (
    ControllerContext controllerContext,
    ModelBindingContext bindingContext
  )
  {
    var providerResult =
    bindingContext.ValueProvider.GetValue(_typeNameKey);

    if (providerResult != null)
    {
      var modelTypeName = providerResult.AttemptedValue;

      var modelType =
        BuildManager.GetReferencedAssemblies()
          .Cast<Assembly>()
          .SelectMany(x => x.GetExportedTypes())
          .Where(type => !type.IsInterface)
          .Where(type => !type.IsAbstract)
          .Where(bindingContext.ModelType.IsAssignableFrom)
          .FirstOrDefault(type =>
            string.Equals(type.Name, modelTypeName,
              StringComparison.OrdinalIgnoreCase));

      if (modelType != null)
      {
        var metaData =
        ModelMetadataProviders.Current
        .GetMetadataForType(null, modelType);

        bindingContext.ModelMetadata = metaData;
      }
    }

    // Fall back to default model binding behavior
    return base.BindModel(controllerContext, bindingContext);
  }
}

Pour prendre en charge la liaison de modèle à une interface, le binder de modèle doit d'abord convertir l'interface en type concret. Afin d'effectuer cette opération, AbstractModelBinder demande la clé « __type__ » aux fournisseurs de valeurs de la requête. L'utilisation des fournisseurs de valeurs pour ce type de données apporte une souplesse en terme d'emplacement de définition de la valeur « __type__ ». Par exemple, la clé peut être définie comme faisant partie du routage (dans les données de routage), spécifiée comme paramètre de la querystring ou même représentée comme champ dans les données de publication de formulaire.

Ensuite, AbstractModelBinder utilise le nom de type concret pour générer un nouvel ensemble de métadonnées qui décrit les détails de la classe concrète. AbstractModelBinder utilise ces nouvelles métadonnées pour remplacer la propriété ModelMetadata existante qui décrivait le type de modèle abstrait initial, ce qui forçait le binder de modèle à oublier qu'il avait été lié à un type non concret au départ.

Une fois qu'AbstractModelBinder a remplacé les métadonnées du modèle par toutes les informations nécessaires à la liaison au modèle approprié, il redonne simplement le contrôle à la logique du DefaultModelBinder de base afin de le laisser gérer le reste du travail.

AbstractModelBinder est un excellent exemple qui illustre la façon dont vous pouvez étendre la logique de liaison par défaut avec votre propre logique personnalisée sans réinventer la roue, en dérivant directement depuis l'interface IModelBinder.

Sélection du binder de modèle

La création des binders de modèles personnalisés n'est que la première étape. Pour appliquer la logique de liaison de modèle personnalisée dans votre application, vous devez également enregistrer les binders de modèles personnalisés. La plupart des didacticiels présentent deux méthodes permettant d'effectuer cette opération.

Collection ModelBinders globale La solution généralement recommandée pour remplacer le binder de modèle des types spécifiques consiste à enregistrer un mappage type-vers-binder dans le dictionnaire ModelBinders.Binders.

L'exemple de code suivant demande à l'infrastructure d'utiliser AbstractModelBinder pour lier les modèles Currency :

ModelBinders.Binders.Add(typeof(Currency), new AbstractModelBinder());

Replacement du modèle de binder par défaut Pour remplacer le gestionnaire global par défaut, une autre solution consiste à attribuer un binder de modèle à la propriété ModelBinders.Binders.DefaultBinder. Par exemple :

ModelBinders.Binders.DefaultBinder = new AbstractModelBinder();

Bien que ces deux approches fonctionnent bien pour la plupart des scénarios, ASP.NET MVC vous permet d'enregistrer un binder de modèle pour un type de deux autres façons : les attributs et les fournisseurs.

Attributs personnalisés pour agrémenter les modèles

Outre l'ajout d'un mappage de type au dictionnaire ModelBinders, l'infrastructure ASP.NET MVC offre également System.Web.Mvc.CustomModelBinderAttribute, un attribut qui vous permet de créer dynamiquement un binder de modèle pour chaque classe ou propriété à laquelle l'attribut est appliqué. La figure 9 présente l'implémentation d'un CustomModelBinderAttribute, qui crée un AbstractModelBinder.

Figure 9 Implémentation de CustomModelBinderAttribute

[AttributeUsage(
  AttributeTargets.Class | AttributeTargets.Enum |
  AttributeTargets.Interface | AttributeTargets.Parameter |
  AttributeTargets.Struct | AttributeTargets.Property,
  AllowMultiple = false, Inherited = false
)]
public class AbstractModelBinderAttribute : CustomModelBinderAttribute
{
  public override IModelBinder GetBinder()
  {
    return new AbstractModelBinder();
  }
}

Vous pouvez ensuite appliquer AbstractModelBinderAttribute à toutes les classes ou propriétés de modèle, de la façon suivante :

public class Product
{
  [AbstractModelBinder]
  public IEnumerable<CurrencyRequest> UnitPrice { get; set; }
  // ...
}

Désormais, lorsque le binder de modèle tente de rechercher le binder approprié pour Product.UnitPrice, il découvre AbstractModelBinderAttribute et utilise AbstractModelBinder pour lier la propriété Product.UnitPrice.

L'utilisation des attributs de binder de modèle personnalisés est une excellente méthode pour mettre en place une approche plus déclarative de la configuration des binders de modèles, sans nuire à la simplicité de la collection globale de binders de modèles. En outre, le fait que les attributs de binders de modèles personnalisés puissent être appliqués aux propriétés de classes et individuelles signifie que vous disposez d'un contrôle précis sur le processus de liaison de modèle.

Demandez aux binders !

Les fournisseurs de binders de modèles offrent la possibilité d'exécuter un code arbitraire en temps réel afin de déterminer le meilleur binder de modèle possible pour un type donné. En tant que tels, ils fournissent un excellent compromis entre l'enregistrement de binder de modèle explicite pour les types de modèles individuels, l'enregistrement statique reposant sur les attributs et un binder de modèle par défaut défini pour tous les types.

Le code suivant montre comment créer un IModelBinderProvider qui fournit un AbstractModelBinder pour toutes les interfaces et tous les types abstraits :

public class AbstractModelBinderProvider : IModelBinderProvider
{
  public IModelBinder GetBinder(Type modelType)
  {
    if (modelType.IsAbstract || modelType.IsInterface)
      return new AbstractModelBinder();
 
    return null;
  }
}

La logique qui dicte si AbstractModelBinder s'applique à un type de modèle donné est relativement simple : s'agit-il d'un type non concret ? Si tel est le cas, AbstractModelBinder est le binder de modèle approprié pour le type. Vous devez donc instancier le binder de modèle et le retourner. Si tel n'est pas le cas, AbstractModelBinder n'est pas approprié. Retournez alors une valeur null afin d'indiquer que le binder de modèle ne correspond pas pour ce type.

Dans le cadre de l'implémentation de la logique .GetBinder, gardez à l'esprit que cette dernière sera exécutée pour chaque propriété candidate à la liaison de modèle. Veillez donc à ce qu'elle soit légère, faute de quoi vous risquez d'introduire des problèmes de performances dans votre application.

Afin de commencer à utiliser un fournisseur de binders de modèles, ajoutez-le à une liste de fournisseurs gérée dans la collection ModelBinderProviders.BinderProviders. Par exemple, enregistrez AbstractModelBinder de la façon suivante :

var provider = new AbstractModelBinderProvider();
ModelBinderProviders.BinderProviders.Add(provider);

Et sans plus de complications, vous venez d'ajouter la prise en charge de la liaison de modèle pour les types non concrets dans toute votre application.

L'approche de la liaison de modèle rend la sélection de liaison de modèle nettement plus dynamique en évitant à l'infrastructure la charge de devoir déterminer quel est le binder de modèle approprié et en attribuant ce rôle aux éléments les plus appropriés : les binders de modèles eux-mêmes.

Points d'extensibilité clés

Comme toute autre méthode, la liaison de modèle ASP.NET MVC permet aux actions de contrôleur d'accepter des types d'objets complexes comme paramètres. La liaison de modèle favorise également une meilleure séparation des préoccupations en distinguant la logique de remplissage des objets de celle qui utilise les objets remplis.

J'ai exploré certains points d'extensibilité clés dans l'infrastructure de liaison de modèle qui vous permettront de l'utiliser au maximum de ses capacités. Prendre le temps de comprendre la liaison de modèle ASP.NET MVC et son utilisation correcte peut avoir un impact important, même sur les applications les plus simples.

Jess Chadwick est un consultant logiciel indépendant spécialisé dans les technologies Web. Il bénéficie d'une expérience de plus de dix ans dans le domaine du développement, allant des appareils intégrés dans les start-up aux batteries de serveurs Web d'entreprise auprès de sociétés figurant dans le classement Fortune 500. Il fait partie des ASPInsider, est Microsoft MVP en ASP.NET, rédige des livres et des articles pour des magazines. Il est activement impliqué dans la communauté de développement et fait régulièrement des présentations auprès de groupes d'utilisateurs et lors de conférences. En outre, il est à la tête du groupe d'utilisateurs .NET « NJDOTNET Central New Jersey ».

Merci à l'expert technique suivant d'avoir relu cet article : Phil Haack