ASP.NET MVC et les métas données

ASP.NET logo
Dans notre quotidien de développeur Web, nous sommes régulièrement amenés à devoir écrire et gérer des formulaires. Si vous connaissez un peu ASP.NET MVC, vous reconnaîtrez probablement que les deux fonctionnalités phares du Framework pour le développement de formulaires sont le Model Binding et les HTML Helpers. Aujourd’hui, je vous propose de découvrir comment ces deux éléments utilisent les métas donnés présentes sur nos modèles.

Pourquoi des métas données ?

Généralement, si je dois réaliser un formulaire ASP.NET MVC, je suis les étapes suivantes.

La création du modèle

Je crée une classe qui représente le modèle, ou plutôt le view-model, associé à mon formulaire. Si le formulaire représente la création d’un produit, alors je ferais une classe du type ProductCreationViewModel. Je n’utilise jamais directement une classe métier de mon application dans une vue ou en paramètre d’une action. Cela me permet de découpler mes vues du modèle métier, notamment pour simplifier le refactoring, limiter la surface d’attaque par over posting et surtout, d’enrichir mon modèle avec des éléments propres à l’affichage.

Voici donc un exemple de ce genre de modèle, enrichi grâce au mécanisme de Data Annotations du Framework .Net.

La création du contrôleur

Le contrôleur que j’écris contient deux méthodes, une première censée réagir au verbe GET qui s’affiche lorsque le formulaire vient d’être ouvert par un utilisateur, et une seconde, censée réagir au verbe POST, lorsque l’utilisateur essaye de valider la création d’un nouveau produit. C’est cette deuxième action qui est particulièrement intéressant car elle utilise le ModelBinding pour recevoir une instance de ma classe ProductCreationViewModel en paramètre et qu’elle consulte l’état de la validation de ce modèle par la propriété ModelState.IsValid.

La création de la vue

Dernière étape, il ne me reste plus qu’à créer la vue représentant le formulaire de création d’un produit. Plutôt que d’écrire ce formulaire en utilisant directement des tags HTML form, input, etc., je préfère tirer parti des HTML Helpers d’ASP.NET MVC. Ces derniers sont des méthodes d’extensions, utilisables avec n’importe quel moteur de vue, et qui agissent comme des contrôles Web. Ainsi, je peux utiliser un HTML Helper pour générer un label pour une propriété de mon modèle, mais aussi pour générer le champ de saisi associé à cette même propriété ou même un validateur côté client.

Le point fort d’ASP.NET MVC réside dans le fait que ces HTML Helpers savent analyser mon modèle et repérer les attributs Data Annotations et s’en servir pour plusieurs choses :

  • Utiliser la bonne chaîne de caractère (éventuellement une ressource) pour le label d’une propriété ;
  • Utiliser le bon type d’input pour une propriété ;
  • Générer le ou les bons validateurs côté client.

Dans ce genre de scénario, on voit clairement que c’est le fait d’avoir annoté mon modèle qui a facilité la génération de ma vue et la validation de mon modèle. Cependant, dans certains scénarios, il peut être problématique de figer ces annotations dans le code source de l’application. Le changement d’une règle entraîne automatiquement une recompilation de l’application, et le réglage de ces règles ne peut pas être externalisé dans un fichier de configuration…

Mais ce constat est sans compter le pouvoir d’extensibilité d’ASP.NET MVC ! En fait, il est possible de changer uniquement le mécanisme qui lit les métas donnés d’un modèle, sans risque de perdre la validation des instances de modèles hydratées par le ModelBinding et sans risque d’affecter le bon fonctionnement des HTML Helpers. Ce mécanisme est un fournisseur de métas données.

ASP.NET MVC et les fournisseurs de métas données

En fait, la fin du paragraphe précédent contient une exactitude : il n’est pas uniquement question de fournisseur de métas données. Lorsque l’on élabore une classe de modèle que l’on choisit d’enrichir avec les Data Annotations, on peut distinguer deux groupes d’attributs :

  • Les attributs d’affichage (type Display, Description, UIHint, etc.)
  • Les attributs de validation (type Required, StringLength, Compare, etc.)

Ainsi, on peut de la même manière distinguer deux types de fournisseur :

  • Les fournisseurs de métas données (MetadataProvider), qui correspondent grosso modo aux attributs d’affichage. Il ne peut y avoir qu’un seul fournisseur de métas données à la fois pour toute l’application.
  • Les fournisseurs de validation (ModelValidator), qui correspondent aux attributs de validation. Il peut y avoir plusieurs fournisseurs de validation qui cohabitent au sein de la même application.

Ainsi d’ASP.NET MVC, ces deux types de fournisseurs ne sont pas implémentés de la même manière et ne s’utilisent pas de la même manière.

