ASP.NET MVC Conditional validation
Asked Answered
C

12

132

How to use data annotations to do a conditional validation on model?

For example, lets say we have the following model (Person and 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;
    }
}

And the following view:

<%= 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)%>

I would like to be the "Senior.Description" property conditional required field based on the selection of the "IsSenior" propery (true -> required). How to implement conditional validation in ASP.NET MVC 2 with data annotations?

Counterfactual answered 10/3, 2010 at 13:19 Comment(4)
I've recently asked similar question: #2281039Mclaurin
I'm confused. A Senior object is always a senior, so why can IsSenior be false in that case. Don't you just need the 'Person.Senior' property to be null when Person.IsSenior is false. Or why not implement the IsSenior property as follows: bool IsSenior { get { return this.Senior != null; } }.Platyhelminth
Steven: "IsSenior" translates to the checkbox field in the view. When user checks the "IsSenior" checkBox then the "Senior.Description" Field become mandatory.Counterfactual
Darin Dimitrov: Well sort of, but not quite. You see, how would you achieve that the the error mesage is appent to the specific field? If you validate at object level, you get an error at object level. I need error on property level.Counterfactual
C
72

I have solved this by handling the "ModelState" dictionary, which is contained by the controller. The ModelState dictionary includes all the members that have to be validated.

Here is the solution:

If you need to implement a conditional validation based on some field (e.g. if A=true, then B is required), while maintaining property level error messaging (this is not true for the custom validators that are on object level) you can achieve this by handling "ModelState", by simply removing unwanted validations from it.

...In some class...

public bool PropertyThatRequiredAnotherFieldToBeFilled
{
  get;
  set;
}

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

...class continues...

...In some controller action ...

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

...

With this we achieve conditional validation, while leaving everything else the same.


UPDATE:

This is my final implementation: I have used an interface on the model and the action attribute that validates the model which implements the said interface. Interface prescribes the Validate(ModelStateDictionary modelState) method. The attribute on action just calls the Validate(modelState) on IValidatorSomething.

I did not want to complicate this answer, so I did not mention the final implementation details (which, at the end, matter in production code).

Counterfactual answered 11/3, 2010 at 12:30 Comment(8)
The downside is that one of part your validation logic is located in the model and the other part in the controller(s).Abscind
Well of course this is not necessary. I just show the most basic example. I have implemented this with interface on model and with action attribute that validates model which implements the mentioned interface. Interface perspires the Validate(ModelStateDictionary modelState) method. So finally you DO all validation in the model. Anyway, good point.Counterfactual
I like the simplicity of this approach in the mean time until the MVC team builds something better out of the box. But does your solution work with client side validation enabled??Bessie
@Aaron: I am happy that you like solution, but unfortunately this solution does not work with client side validation (as every validation attribute need its JavaScript implementation). You could help yourself with "Remote" attribute, so just Ajax call will be emitted to validate it.Counterfactual
Are you able to expand on this answer? This makes some sense, but I want to make sure I'm crystal on it. I'm faced with this exact situation, and I want to get it resolved.Lavender
@Levitikon: Sure it does not, as any other custom validation implementation only on server side. There is also possibility to implement this validation that is supported on client side.Counterfactual
@PeterStegnar will you please provide an example through an interface. I am more interested in to know how this could done with an interface.Harlot
@AmmarKhan What do you mean thru interface? Usually you do not use interface in a MVC model ... Or what do you mean? Interface like UI?Counterfactual
W
160

There's a much better way to add conditional validation rules in MVC3; have your model inherit IValidatableObject and implement the Validate method:

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.");
    }
}

Read more at Introducing ASP.NET MVC 3 (Preview 1).

