ASP.NET MVC Trucs et astuces - créez vos propres validateurs serveur et client

ASP.NET MVC propose nativement des outils pour valider la saisie des utilisateurs. Ces outils sont principalement les data annotations, qui permettent de décorer les modèles de vue à l’aide d’attributs pour indiquer que tel ou tel champ est requis, soumis à une longueur de chaîne de caractères, une expression régulière (etc.), le model state ASP.NET MVC qui permet de valider un modèle côté serveur et le plugin jQuery validation, qui permet de valider (ou d’aider à) la saisie côté client.

Par défaut, voilà les validateurs qui sont proposés dans ASP.NET MVC :

  • Compare : permet d’indiquer qu’une propriété doit être égale à une autre
  • DataType : permet d’indiquer le type de données porté par la propriété (adresse email, téléphone, nombre…)
  • MaxLength : permet d’indiquer que la propriété doit avoir un nombre de caractères maximum
  • MinLength : permet d’indiquer que la propriété doit avoir un nombre de caractères minimum
  • Range : permet d’indiquer qu’une propriété doit se trouver entre deux valeurs
  • RegularExpression : permet d’indiquer qu’une propriété doit respecter une certaine expression régulière
  • Required : permet d’indiquer qu’une propriété doit être saisie
  • StringLength : permet d’indiquer qu’une propriété doit avoir une taille minimum et maximum
  • Remote : permet de valider une propriété côté serveur, à l’aide d’un appel AJAX

Bien que ces différents attributs soient souvent utilisés au cours du développement d’une application ASP.NET MVC, il est rare que ceux-ci suffisent à couvrir tous les scénarios de validation possibles et imaginables. Pour cela, le Framework de validation a été pensé de façon à être totalement extensible : tous les attributs héritent de la classe ValidationAttribute pour la validation serveur et peuvent implémenter l’interface IClientValidatable pour la validation côté client, à l’aide du plugin jQuery qu’il est également possible d’enrichir.

Par exemple, il est assez fréquent d’avoir des scénarios où si l’utilisateur coche une case, alors un ou plusieurs autres champs doivent devenir obligatoires. Pour ce type de validation conditionnelle, il est possible de créer son propre attribut, RequiredIfTrue, par exemple.

Etape 1 : la validation côté serveur

Pour commencer, il faut donc que votre attribut de validation hérite de la classe ValidationAttribute. Dans le cas présent, l’attribut RequiredIfTrue doit prendre le nom de la propriété qui déclenche la validation lorsqu’elle passe à “true” :

public class RequiredIfTrueAttribute : ValidationAttribute
{
    private string otherProperty;

    public RequiredIfTrueAttribute(string otherProperty)
    {
        this.otherProperty = otherProperty;
    }
}

Pour implémenter la validation côté serveur, il suffit de surcharger la méthode IsValid de la classe ValidationAttribut :

public class RequiredIfTrueAttribute : ValidationAttribute
{
    private string otherProperty;

    public RequiredIfTrueAttribute(string otherProperty)
    {
        this.otherProperty = otherProperty;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        PropertyInfo propertyInfo = validationContext.ObjectType.GetProperty(this.otherProperty);
        object otherPropertyValue = propertyInfo.GetMethod.Invoke(validationContext.ObjectInstance, null);

        if (!(otherPropertyValue is bool))
            throw new NotSupportedException("La propriété de comparaison doit être de type bool");

        bool bValue = (bool)otherPropertyValue;

        if (bValue && value == null)
            return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));

        return ValidationResult.Success;
    }
}

A partir de là, la validation serveur est fonctionnelle. Pour la tester, on peut prendre pour exemple le modèle suivant :

public class NewsletterModel
{
    public bool SubscribeToNewsletter { get; set; }

    [RequiredIfTrue("SubscribeToNewsletter", ErrorMessage = "L'email est obligatoire si vous souhaitez recevoir la newsletter")]
    public string Email { get; set; }
}

Et la vue associée :

