web-dev-qa-db-fra.com

Meilleures pratiques Validation de ViewModel dans ASP.NET MVC

J'utilise DataAnnotations pour valider mon ViewModel côté client avec jquery.validate.unobtrusive et côté serveur dans l'application ASP.NET MVC.

Il n'y a pas si longtemps, j'ai compris que je pouvais écrire une validation comme ceci:

[Required(ErrorMessage = "{0} is required")]
public string Name { get; set; }

De cette façon, je peux facilement définir des chaînes générales dans config ou dans les ressources et toujours l'utiliser dans DataAnnotations. Il sera donc plus facile de modifier les messages de validation dans l'ensemble de mon application à l'avenir.

Je sais également qu'il existe une bibliothèque FluentValidation qui permet d'ajouter des règles de validation à ViewModel déjà existant. Je sais qu'il y a un problème avec Add/Edit ViewModels qui pourrait avoir des champs similaires mais des ValidationRules différents.

Un autre problème qui vient de la validation du client est que le html nouvellement ajouté à DOM (en utilisant demande ajax) doit être analysé pour activer la validation. Voici comment je le fais:

$('#some-ajax-form').data('validator', null); 
$.validator.unobtrusive.parse('#some-ajax-form');

J'ai donc quelques questions:

  1. Existe-t-il d'autres pratiques utiles qui pourraient aider à centraliser toutes les règles de validation dans l'application?
  2. Quelle est la meilleure façon de résoudre le problème de validation de l'ajout/modification ViewModel? Puis-je utiliser DataAnnotations avec FluentValidation ou séparer Ajouter et Modifier ViewModels est toujours la meilleure option?
  3. Existe-t-il un meilleur moyen d'initialiser la validation sur les nouveaux éléments DOM reçus avec appel ajax autre que je mentionne?

Je ne demande pas comment créer mon propre DataValidators je sais comment le faire. Je cherche des façons de les utiliser de manière plus productive et facile à entretenir.

31
teo van kot

Pour répondre à votre troisième question en premier: Non, il n'y a pas de moyen plus simple que ce que vous faites. Deux lignes de code pour le faire fonctionner ne peuvent guère être plus faciles. Bien qu'il existe un plug-in que vous pourriez utiliser, comme expliqué dans la question la validation discrète ne fonctionne pas avec le contenu dynamique

Votre première question, comment centraliser la validation, j'utilise normalement un fichier de classe séparé pour stocker toutes mes règles de validation. De cette façon, je n'ai pas à parcourir chaque fichier de classe pour trouver les règles, mais les avoir toutes au même endroit. Si c'est mieux, c'est une question de choix. La principale raison pour laquelle j'ai commencé à l'utiliser, c'est pour pouvoir ajouter de la validation aux classes générées automatiquement, comme les classes du Entity Framework.

J'ai donc un fichier appelé ModelValidation.cs dans ma couche de données, et avoir du code pour tous mes modèles comme

/// <summary>
/// Validation rules for the <see cref="Test"/> object
/// </summary>
/// <remarks>
/// 2015-01-26: Created
/// </remarks>
[MetadataType(typeof(TestValidation))]
public partial class Test { }
public class TestValidation
{
    /// <summary>Name is required</summary>
    [Required]
    [StringLength(100)]
    public string Name { get; set; }

    /// <summary>Text is multiline</summary>
    [DataType(DataType.MultilineText)]
    [AllowHtml]
    public string Text { get; set; }
}

Maintenant, comme vous l'avez remarqué, je ne fournis pas le message d'erreur réel. J'utilise conventions par Haacked pour ajouter les messages. Il simplifie l'ajout de règles de validation localisées.

Cela revient essentiellement à un fichier de ressources contenant quelque chose comme:

Test_Name = "Provide name"
Test_Name_Required = "Name is required"

Et ces messages et noms seront utilisés lorsque vous appelez le MVC view code comme

<div class="editor-container">
    <div class="editor-label">
        @Html.LabelFor(model => model.Name) <!--"Provide name"-->
    </div>
    <div class="editor-field">
        @Html.EditorFor(model => model.Name)
        @Html.ValidationMessageFor(model => model.Name) <!--"Name is required"-->
    </div>
</div>