Woodbury answered 12/3, 2011 at 20:40 Comment(11)
if property is "int" type, that requires value, if fill that field, Validate does not work..Fakir
Unfortunately, Microsoft put this in the wrong layer - validation is business logic and this interface is in the System.Web DLL. In order to use this, you have to give your business layer a dependency on a presentation technology.Hipparchus
you do if you implement it - see full example at falconwebtech.com/post/…Woodbury
Is there any equvalent solution in asp.net mvc2? IValidatableObject inter face & ValidationResult, ValidationContext classes are not available in asp.net mvc2 (.net framework 3.5)Clermontferrand
falconwebtech.com/post/… - @Woodbury this isn't workingHumdinger
This isn't working. How do you pass in the validationContext when calling myViewmodel.Validate(..)? Does Validate need to be called manually?Footstalk
@Footstalk you should be calling ModelState.IsValid - not calling Validate directlyWoodbury
@Woodbury thanks it's calling validate now but the validateMessageFor(m => m.myField) isn't showing the error message. How does the error message get bound to the specific viewModel's property? Thanks for your help.Footstalk
You need to add a Validatio message to your view - Html.ValidationMessage("SomProperty", "*")Woodbury
IMHO this should be marked as the correct answer. While you could build a custom attribute and all that, you're leaking something into the system that isn't necessary (unless you want to validate multiple models conditionally). This is the cleanest way and will be called in the controller when you (normally) do a if(ModelState.IsValid) call before saving/processing/etc.Sorrows
I know this an older thread, but for completion - @Woodbury - to bind the error to a specific property (e.g. for client side model binding validation error displaying), use the overloaded method instead. "yield return new ValidationResult("ErrorMessage.", new[] {"PutNameOfPropertyHere"}); Without this, the validation error is generic, and would require a catch-all warning label within the view, to be displayed.Sigismond
C
72

I have solved this by handling the "ModelState" dictionary, which is contained by the controller. The ModelState dictionary includes all the members that have to be validated.

Here is the solution:

If you need to implement a conditional validation based on some field (e.g. if A=true, then B is required), while maintaining property level error messaging (this is not true for the custom validators that are on object level) you can achieve this by handling "ModelState", by simply removing unwanted validations from it.

...In some class...

public bool PropertyThatRequiredAnotherFieldToBeFilled
{
  get;
  set;
}

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

...class continues...

...In some controller action ...

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

...

With this we achieve conditional validation, while leaving everything else the same.


UPDATE:

This is my final implementation: I have used an interface on the model and the action attribute that validates the model which implements the said interface. Interface prescribes the Validate(ModelStateDictionary modelState) method. The attribute on action just calls the Validate(modelState) on IValidatorSomething.

I did not want to complicate this answer, so I did not mention the final implementation details (which, at the end, matter in production code).

Counterfactual answered 11/3, 2010 at 12:30 Comment(8)
The downside is that one of part your validation logic is located in the model and the other part in the controller(s).Abscind
Well of course this is not necessary. I just show the most basic example. I have implemented this with interface on model and with action attribute that validates model which implements the mentioned interface. Interface perspires the Validate(ModelStateDictionary modelState) method. So finally you DO all validation in the model. Anyway, good point.Counterfactual
I like the simplicity of this approach in the mean time until the MVC team builds something better out of the box. But does your solution work with client side validation enabled??Bessie
@Aaron: I am happy that you like solution, but unfortunately this solution does not work with client side validation (as every validation attribute need its JavaScript implementation). You could help yourself with "Remote" attribute, so just Ajax call will be emitted to validate it.Counterfactual
Are you able to expand on this answer? This makes some sense, but I want to make sure I'm crystal on it. I'm faced with this exact situation, and I want to get it resolved.Lavender
@Levitikon: Sure it does not, as any other custom validation implementation only on server side. There is also possibility to implement this validation that is supported on client side.Counterfactual
@PeterStegnar will you please provide an example through an interface. I am more interested in to know how this could done with an interface.Harlot
@AmmarKhan What do you mean thru interface? Usually you do not use interface in a MVC model ... Or what do you mean? Interface like UI?Counterfactual
S
42

I had the same problem yesterday but I did it in a very clean way which works for both client side and server side validation.

Condition: Based on the value of other property in the model, you want to make another property required. Here is the 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;
    }
}

Here PropertyName is the property on which you want to make your condition DesiredValue is the particular value of the PropertyName (property) for which your other property has to be validated for required

Say you have the following

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

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

At last but not the least , register adapter for your attribute so that it can do client side validation (I put it in global.asax, Application_Start)

 DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(RequiredIfAttribute),typeof(RequiredAttributeAdapter));
Sonnnie answered 12/4, 2013 at 16:0 Comment(8)
This is was the original starting point miroprocessordev.blogspot.com/2012/08/…Sonnnie
Is there any equvalent solution in asp.net mvc2? ValidationResult, ValidationContext classes are not available in asp.net mvc2 (.net framework 3.5)Clermontferrand
@Clermontferrand I didnt work on mvc2 and dont know but if there is any validation , try to extend thatSonnnie
This only works server-side as the linked blog statesDublin
I managed to get this working in the client side with MVC5, but in client it fires up the validation no matter what the DesiredValue is.Denouement
@Dan Hunex : In MVC4, I have not managed to work properly on client side and it fires up the validation no matter what the DesiredValue is. Any help pls?Wyman
How to make client side validation work: https://mcmap.net/q/174918/-requiredif-conditional-validation-attributeErme
What is ErrorMessageResourceType ? I am passing null I am getting error.Ballad
J
39