@using (Html.BeginForm()) 
{
    @Html.AntiForgeryToken()
    
    <div class="form-horizontal">
        <h4>NewsletterModel</h4>
        <hr />
        @Html.ValidationSummary(true)

        <div class="form-group">
            @Html.LabelFor(model => model.SubscribeToNewsletter, new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.SubscribeToNewsletter)
                @Html.ValidationMessageFor(model => model.SubscribeToNewsletter)
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.Email, new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Email)
                @Html.ValidationMessageFor(model => model.Email)
            </div>
        </div>

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
}

Ainsi que le contrôleur :

public class TestController : Controller
{
    public ActionResult Index()
    {
        return View(new NewsletterModel());
    }

    [HttpPost]
    public ActionResult Index(NewsletterModel model)
    {
        if (!ModelState.IsValid)
        {
            return View(model);
        }

        return RedirectToAction("Index", "Home");
    }
}

Si vous testez ce code, vous constaterez que lorsque la case pour s’abonner à la newsletter est cochée, la saisie d’un e-mail est alors obligatoire :

image

Etape 2 : la validation côté client

Afin de rendre l’interface plus agréable à l’utilisateur, il est possible d’étendre la validation côté client. Pour ça, il est nécessaire d’implémenter l’interface IClientValidatable sur votre attribut de validation. Cette interface vous demande de redéfinir une méthode GetClientValidationRules qui permet tout simplement de générer le markup html nécessaire à la validation jQuery. Dans l’exemple présent, on obtient :

public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
    var rules = new List<ModelClientValidationRule>();

    var requiredIfTrueRule = new ModelClientValidationRule();
    requiredIfTrueRule.ErrorMessage = FormatErrorMessage(metadata.DisplayName);
    requiredIfTrueRule.ValidationType = "requirediftrue";
    requiredIfTrueRule.ValidationParameters.Add("otherproperty", this.otherProperty);

    rules.Add(requiredIfTrueRule);

    return rules;
}

La propriété ValidationType sur la règle représente le validateur jQuery qui sera rajouté juste après. La propriété ValidationParameters représente les différents paramètres à passer au validateur jQuery. Dès lors, si vous regardez le markup html généré dans la vue, vous constaterez que tous les éléments pour la validation jquery sont présents :

<div class="col-md-10">
    <input class="text-box single-line" data-val="true" 
        data-val-requirediftrue="L&#39;email est obligatoire si vous souhaitez recevoir la newsletter" 
        data-val-requirediftrue-otherproperty="SubscribeToNewsletter" 
        id="Email" name="Email" type="text" value="" />
    <span class="field-validation-valid" data-valmsg-for="Email" data-valmsg-replace="true"></span>
</div>

Il est maintenant nécessaire d’étendre le plugin jQuery validation afin de lui rajouter deux éléments :

  • Un adapter jquery unobtrusive : cet élément permet d’interpréter les nouveaux attributs data-val-requirediftrue, data-val-requirediftrue-otherproperty dans le markup html
  • Un validateur jquery : cet élément constitue le code métier de la validation du champ

Pour cela, il suffit juste de charger le script suivant après le chargement de jQuery validation et jQuery unobtrusive :

jQuery.validator.unobtrusive.adapters.add(
    'requirediftrue', ['properties'], function (options) {
        options.rules.requirediftrue = options.params;
        options.messages.requirediftrue = options.message;
    }
);

jQuery.validator.addMethod('requirediftrue', function (value, element, params) {
    var jElement = $(element);
    var otherProperty = jElement.attr('data-val-requirediftrue-otherproperty');
    if (!!(otherProperty !== undefined)) {

        var elementName = element.id;

        var otherValue = $('#' + otherProperty).val().toLowerCase();

        var compareValue = 'true';

        var required = otherValue === compareValue;

        if (required && (value === null || value == "")) {
            return false;
        }
    }
    return true;
});

Et voilà, le tour est joué ! Si vous re-testez votre page, vous constaterez bien que la validation s’effectue côté client également !

Je profite également de ce post pour repréciser que la validation est TOUJOURS OBLIGATOIRE côté serveur, et qu’il ne s’agit au final que d’une “aide à la saisie” côté client.

A bientôt!

Julien

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus