Validation conditionnelle ASP.NET MVC

129

Comment utiliser les annotations de données pour effectuer une validation conditionnelle sur un modèle?

Par exemple, disons que nous avons le modèle suivant (Personne et Senior):

public class Person
{
    [Required(ErrorMessage = "*")]
    public string Name
    {
        get;
        set;
    }

    public bool IsSenior
    {
        get;
        set;
    }

    public Senior Senior
    {
        get;
        set;
    }
}

public class Senior
{
    [Required(ErrorMessage = "*")]//this should be conditional validation, based on the "IsSenior" value
    public string Description
    {
        get;
        set;
    }
}

Et la vue suivante:

<%= Html.EditorFor(m => m.Name)%>
<%= Html.ValidationMessageFor(m => m.Name)%>

<%= Html.CheckBoxFor(m => m.IsSenior)%>
<%= Html.ValidationMessageFor(m => m.IsSenior)%>

<%= Html.CheckBoxFor(m => m.Senior.Description)%>
<%= Html.ValidationMessageFor(m => m.Senior.Description)%>

Je voudrais être le champ obligatoire conditionnel de propriété "Senior.Description" basé sur la sélection de la propriété "IsSenior" (vrai -> obligatoire). Comment implémenter la validation conditionnelle dans ASP.NET MVC 2 avec des annotations de données?

Peter Stegnar
la source
1
J'ai récemment posé une question similaire: stackoverflow.com/questions/2280539/…
Darin Dimitrov
Je suis confus. Un Seniorobjet est toujours un senior, alors pourquoi IsSenior peut-il être faux dans ce cas. N'avez-vous pas simplement besoin que la propriété 'Person.Senior' soit nulle lorsque la valeur Person.IsSeniorest false. Ou pourquoi ne pas mettre en œuvre la IsSeniorpropriété comme suit: bool IsSenior { get { return this.Senior != null; } }.
Steven
Steven: "IsSenior" se traduit par le champ de la case à cocher dans la vue. Lorsque l'utilisateur coche la case à cocher "IsSenior", le champ "Senior.Description" devient obligatoire.
Peter Stegnar
Darin Dimitrov: En quelque sorte, mais pas tout à fait. Vous voyez, comment feriez-vous pour que le message d'erreur soit associé au champ spécifique? Si vous validez au niveau de l'objet, vous obtenez une erreur au niveau de l'objet. J'ai besoin d'une erreur au niveau de la propriété.
Peter Stegnar

Réponses:

150

Il existe une bien meilleure façon d'ajouter des règles de validation conditionnelle dans MVC3; faites hériter votre modèle IValidatableObjectet implémentez la Validateméthode:

public class Person : IValidatableObject
{
    public string Name { get; set; }
    public bool IsSenior { get; set; }
    public Senior Senior { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) 
    { 
        if (IsSenior && string.IsNullOrEmpty(Senior.Description)) 
            yield return new ValidationResult("Description must be supplied.");
    }
}

Pour en savoir plus, consultez Présentation d'ASP.NET MVC 3 (Aperçu 1) .

viperguynaz
la source
si la propriété est de type "int", cela nécessite une valeur, si remplissez ce champ, Valider ne fonctionne pas ..
Jeyhun Rahimov
2
Malheureusement, Microsoft a placé cela dans la mauvaise couche - la validation est une logique métier et cette interface se trouve dans la DLL System.Web. Pour l'utiliser, vous devez donner à votre couche métier une dépendance à une technologie de présentation.
NightOwl888
7
vous le faites si vous l'implémentez - voir l'exemple complet sur falconwebtech.com/post/…
viperguynaz
4
falconwebtech.com/post/… - @viperguynaz cela ne fonctionne pas
Smit Patel
1
@RayLoveless, vous devriez appeler ModelState.IsValid- ne pas appeler Validate directement
viperguynaz
63

J'ai résolu cela en manipulant le dictionnaire "ModelState" , qui est contenu par le contrôleur. Le dictionnaire ModelState comprend tous les membres qui doivent être validés.

Voici la solution:

Si vous devez implémenter une validation conditionnelle basée sur un champ (par exemple si A = true, alors B est requis), tout en conservant la messagerie d'erreur au niveau de la propriété (ce n'est pas vrai pour les validateurs personnalisés qui sont au niveau de l'objet), vous pouvez y parvenir en gérant "ModelState", en supprimant simplement les validations indésirables.

... dans une classe ...

public bool PropertyThatRequiredAnotherFieldToBeFilled
{
  get;
  set;
}

[Required(ErrorMessage = "*")] 
public string DepentedProperty
{
  get;
  set;
}

... la classe continue ...

... Dans certaines actions du contrôleur ...

if (!PropertyThatRequiredAnotherFieldToBeFilled)
{
   this.ModelState.Remove("DepentedProperty");
}

...

Avec cela, nous obtenons une validation conditionnelle, tout en laissant tout le reste inchangé.


METTRE À JOUR:

C'est ma dernière implémentation: j'ai utilisé une interface sur le modèle et l'attribut action qui valide le modèle qui implémente ladite interface. L'interface prescrit la méthode Validate (ModelStateDictionary modelState). L'attribut on action appelle simplement le Validate (modelState) sur IValidatorSomething.

Je ne voulais pas compliquer cette réponse, donc je n'ai pas mentionné les détails finaux d'implémentation (qui, à la fin, importent dans le code de production).

Peter Stegnar
la source
17
L'inconvénient est que l'une des parties de votre logique de validation est située dans le modèle et l'autre partie dans le (s) contrôleur (s).
Kristof Claes
Bien sûr, ce n'est pas nécessaire. Je montre juste l'exemple le plus élémentaire. J'ai implémenté cela avec une interface sur le modèle et avec un attribut d'action qui valide le modèle qui implémente l'interface mentionnée. L'interface transpire la méthode Validate (ModelStateDictionary modelState). Donc, finalement, vous FAITES toute la validation dans le modèle. Bref, bon point.
Peter Stegnar
J'aime la simplicité de cette approche en attendant que l'équipe MVC construise quelque chose de mieux hors de la boîte. Mais votre solution fonctionne-t-elle avec la validation côté client activée?
Aaron
2
@Aaron: Je suis heureux que vous aimiez la solution, mais malheureusement, cette solution ne fonctionne pas avec la validation côté client (car chaque attribut de validation a besoin de son implémentation JavaScript). Vous pouvez vous aider avec l'attribut "Remote", donc un seul appel Ajax sera émis pour le valider.
Peter Stegnar
Pouvez-vous développer cette réponse? Cela a du sens, mais je veux m'assurer que je suis cristallin. Je suis confronté à cette situation exacte et je veux la résoudre.
Richard B
36

J'ai eu le même problème hier mais je l'ai fait d'une manière très propre qui fonctionne à la fois pour la validation côté client et côté serveur.

Condition: en fonction de la valeur d'une autre propriété dans le modèle, vous souhaitez rendre une autre propriété obligatoire. Voici le code

public class RequiredIfAttribute : RequiredAttribute
{
    private String PropertyName { get; set; }
    private Object DesiredValue { get; set; }

    public RequiredIfAttribute(String propertyName, Object desiredvalue)
    {
        PropertyName = propertyName;
        DesiredValue = desiredvalue;
    }

    protected override ValidationResult IsValid(object value, ValidationContext context)
    {
        Object instance = context.ObjectInstance;
        Type type = instance.GetType();
        Object proprtyvalue = type.GetProperty(PropertyName).GetValue(instance, null);
        if (proprtyvalue.ToString() == DesiredValue.ToString())
        {
            ValidationResult result = base.IsValid(value, context);
            return result;
        }
        return ValidationResult.Success;
    }
}

Ici PropertyName est la propriété sur laquelle vous voulez faire votre condition DesiredValue est la valeur particulière du PropertyName (propriété) pour laquelle votre autre propriété doit être validée pour obligatoire

Dites que vous avez ce qui suit

public class User
{
    public UserType UserType { get; set; }

    [RequiredIf("UserType", UserType.Admin, ErrorMessageResourceName = "PasswordRequired", ErrorMessageResourceType = typeof(ResourceString))]
    public string Password
    {
        get;
        set;
    }
}

Enfin, mais non des moindres, enregistrez l'adaptateur pour votre attribut afin qu'il puisse effectuer une validation côté client (je l'ai mis dans global.asax, Application_Start)

 DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(RequiredIfAttribute),typeof(RequiredAttributeAdapter));
Dan Hunex
la source
C'était le point de départ original miroprocessordev.blogspot.com/2012/08
Dan Hunex
Existe-t-il une solution équivalente dans asp.net mvc2? ValidationResult, les classes ValidationContext ne sont pas disponibles dans asp.net mvc2 (.net framework 3.5)
User_MVC
2
Cela ne fonctionne que côté serveur comme l'indique le blog lié
Pakman
2
J'ai réussi à faire fonctionner cela du côté client avec MVC5, mais dans le client, cela déclenche la validation, quelle que soit la valeur de DesiredValue.
Geethanga
1
@Dan Hunex: Dans MVC4, je n'ai pas réussi à travailler correctement côté client et cela déclenche la validation quelle que soit la valeur de DesiredValue. Des pls d'aide?
Jack
34

J'utilise cette incroyable pépite qui fait des annotations dynamiques ExpressiveAnnotations

Vous pouvez valider n'importe quelle logique dont vous pouvez rêver:

public string Email { get; set; }
public string Phone { get; set; }
[RequiredIf("Email != null")]
[RequiredIf("Phone != null")]
[AssertThat("AgreeToContact == true")]
public bool? AgreeToContact { get; set; }
Korayem
la source
3
La bibliothèque ExpressiveAnnotation est la solution la plus flexible et générique de toutes les réponses ici. Merci d'avoir partagé!
Sudhanshu Mishra le
2
Je me suis cogné la tête en essayant de trouver une solution pendant une bonne journée. ExpressiveAnnotations semble être la solution pour moi!
Caverman
La bibliothèque ExpressiveAnnotation est géniale!
Doug Knudsen
1
Il a également un support côté client!
Nattrass
1
Pas de support pour .NET Core cependant, et il ne semble pas que cela se produise.
gosr le
18

Vous pouvez désactiver les validateurs de manière conditionnelle en supprimant les erreurs de ModelState:

ModelState["DependentProperty"].Errors.Clear();
Pavel Chuchuva
la source
6