Votre deuxième question, à propos de différentes validations pour l'ajout/la modification, peut être traitée de deux manières. La meilleure façon serait d'utiliser les vues telles qu'elles sont réellement prévues. Cela signifie que vous ne transmettez pas vos modèles réels aux vues, mais que vous créez un modèle de vue qui contient uniquement les données. Vous avez donc un modèle de vue pour Create avec les règles de validation appropriées et un modèle de vue pour Edit avec les règles appropriées, et lorsqu'ils passent, vous insérez le résultat dans votre modèle réel. Cela nécessite cependant beaucoup plus de code et de travail manuel, donc je peux imaginer que vous n'êtes pas vraiment prêt à le faire comme ça.

Une autre option serait d'utiliser validation conditionnelle comme expliqué par viperguynaz. Maintenant, au lieu d'un booléen, mes classes qui nécessitent un changement entre edit/add ont un primary keyIdint. Je vérifie donc si Id>0 pour déterminer s'il s'agit d'une modification ou non.

MISE À JOUR:

Si vous souhaitez mettre à jour la validation à chaque appel ajax, vous pouvez utiliser jQuery ajaxComplete. Cela revalidera tous les formulaires après chaque demande ajax.

$( document ).ajaxComplete(function() {
    $('form').each(function() {
        var $el = $(this);
        $el.data('validator', null); 
        $.validator.unobtrusive.parse($el);
    })
});

Si c'est quelque chose que vous voulez, cela dépend de la fréquence à laquelle vous recevez un formulaire via AJAX. Si vous avez beaucoup de demandes de AJAX, comme interroger un état toutes les 10 secondes, vous ne voulez pas cela. Si vous avez une requête AJAX occasionnelle, qui contient principalement un formulaire, vous pouvez l'utiliser.

Si votre AJAX renvoie un formulaire que vous souhaitez valider, alors oui, il est recommandé de mettre à jour la validation. Mais je suppose qu'une meilleure question serait "Ai-je vraiment besoin d'envoyer le formulaire par AJAX?" AJAX est amusant et utile, mais il doit être utilisé avec soin et réflexion.

14
Hugo Delsing

La validation discrète Jquery fonctionne en appliquant des attributs aux éléments INPUT qui demandent à la bibliothèque cliente de valider cet élément à l'aide d'une règle qui est mappée à l'attribut respectif. Par exemple: l'attribut html data-val-required Est reconnu par la bibliothèque discrète et la fait valider cet élément par rapport à la règle correspondante.

Dans . NET MVC, vous pouvez faire en sorte que cela se produise automatiquement pour certaines règles spécifiques en appliquant des attributs aux propriétés de votre modèle. Les attributs tels que Required et MaxLength fonctionnent parce que les assistants HTML savent lire ces attributs et ajouter à leur sortie les attributs HTML correspondants que la bibliothèque discrète comprend.

Si vous ajoutez des règles de validation à vos modèles dans IValidatableObject ou en utilisant FluentValidation, l'aide HTML ne verra pas ces règles et n'essayera donc pas de les traduire en attributs discrets.

En d'autres termes, la coordination "gratuite" que vous avez constatée jusqu'à présent en appliquant des attributs à votre modèle et en obtenant la validation du client est limitée aux attributs de validation et, en outre, est limitée (par défaut) uniquement aux attributs qui correspondent directement à des règles discrètes.

Le bon côté est que vous êtes libre de créer vos propres attributs de validation personnalisés, et en implémentant IClientValidatable, le Html Helper ajoutera un attribut discret avec le nom de votre choix que vous pourrez ensuite apprendre à la bibliothèque discrète à respecter. .

