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:
ViewModel
? Puis-je utiliser DataAnnotations
avec FluentValidation ou séparer Ajouter et Modifier ViewModels
est toujours la meilleure option?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.
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 key
Id
int
. 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.
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()
.
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');
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: