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?

Andrew
  • 4,953
  • 15
  • 40
  • 58
Peter Stegnar
  • 12,615
  • 12
  • 62
  • 80
  • 1
    I've recently asked similar question: http://stackoverflow.com/questions/2280539/custom-model-validation-of-dependent-properties-using-data-annotations – Darin Dimitrov Mar 10 '10 at 13:44
  • 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; } }`. – Steven Mar 10 '10 at 13:46
  • Steven: "IsSenior" translates to the checkbox field in the view. When user checks the "IsSenior" checkBox then the "Senior.Description" Field become mandatory. – Peter Stegnar Mar 10 '10 at 14:47
  • 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. – Peter Stegnar Mar 10 '10 at 14:51

12 Answers12

159

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).

James Skemp
  • 8,018
  • 9
  • 64
  • 107
viperguynaz
  • 12,044
  • 4
  • 30
  • 41
  • if property is "int" type, that requires value, if fill that field, Validate does not work.. – Jeyhun Rahimov Dec 06 '12 at 12:45
  • 2
    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. – NightOwl888 Jul 28 '13 at 15:45
  • 7
    you do if you implement it - see full example at http://www.falconwebtech.com/post/MVC3-Custom-Validation-Attributes-for-Client-Server-Side-Validation-with-Unobtrusive-Ajax – viperguynaz Sep 13 '13 at 15:48
  • 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) – User_MVC Nov 08 '13 at 07:46
  • 5
    http://www.falconwebtech.com/post/MVC3-Custom-Validation-Attributes-for-Client-Server-Side-Validation-with-Unobtrusive-Ajax - @viperguynaz this isn't working – Smit Patel Jun 19 '17 at 06:51
  • This isn't working. How do you pass in the validationContext when calling myViewmodel.Validate(..)? Does Validate need to be called manually? – RayLoveless Jan 10 '18 at 19:47
  • 2
    @RayLoveless you should be calling `ModelState.IsValid` - not calling Validate directly – viperguynaz Jan 11 '18 at 06:32
  • @viperguynaz 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. – RayLoveless Jan 11 '18 at 18:52
  • You need to add a Validatio message to your view - `Html.ValidationMessage("SomProperty", "*")` – viperguynaz Jan 13 '18 at 19:38
  • 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. – Bil Simser Oct 15 '18 at 16:03
  • 3
    I know this an older thread, but for completion - @viperguynaz - 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. – user3280560 Nov 04 '20 at 14:05
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).

Jakov
  • 720
  • 1
  • 12
  • 29
Peter Stegnar
  • 12,615
  • 12
  • 62
  • 80
  • 20
    The downside is that one of part your validation logic is located in the model and the other part in the controller(s). – Kristof Claes Mar 11 '10 at 12:45
  • 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. – Peter Stegnar Mar 11 '10 at 16:49
  • 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?? – Aaron Feb 21 '11 at 01:58
  • 2
    @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. – Peter Stegnar Feb 21 '11 at 12:07
  • 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. – Richard B Jun 20 '11 at 15:45
  • @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. – Peter Stegnar Jul 20 '13 at 16:57
  • @PeterStegnar will you please provide an example through an interface. I am more interested in to know how this could done with an interface. – Ammar Khan Mar 10 '16 at 11:55
  • @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? – Peter Stegnar Mar 11 '16 at 13:22
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));
Ciarán Bruen
  • 5,221
  • 13
  • 59
  • 69
Dan Hunex
  • 5,172
  • 2
  • 27
  • 38
  • This is was the original starting point http://miroprocessordev.blogspot.com/2012/08/aspnet-mvc-conditional-validation-using.html – Dan Hunex Apr 13 '13 at 17:00
  • Is there any equvalent solution in asp.net mvc2? ValidationResult, ValidationContext classes are not available in asp.net mvc2 (.net framework 3.5) – User_MVC Nov 08 '13 at 07:45
  • @User_MVC I didnt work on mvc2 and dont know but if there is any validation , try to extend that – Dan Hunex Dec 04 '13 at 23:04
  • 2
    This only works server-side as the linked blog states – Pakman Jan 29 '14 at 19:44
  • 2
    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. – Geethanga Feb 23 '14 at 05:32
  • 1
    @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? – Jack Jan 11 '15 at 18:54
  • How to make client side validation work: http://stackoverflow.com/a/15975880/2419808 – Yulian May 09 '16 at 10:08
  • What is `ErrorMessageResourceType` ? I am passing null I am getting error. – John Adam Jun 18 '17 at 08:56
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; }
Korayem
  • 12,108
  • 5
  • 69
  • 56
18

You can disable validators conditionally by removing errors from ModelState:

ModelState["DependentProperty"].Errors.Clear();
Pavel Chuchuva
  • 22,633
  • 10
  • 99
  • 115
8

Thanks Merritt :)

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

James Skemp
  • 8,018
  • 9
  • 64
  • 107
Simon Ince
  • 149
  • 1
  • 1
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.

bojingo
  • 572
  • 1
  • 6
  • 12
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.

Den
  • 360
  • 1
  • 9
  • The part about extending `DataAnnotationsModelValidator` was exactly what I needed to see. Thank you. – twip Jan 27 '16 at 19:08
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));
        }
    }
}
Steven
  • 166,672
  • 24
  • 332
  • 435
  • "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. – Peter Stegnar Mar 11 '10 at 08:34
2

Check out Simon Ince's Conditional Validation in MVC.

I am working through his example project right now.

James Skemp
  • 8,018
  • 9
  • 64
  • 107
Merritt
  • 2,333
  • 21
  • 23
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.

Jeremy Ray Brown
  • 1,499
  • 19
  • 23
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.