I've been using this amazing nuget that does dynamic annotations ExpressiveAnnotations

You could validate any logic you can dream of:

public string Email { get; set; }
public string Phone { get; set; }
[RequiredIf("Email != null")]
[RequiredIf("Phone != null")]
[AssertThat("AgreeToContact == true")]
public bool? AgreeToContact { get; set; }
Jailbird answered 17/6, 2016 at 15:52 Comment(6)
The ExpressiveAnnotation library is the most flexible and generic solution of all answers here. Thanks for sharing!Gayn
I've been banging my head trying to find a solution for a solid day. ExpressiveAnnotations looks to be the fix for me!Pierette
ExpressiveAnnotation library is awesome!Arithmetician
It has client side support too!Liftoff
No support for .NET Core though, and doesn't look like it'll happen.Hill
.NET Core clone of ExpressiveAnnotations which works for me: github.com/uon-nuget/UoN.ExpressiveAnnotations.NetCorePompidou
U
18

You can disable validators conditionally by removing errors from ModelState:

ModelState["DependentProperty"].Errors.Clear();
Unqualified answered 13/7, 2010 at 20:37 Comment(0)
S
8

Thanks Merritt :)

I've just updated this to MVC 3 in case anyone finds it useful: Conditional Validation in ASP.NET MVC 3.

Sidonie answered 6/2, 2011 at 18:10 Comment(0)
R
6

There is now a framework that does this conditional validation (among other handy data annotation validations) out of the box: http://foolproof.codeplex.com/

Specifically, take a look at the [RequiredIfTrue("IsSenior")] validator. You put that directly on the property you want to validate, so you get the desired behavior of the validation error being associated to the "Senior" property.

It is available as a NuGet package.

Redmer answered 4/12, 2013 at 15:44 Comment(0)
P
3

You need to validate at Person level, not on Senior level, or Senior must have a reference to its parent Person. It seems to me that you need a self validation mechanism that defines the validation on the Person and not on one of its properties. I'm not sure, but I don't think DataAnnotations supports this out of the box. What you can do create your own Attribute that derives from ValidationAttribute that can be decorated on class level and next create a custom validator that also allows those class-level validators to run.

I know Validation Application Block supports self-validation out-of the box, but VAB has a pretty steep learning curve. Nevertheless, here's an example using 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));
        }
    }
}
Platyhelminth answered 10/3, 2010 at 16:40 Comment(1)
"You need to validate at Person level, not on Senior level" Yes this is an option, but you loose the ability that the error is appended to particular field, that is required in the Senior object.Counterfactual
E
3

I had the same problem, needed a modification of [Required] attribute - make field required in dependence of http request.The solution was similar to Dan Hunex answer, but his solution didn't work correctly (see comments). I don't use unobtrusive validation, just MicrosoftMvcValidation.js out of the box. Here it is. Implement your custom attribute:

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
    }


}

Then you need to implement your custom provider to use it as an adapter in your 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 };
      }
}

And modify your global.asax with a line

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

and here it is

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

The main advantage for me is that I don't have to code custom client validator as in case of unobtrusive validation. it works just as [Required], but only in cases that you want.

Extrovert answered 6/3, 2014 at 9:20 Comment(1)
The part about extending DataAnnotationsModelValidator was exactly what I needed to see. Thank you.Byssus
M
2

Check out Simon Ince's Conditional Validation in MVC.

I am working through his example project right now.

Mientao answered 25/10, 2010 at 20:52 Comment(0)
C
0

Typical usage for conditional removal of error from Model State:

  1. Make conditional first part of controller action
  2. Perform logic to remove error from ModelState
  3. Do the rest of the existing logic (typically Model State validation, then everything else)

Example:

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

In your example, keep everything as is and add the logic suggested to your Controller's Action. I'm assuming your ViewModel passed to the controller action has the Person and Senior Person objects with data populated in them from the UI.

Crenelate answered 9/8, 2015 at 20:10 Comment(0)
M
0

I'm using MVC 5 but you could try something like this:

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; }

In your case you would say something like "IsSenior == true". Then you just need to check the validation on your post action.

Mauromaurois answered 6/1, 2017 at 18:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.