Il s'agit d'un attribut personnalisé que nous utilisons qui garantit qu'une date tombe après une autre date:

    [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class DateGreaterThanAttribute : ValidationAttribute, IClientValidatable
{
    string otherPropertyName;

    public DateGreaterThanAttribute(string otherPropertyName, string errorMessage = null)
        : base(errorMessage)
    {
        this.otherPropertyName = otherPropertyName;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        ValidationResult validationResult = ValidationResult.Success;
        // Using reflection we can get a reference to the other date property, in this example the project start date
        var otherPropertyInfo = validationContext.ObjectType.GetProperty(this.otherPropertyName);
        // Let's check that otherProperty is of type DateTime as we expect it to be
        if (otherPropertyInfo.PropertyType.Equals(new DateTime().GetType()))
        {
            DateTime toValidate = (DateTime)value;
            DateTime referenceProperty = (DateTime)otherPropertyInfo.GetValue(validationContext.ObjectInstance, null);
            // if the end date is lower than the start date, than the validationResult will be set to false and return
            // a properly formatted error message
            if (toValidate.CompareTo(referenceProperty) < 1)
            {
                validationResult = new ValidationResult(this.GetErrorMessage(validationContext));
            }
        }
        else
        {
            // do nothing. We're not checking for a valid date here
        }

        return validationResult;
    }

    public override string FormatErrorMessage(string name)
    {
        return "must be greater than " + otherPropertyName;
    }

    private string GetErrorMessage(ValidationContext validationContext)
    {
        if (!this.ErrorMessage.IsNullOrEmpty())
            return this.ErrorMessage;
        else
        {
            var thisPropName = !validationContext.DisplayName.IsNullOrEmpty() ? validationContext.DisplayName : validationContext.MemberName;
            var otherPropertyInfo = validationContext.ObjectType.GetProperty(this.otherPropertyName);
            var otherPropName = otherPropertyInfo.Name;
            // Check to see if there is a Displayname attribute and use that to build the message instead of the property name
            var displayNameAttrs = otherPropertyInfo.GetCustomAttributes(typeof(DisplayNameAttribute), false);
            if (displayNameAttrs.Length > 0)
                otherPropName = ((DisplayNameAttribute)displayNameAttrs[0]).DisplayName;

            return "{0} must be on or after {1}".FormatWith(thisPropName, otherPropName);
        }
    }

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        //string errorMessage = this.FormatErrorMessage(metadata.DisplayName);
        string errorMessage = ErrorMessageString;

        // The value we set here are needed by the jQuery adapter
        ModelClientValidationRule dateGreaterThanRule = new ModelClientValidationRule();
        dateGreaterThanRule.ErrorMessage = errorMessage;
        dateGreaterThanRule.ValidationType = "dategreaterthan"; // This is the name the jQuery adapter will use
        //"otherpropertyname" is the name of the jQuery parameter for the adapter, must be LOWERCASE!
        dateGreaterThanRule.ValidationParameters.Add("otherpropertyname", otherPropertyName);

        yield return dateGreaterThanRule;
    }
}

Nous pouvons appliquer l'attribut au modèle en tant que tel:

    [DateGreaterThan("Birthdate", "You have to be born before you can die")]
    public DateTime DeathDate { get; set; }

Cela entraîne l'aide de HTML à rendre les deux attributs suivants sur l'élément INPUT lors de l'appel de Html.EditorFor Sur une propriété de modèle qui a cet attribut:

data-val-dategreaterthan="You have to be born before you can die" 
data-val-dategreaterthan-otherpropertyname="Birthdate" 

Jusqu'ici tout va bien, mais maintenant je dois enseigner à la validation discrète quoi faire avec ces attributs. Tout d'abord, je dois créer une règle nommée pour la validation jquery:

    // Value is the element to be validated, params is the array of name/value pairs of the parameters extracted from the HTML, element is the HTML element that the validator is attached to
jQuery.validator.addMethod("dategreaterthan", function (value, element, params) {
    return Date.parse(value) > Date.parse($(params).val());
});

Et puis ajoutez un adaptateur discret pour cette règle qui mappe l'attribut à la règle:

jQuery.validator.unobtrusive.adapters.add("dategreaterthan", ["otherpropertyname"], function (options) {
    options.rules["dategreaterthan"] = "#" + options.params.otherpropertyname;
    options.messages["dategreaterthan"] = options.message;
});

Après avoir fait tout cela, je peux obtenir cette règle de validation "gratuitement" n'importe où ailleurs dans mon application simplement en appliquant cet attribut au modèle.

Pour répondre à votre question sur la façon d'appliquer des règles conditionnellement selon que le modèle est utilisé dans une opération d'ajout ou de modification: cela peut probablement être fait en ajoutant une logique supplémentaire à vos attributs personnalisés et en utilisant à la fois la méthode IsValid la méthode de règles GetClientValidation tente de glaner du contexte du modèle en utilisant la réflexion. Mais honnêtement, cela me semble un gâchis. Pour cela, je m'appuierais uniquement sur la validation du serveur et sur les règles que vous choisiriez d'appliquer à l'aide de la méthode IValidatableObject.Validate().

5
esmoore68

Comme d'autres l'ont dit, il n'y a pas de telles astuces, pas de moyen facile de centraliser vos validations.