Accéder aux métas données

La ligne de code suivante permet de récupérer le fournisseur de métas données utilisé par l’application.

var currentModelMetadataProvider = ModelMetadataProviders.Current;

Pour implémenter un fournisseur de métas données, il suffit d’hériter directement ou indirectement de la classe ModelMetadataProvider. Cette classe, abstraite, impose l’implémentation des trois méthodes publiques suivantes.

public abstract class ModelMetadataProvider
{
    public abstract IEnumerable<ModelMetadata> GetMetadataForProperties(object container, Type containerType);
    public abstract ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, string propertyName);
    public abstract ModelMetadata GetMetadataForType(Func<object> modelAccessor, Type modelType);
}

Par exemple, le fournisseur utilisé par défaut par ASP.NET MVC est de type CachedDataAnnotationsModelMetadataProvider.

En interne, les HTML Helpers n’appellent pas directement ces méthodes. Il existe en fait une méthode statique sur la classe ModelMetadata qui permet d’encapsuler l’appel au fournisseur de métas données tout en continuant à manipuler une expression. Ainsi, il n’est pas nécessaire d’aller inspecter soi-même le contenu de l’expression pour récupérer le nom de la propriété ciblée par le helper avant de faire un appel à la méthode GetMetadataForProperty du ModelMetadataProvider.

L’exemple ci-dessous représente l’utilisation de cette méthode.

var modelMetadata = ModelMetadata.FromLambdaExpression<Product, string>(p => p.Name, new ViewDataDictionary<Product>());

A partir de là, la classe ModelMetadata permet d’accéder aux éléments suivants (entres autres).

public class ModelMetadata
{        
    public virtual string DataTypeName { get; set; }

    public virtual string Description { get; set; }

    public virtual string DisplayFormatString { get; set; }

    public virtual string DisplayName { get; set; }

    public virtual string EditFormatString { get; set; }

    public virtual bool HideSurroundingHtml { get; set; }

    public virtual bool IsReadOnly { get; set; }

    public virtual bool IsRequired { get; set; }

    public virtual string TemplateHint { get; set; }
}

Toute la soupe normalement nécessaire pour récupérer la valeur des différents attributs d’annotations de données par réflexion est ainsi masquée. Et si le fournisseur de métas données ne se basent pas sur ces attributs, l’interface d’utilisation par les HTML Helpers reste la même. Pratique, n’est-ce pas ?

Accéder aux validateurs

Du côté de la validation, un fournisseur doit dériver de la classe abstraite ModelValidatorProvider ci-dessous.

public abstract class ModelValidatorProvider
{
    public abstract IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context);
}  

Là encore, l’utilisation du fournisseur peut être simplifiée en faisant appel directement à une méthode statique. Celle-ci est présente sur la classe ModelValidator. L’objectif est justement d’obtenir une instance de la classe ModelValidator afin de pouvoir faire appel à sa méthode Validate et récupérer une collection de ModelValidationResult.

var modelValidator = ModelValidator.GetModelValidator(metadata, ControllerContext);

var modelValidationResults = modelValidator.Validate(product);

Le Model Binder par défaut d’ASP.NET MVC récupère de cette manière un ModelValidator et place les résultats de validation dans la collection ModelState. Le même comportement à également lieu lorsque l’on appelle manuellement la méthode TryUpdateModel dans un contrôleur.

Implémenter un fournisseur de métas données

Et voilà ! En implémentant un fournisseur personnalisé, il devient tout à fait possible d’aller lire des métas donnés depuis un fichier de configuration ou depuis une base de données. C’est un point assez méconnu d’ASP.NET MVC et dans certains projets, le fait de considérer les métas donnés comme figées dans le code sous la forme d’annotations d’un modèle, peut rebuter certains décideurs.

Pour la suite de ce billet, je vous propose de nous mettre dans la situation où, plutôt que de considérer les métas données comme étant présentes sur la forme d’attributs annotant le modèle, d’imaginer qu’ils soient présents dans un fichier de configuration XML.

Pour implémenter un nouveau fournisseur, plutôt que de dériver directement de la classe ModelMetadataProvider, je vous conseille plutôt de créer une classe qui dérive de AssociatedMetadataProvider. Ainsi, vous n’aurez plus qu’une seule méthode à implémenter, CreateMetadata.

Dans mon exemple, pour ne pas être trop verbeux, je ne vais considérer que deux propriétés de métas données : DisplayName et Description. Je commence donc pas créer une classe de modèle représentant une structure de données chargées de contenir les métas données extraites depuis le fichier de configuration.

public class ConfigurationMetadata
{
    public string DisplayName { get; set; }
    public string Description { get; set; }
}

