MVC Property Validation Gotcha
Recently I was working on an MVC 4 application and I ran into a couple of issues with how property validation works with the default binder. I thought it was useful enough to share.
Conditionally Required Fields
The first issue I ran into was conditionally requiring a field based upon some other field. In my case it was for collection payment information. On my site I can accept either credit cards or checks. For credit cards I need to collect one set of data. For checks a different set. The user selects the payment method and the appropriate fields are shown. The model to support this exposes a simple boolean property called IsCreditCard. Now comes the fun part, marking the fields as required. Because of how we render our required fields on the client I needed to apply the RequiredAttribute on the fields to get the client-side behavior correct. But then all the fields would be required irrelevant of payment method. Therefore I created a new validation attribute deriving from RequiredAttribute called RequiredIfAttribute. This attribute allows me to specify a property to check to see if the property is actually required. If the property is true then the standard require validation is performed.
I dropped this attribute onto the appropriate fields and then ran the code. To my surprise none of the fields were required. Setting up a breakpoint I refreshed the page and suddenly all the fields were required. What in the world? I stopped the debugger and restarted it and then ran my test again. This time when the breakpoint was hit I noticed my IsCreditCard property was false which wasn't right. Then it hit me. MVC runs the property's validators right after it sets the property's value. In this case the IsCreditCard property happens to be lower than the field I was validating so the conditional property hadn't been set yet. Hence the first time I ran it the value was false. But after the first refresh the property was set to true, at least until its value gets updated by the binder.
The solution to this was simple. I moved the conditional property, IsCreditCard, to the top of the model so it would be set before any properties that rely on its value. The moral is that validators run when the property is set. Note that I could have implemented IValidatableObject but then I couldn't use the RequiredAttribute which I needed for the client-side behavior.
Another issue I had was with cross-property validation. In this case the user must enter one of two fields. They cannot be marked as required because only one technically is. I thought that the new RequiredIfAttribute I created might help but since property's are set and then validated it would only work for one. A couple of solutions came to mind.
The first solution would be to ignore the first property (first being in model order) and use RequiredIfAttribute on the second property to validate them both. But this would only allow me to set an error on the second property which wasn't good enough. For this view if both fields are empty then both must be flagged as in error.
The second solution was to implement IValidatableObject. This interface is designed to allow a model to validate itself once all properties are set. It has a single Validate method that returns the list of validation errors as ValidationResult. For cross-property validation this is the way to go but there is a problem. With the data annotations an error will get associated with the corresponding property. For our views this causes the field to be marked in a special way to the user. With the interface that becomes harder.
Fortunately ValidationResult allows you to specify the member(s) to apply the error to. Unfortunately it isn't obvious how to get it to work right. The constructor for type allows you to pass a list of member names. Each member is then associated with whatever error you want. For cross-property validation it would make sense to pass both properties to the constructor. This results in both fields being marked as errors but also causes two error messages in any validation summary you are using.
The correct solution to this is to generate multiple errors. The first error contains the error message and (optionally) the field to associate it with. Any related fields are added as additional ValidationResult entries but with no error message. The result is that you get one error in the validation summary and multiple fields marked as errors.
The property validation that MVC does is a lot nicer than the ASP.NET validators we use to have but there are some gaps in its behavior. It is truly designed for per-property validation. For model validation you have to add the IValidatableObject interface. This allows for more flexibility at the cost of complexity. It seems like most errors would be associated with a single field so I don't understand why the ValidationResult constructor doesn't expose an override that allows specifying an error message and a single field name. Beyond this quirk, and if you understand the validation the model binder does, then validating your models is pretty easy.
I haven't tried it but it would be nice if we could apply validation attributes to an entire model rather than implementing IValidatableObject. This would gives us the declarativeness of attributes with the ability to validate the entire model. I could foresee a lot of power annotations being created if we could do model-level validation via attributes. Maybe in a future version of MVC...