Il existe maintenant un cadre qui effectue cette validation conditionnelle (entre autres validations d'annotations de données pratiques) hors de la boîte: http://foolproof.codeplex.com/

Plus précisément, jetez un œil au validateur [RequiredIfTrue ("IsSenior")]. Vous mettez cela directement sur la propriété que vous souhaitez valider, de sorte que vous obtenez le comportement souhaité de l'erreur de validation associée à la propriété "Senior".

Il est disponible sous forme de package NuGet.

bojingo
la source
3

Vous devez valider au niveau Personne, pas au niveau Senior, ou Senior doit avoir une référence à sa Personne Parent. Il me semble que vous avez besoin d'un mécanisme d'auto-validation qui définit la validation sur la Personne et non sur l'une de ses propriétés. Je ne suis pas sûr, mais je ne pense pas que DataAnnotations prend en charge cela dès la sortie de la boîte. Ce que vous pouvez faire créer le vôtreAttribute qui dérive deValidationAttribute peut être décoré au niveau de la classe et ensuite créez un validateur personnalisé qui permet également à ces validateurs de niveau classe de s'exécuter.

Je sais que le bloc d'application de validation prend en charge l'auto-validation prête à l'emploi, mais VAB a une courbe d'apprentissage assez raide. Néanmoins, voici un exemple utilisant VAB:

[HasSelfValidation]
public class Person
{
    public string Name { get; set; }
    public bool IsSenior { get; set; }
    public Senior Senior { get; set; }

    [SelfValidation]
    public void ValidateRange(ValidationResults results)
    {
        if (this.IsSenior && this.Senior != null && 
            string.IsNullOrEmpty(this.Senior.Description))
        {
            results.AddResult(new ValidationResult(
                "A senior description is required", 
                this, "", "", null));
        }
    }
}
Steven
la source
"Vous devez valider au niveau Personne, pas au niveau Senior" Oui, c'est une option, mais vous perdez la possibilité que l'erreur soit ajoutée à un champ particulier, qui est requis dans l'objet Senior.
Peter Stegnar
3

J'ai eu le même problème, j'avais besoin d'une modification de l'attribut [Obligatoire] - champ make requis en fonction de la requête http. La solution était similaire à la réponse de Dan Hunex, mais sa solution ne fonctionnait pas correctement (voir commentaires). Je n'utilise pas de validation discrète, juste MicrosoftMvcValidation.js prêt à l'emploi. C'est ici. Implémentez votre attribut personnalisé:

public class RequiredIfAttribute : RequiredAttribute
{

    public RequiredIfAttribute(/*You can put here pararmeters if You need, as seen in other answers of this topic*/)
    {

    }

    protected override ValidationResult IsValid(object value, ValidationContext context)
    {

    //You can put your logic here   

        return ValidationResult.Success;//I don't need its server-side so it always valid on server but you can do what you need
    }


}

Ensuite, vous devez implémenter votre fournisseur personnalisé pour l'utiliser comme adaptateur dans votre global.asax

public class RequreIfValidator : DataAnnotationsModelValidator <RequiredIfAttribute>
{

    ControllerContext ccontext;
    public RequreIfValidator(ModelMetadata metadata, ControllerContext context, RequiredIfAttribute attribute)
       : base(metadata, context, attribute)
    {
        ccontext = context;// I need only http request
    }

//override it for custom client-side validation 
     public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
     {       
               //here you can customize it as you want
         ModelClientValidationRule rule = new ModelClientValidationRule()
         {
             ErrorMessage = ErrorMessage,
    //and here is what i need on client side - if you want to make field required on client side just make ValidationType "required"    
             ValidationType =(ccontext.HttpContext.Request["extOperation"] == "2") ? "required" : "none";
         };
         return new ModelClientValidationRule[] { rule };
      }
}

Et modifiez votre global.asax avec une ligne

DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(RequiredIfAttribute), typeof(RequreIfValidator));

et c'est ici

[RequiredIf]
public string NomenclatureId { get; set; }

Le principal avantage pour moi est que je n'ai pas à coder un validateur client personnalisé comme en cas de validation discrète. cela fonctionne exactement comme [Obligatoire], mais uniquement dans les cas que vous souhaitez.

Tanière
la source
La partie sur l'extension DataAnnotationsModelValidatorétait exactement ce que j'avais besoin de voir. Je vous remercie.
twip le
0

Utilisation typique pour la suppression conditionnelle d'une erreur de l'état du modèle:

  1. Rendre la première partie conditionnelle de l'action du contrôleur
  2. Exécutez la logique pour supprimer l'erreur de ModelState
  3. Faites le reste de la logique existante (généralement la validation de l'état du modèle, puis tout le reste)

Exemple:

public ActionResult MyAction(MyViewModel vm)
{
    // perform conditional test
    // if true, then remove from ModelState (e.g. ModelState.Remove("MyKey")

    // Do typical model state validation, inside following if:
    //     if (!ModelState.IsValid)

    // Do rest of logic (e.g. fetching, saving

Dans votre exemple, gardez tout tel quel et ajoutez la logique suggérée à l'action de votre contrôleur. Je suppose que votre ViewModel passé à l'action du contrôleur contient les objets Person et Senior Person avec des données remplies à partir de l'interface utilisateur.

Jeremy Ray Brown
la source
0

J'utilise MVC 5 mais vous pouvez essayer quelque chose comme ceci:

public DateTime JobStart { get; set; }

[AssertThat("StartDate >= JobStart", ErrorMessage = "Time Manager may not begin before job start date")]
[DisplayName("Start Date")]
[Required]
public DateTime? StartDate { get; set; }

Dans votre cas, vous diriez quelque chose comme "IsSenior == true". Ensuite, il vous suffit de vérifier la validation de votre action de publication.


la source