MVC 3 RequiredIf validator for multiple values
Recently I was required to create a form that included the dependencies of "If zzz field equals xx, then yyy field is required" hierarchical type structure to my viewmodel, and came across this awesome post by Simon Ince describing exactly how to do it for a 1:1 relationship using DataAttributes... Problem for me: I needed the field to be Required if the selected value in my dropdownlist was either 1, 2 or 3. It was not required if the value was 0.
Uh ohhhhhhh......
well, I changed his control a bit and it was surprisingly easy to do (cheers, microsoft). Here is my code! (note: if you use my code instead of Simon's, its the exact same syntax to call it so you can switch it out with this one with no repercussions, this one just accepts an unlimited number of items to compare against if need be not just a single one).
using System;using System.Collections.Generic;using System.Linq;using System.Web;using System.ComponentModel.DataAnnotations;using System.Web.Mvc;using System.Collections;using System.Text;namespace MyProject.Validation.ValidationAttributes{public class RequiredIfAttribute : ValidationAttribute, IClientValidatable{private RequiredAttribute _innerAttribute = new RequiredAttribute();private string _dependentProperty;private object[] _targetValue;public RequiredIfAttribute(string dependentProperty, params object[] targetValue){this._dependentProperty = dependentProperty;this._targetValue = targetValue;}protected override ValidationResult IsValid(object value, ValidationContext validationContext){// get a reference to the property this validation depends uponvar containerType = validationContext.ObjectInstance.GetType();var field = containerType.GetProperty(this._dependentProperty);if (field != null){// get the value of the dependent propertyvar dependentvalue = field.GetValue(validationContext.ObjectInstance, null);foreach (var obj in _targetValue){// compare the value against the target valueif ((dependentvalue == null && this._targetValue == null) ||(dependentvalue != null && dependentvalue.Equals(obj))){// match => means we should try validating this fieldif (!_innerAttribute.IsValid(value))// validation failed - return an errorreturn new ValidationResult(this.ErrorMessage, new[] { validationContext.MemberName });}}}return ValidationResult.Success;}public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context){var rule = new ModelClientValidationRule(){ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()),ValidationType = "requiredif",};string depProp = BuildDependentPropertyId(metadata, context as ViewContext);// find the value on the control we depend on;// if it's a bool, format it javascript style// (the default is True or False!)StringBuilder sb = new StringBuilder();foreach (var obj in this._targetValue){string targetValue = (obj ?? "").ToString();if (obj.GetType() == typeof(bool))targetValue = targetValue.ToLower();sb.AppendFormat("|{0}", targetValue);}rule.ValidationParameters.Add("dependentproperty", depProp);rule.ValidationParameters.Add("targetvalue", sb.ToString().TrimStart('|'));yield return rule;}private string BuildDependentPropertyId(ModelMetadata metadata, ViewContext viewContext){// build the ID of the propertystring depProp = viewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(this._dependentProperty);// unfortunately this will have the name of the current field appended to the beginning,// because the TemplateInfo's context has had this fieldname appended to it. Instead, we// want to get the context as though it was one level higher (i.e. outside the current property,// which is the containing object (our Person), and hence the same level as the dependent property.var thisField = metadata.PropertyName + "_";if (depProp.StartsWith(thisField))// strip it off againdepProp = depProp.Substring(thisField.Length);return depProp;}}}
PS, It does client validation too.
$.validator.addMethod('requiredif',function (value, element, parameters) {var id = '#' + parameters['dependentproperty'];// get the target value (as a string,// as that's what actual value will be)var targetvalue = parameters['targetvalue'];targetvalue = (targetvalue == null ? '' : targetvalue).toString();var targetvaluearray = targetvalue.split('|');for (var i = 0; i < targetvaluearray.length; i++) {// get the actual value of the target control// note - this probably needs to cater for more// control types, e.g. radiosvar control = $(id);var controltype = control.attr('type');var actualvalue =controltype === 'checkbox' ?control.attr('checked') ? "true" : "false" :control.val();// if the condition is true, reuse the existing// required field validator functionalityif (targetvaluearray[i] === actualvalue) {return $.validator.methods.required.call(this, value, element, parameters);}}return true;});$.validator.unobtrusive.adapters.add('requiredif',['dependentproperty', 'targetvalue'],function (options) {options.rules['requiredif'] = {dependentproperty: options.params['dependentproperty'],targetvalue: options.params['targetvalue']};options.messages['requiredif'] = options.message;});
So, how do I use this and why is it useful?
For my example, I created an Enum to encapsulate my values to compare against. makes it alot neater and easier to keep track of when you have alot of requiredif validators on various fields.
public enum ApplicationStatus{Pending = 0,Submitted = 1,Disapproved = 2,Approved = 3}
And In my viewmodel, I wanted my ApplicationSubmitted field required only if the application has actually been submitted (aka, past the "pending" stage.)
public SelectList ApplicationStatus{ get; set; }[Required]public int SelectedApplicationStatus { get; set; }[RequiredIf("SelectedApplicationStatus",(int)Enums.ApplicationStatus.Submitted,(int)Enums.ApplicationStatus.Approved,(int)Enums.ApplicationStatus.Disapproved, ErrorMessage="The Application Submission Date is required")]public DateTime? ApplicationSubmitted { get; set; }
And with a simple line to display your validation message like so:
@Html.ValidationMessageFor(m => m.ApplicationSubmitted)
You're right to go.
BALLIN'
Let us know what you think