J'ai quelques approches qui pourraient vous intéresser. Prenez note que c'est ainsi que "nous" avons résolu le même problème auparavant. À vous de voir si vous pouvez trouver notre solution maintenable et productive.

Je sais qu'il y a un problème avec Add/Edit ViewModels qui pourrait avoir des champs similaires mais des ValidationRules différents.

Approche d'héritage

Vous pouvez obtenir une validation centralisée à l'aide d'une classe de base et utiliser des sous-classes pour des validations spécifiques.

// Base class. That will be shared by the add and edit
public class UserModel
{
    public int ID { get; set; }
    public virtual string FirstName { get; set; } // Notice the virtual?

    // This validation is shared on both Add and Edit.
    // A centralized approach.
    [Required]
    public string LastName { get; set; }
}

// Used for creating a new user.
public class AddUserViewModel : UserModel
{
    // AddUser has its own specific validation for the first name.
    [Required]
    public override string FirstName { get; set; } // Notice the override?
}

// Used for updating a user.
public class EditUserViewModel : UserModel
{
    public override string FirstName { get; set; }
}

Extension de l'approche ValidationAttribute

À l'aide de ValidationAtribute personnalisé, vous pouvez obtenir une validation centralisée. Ce n'est que l'implémentation de base, je vous montre juste l'idée.

using System.ComponentModel.DataAnnotations;
public class CustomEmailAttribute : ValidationAttribute
{
    public CustomEmailAttribute()
    {
        this.ErrorMessage = "Error Message Here";
    }

    public override bool IsValid(object value)
    {
        string email = value as string;

        // Put validation logic here.

        return valid;
    }
}

Vous utiliseriez comme tel

public class AddUserViewModel
{
    [CustomEmail]
    public string Email { get; set; }

    [CustomEmail]
    public string RetypeEmail { get; set; }
}

Existe-t-il un meilleur moyen d'initialiser la validation sur les nouveaux éléments DOM reçus avec un appel ajax autre que je mentionne?

C'est ainsi que je relie les validateurs sur les éléments dynamiques.

/** 
* Rebinds the MVC unobtrusive validation to the newly written
* form inputs. This is especially useful for forms loaded from
* partial views or ajax.
*
* Credits: http://www.mfranc.com/javascript/unobtrusive-validation-in-partial-views/
* 
* Usage: Call after pasting the partial view
*
*/
function refreshValidators(formSelector) {
    //get the relevant form 
    var form = $(formSelector);
    // delete validator in case someone called form.validate()
    $(form).removeData("validator");
    $.validator.unobtrusive.parse(form);
};

Usage

// Dynamically load the add-user interface from a partial view.
$('#add-user-div').html(partialView);

// Call refresh validators on the form
refreshValidators('#add-user-div form');
5
Yorro

Il existe différentes manières de faire en sorte que la validation client, comme celle que Microsoft utilise pour MVC, fonctionne avec la bibliothèque ubobtrusive créée par elle-même pour l'intégration avec DataAnnotations. Mais , après quelques années de travail avec cet outil utile, je m'en lasse, ce qui est ennuyeux et fastidieux à utiliser dans les cas où nous avons besoin de ViewModels (et probablement séparé ViewModels pour créer/modifier des modèles).

Une autre façon est d'utiliser MVVM qui fonctionne bien avec MVC car les deux paradigmes sont assez similaires. Dans MVC, vous avez un modèle limité uniquement côté serveur lorsque le client envoie du contenu au serveur. Alors que MVVM lie un modèle local avec l'interface utilisateur directement sur le client. Jetez un œil à Knockoutjs , celui connu qui vous aide à comprendre comment travailler avec MVVM.

Dans cet esprit, je répondrai à vos questions dans l'ordre:

  1. Vous ne pouvez pas centraliser les règles de validation dans l'application, sauf en créant des classes partagées et en les réutilisant en appelant des modèles/ViewModels distincts.
  2. Si vous souhaitez utiliser Microsoft Validator, la séparation de l'ajout/modification de ViewModels est une meilleure option en raison de sa lisibilité et de sa facilité de modification.
  3. Je n'ai jamais dit que le Knockoutjs est meilleur, ils sont différents les uns des autres, vous donne juste une certaine flexibilité pour créer des vues basées sur les exigences du modèle. Cela vous éloigne également de la centralisation des validations :(
1