A partir de là, j’ai également besoin d’un service qui se charge d’extraire les métas données depuis le fichier XML. Ce n’est pas le sujet de ce billet de blog donc je ne vais pas vous copier l’implémentation, mais en gros, l’interface est la suivante. Une simple méthode GetConfigurationMetadata suffit, avec le type du modèle pour lequel il faut extraire les métas données et le nom de la propriété ciblée.

public interface IConfigurationMetadataProvider
{
    ConfigurationMetadata GetConfigurationMetadata(Type type, string propertyName);
}

Enfin, il ne reste plus qu’à créer l’implémentation du fournisseur de métas données. Comme je l’ai dit un peu plus tôt, en dérivant de AssociatedMetadataProvider, il n’y a qu’à implémenter la méthode CreateMetadata. Le reste est assez simple, je vais récupérer une instance de ConfigurationMetadata à partir des éléments que je reçois en paramètre de l’appel à la méthode CreateMetadata. Une fois ces éléments en main, je les recopie dans une instance de ModelMetadata.

public class CustomizedModelMetadataProvider : AssociatedMetadataProvider
{
    private readonly IConfigurationMetadataProvider _configurationMetadataProvider;

    public CustomizedModelMetadataProvider(IConfigurationMetadataProvider configurationMetadataProvider)
    {
        _configurationMetadataProvider = configurationMetadataProvider;
    }

    protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
    {
        var modelMetadata = new ModelMetadata(this, containerType, modelAccessor, modelType, propertyName);

        var configurationMetadata = _configurationMetadataProvider.GetConfigurationMetadata(containerType, propertyName);

        if (configurationMetadata == null)
        {
            return modelMetadata;
        }

        if (!string.IsNullOrEmpty(configurationMetadata.DisplayName))
        {
            modelMetadata.DisplayName = configurationMetadata.DisplayName;
        }

        if (!string.IsNullOrEmpty(configurationMetadata.Description))
        {
            modelMetadata.Description = configurationMetadata.Description;
        }

        return modelMetadata;
    }
}

Le paramètre modelAccessor peut permettre de récupérer l’instance du modèle qui est actuellement en cours d’évaluation. Cela peut être particulièrement utile pour récupérer la valeur d’une propriété et la tester (dans le cas où les métas données sont conditionnées par rapport à une valeur). Cependant, il faut être particulièrement vigilent et faire attention à deux choses :

La présence d’une instance du modèle n’est pas garantie. Le fournisseur de métas données peut très bien avoir été invoqué uniquement sur un type, sans instance. C’est le cas notamment lorsque le fournisseur est utilisé par le Model Binder par défaut.

Le paramètre modelAccessor n’est pas d’un type très user-friendly .. Pour obtenir l’instance du modèle, il va donc falloir faire appel à l’API reflection sur la propriété Target du Func.

Le code n’est pas très joli, c’est un peu du bricolage, mais c’est la seule difficulté que l’on peut rencontrer dans l’implémentation d’un fournisseur de métas données personnalisés.

var target = modelAccessor.Target;

var instance = target.GetType().GetField("container").GetValue(target);

Pour rendre le fournisseur complètement utilisable, il ne reste qu’à le brancher aux propriétés que j’ai évoquées en tout début d’article. Dans mon cas, je fais le choix de le résoudre pas le DependencyResolver, cela me permet d’injecter automatiquement mon service de manipulation de la configuration.

ModelMetadataProviders.Current = DependencyResolver.Current.GetService<CustomizedModelMetadataProvider>();

L’implémentation d’un fournisseur de validation est assez similaire donc je ne vais pas l’explorer ici. Notez seulement que vous pouvez dériver directement de ModelValidatorProvider et qu’il n’y a qu’une seule méthode à implémenter, GetValidators. L’instanciation est un peu différente puisqu’à la différence des fournisseurs de métas données, il peut y avoir plusieurs fournisseurs de validation en parallèle.

ModelValidatorProviders.Providers.Add(DependencyResolver.Current.GetService<CustomModelValidatorProvider>());

Cette dernière remarque est assez importante : si vous remplacez le fournisseur de métas données de base d’ASP.NET MVC par le vôtre, alors la recherche de métas données par attributs Data Annotations ne fonctionnera plus ! Si vous voulez conserver les deux fonctionnements en parallèle, il faut créer un fournisseur hybride. Par exemple, soit votre fournisseur trouve des métas données dans un fichier de configuration XML et dans ce cas, il les utilise, sinon il se rabat sur d’éventuelles métas données présentes sous la forme d’attributs décorant le modèle.

J’espère que ce billet aura été instructif et qu’il vous aidera à mettre en place de nouveaux scénarios de développement avec ASP.NET MVC.

A bientôt

Nombre de vue : 149

AJOUTER UN COMMENTAIRE