RequiredIf Conditional Validation Attribute

zSynopsis picture zSynopsis · Sep 12, 2011 · Viewed 115.8k times · Source

I was looking for some advice on the best way to go about implementing a validation attribute that does the following.

Model

public class MyInputModel 
{
    [Required]
    public int Id {get;set;}

    public string MyProperty1 {get;set;}
    public string MyProperty2 {get;set;}
    public bool MyProperty3 {get;set;}

}

I want to have atleast prop1 prop2 prop3 with a value and if prop3 is the only value filled it it should not equal false. How would i go about writing a validation attribute(s?) for this?

Thanks for any help!

Answer

Dan Hunex picture Dan Hunex · Apr 12, 2013

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

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 enum UserType
{
  Admin,
  Regular
}

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

EDITED

Some people was complaining that the client side fires no matter what or it does not work. So I modified the above code to do conditional client side validation with Javascript as well. For this case you don't need to register adapter

 public class RequiredIfAttribute : ValidationAttribute, IClientValidatable
    {
        private String PropertyName { get; set; }
        private Object DesiredValue { get; set; }
        private readonly RequiredAttribute _innerAttribute;

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

        protected override ValidationResult IsValid(object value, ValidationContext context)
        {
            var dependentValue = context.ObjectInstance.GetType().GetProperty(PropertyName).GetValue(context.ObjectInstance, null);

            if (dependentValue.ToString() == DesiredValue.ToString())
            {
                if (!_innerAttribute.IsValid(value))
                {
                    return new ValidationResult(FormatErrorMessage(context.DisplayName), new[] { context.MemberName });
                }
            }
            return ValidationResult.Success;
        }

        public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
        {
            var rule = new ModelClientValidationRule
            {
                ErrorMessage = ErrorMessageString,
                ValidationType = "requiredif",
            };
            rule.ValidationParameters["dependentproperty"] = (context as ViewContext).ViewData.TemplateInfo.GetFullHtmlFieldId(PropertyName);
            rule.ValidationParameters["desiredvalue"] = DesiredValue is bool ? DesiredValue.ToString().ToLower() : DesiredValue;

            yield return rule;
        }
    }

And finally the javascript ( bundle it and renderit...put it in its own script file)

$.validator.unobtrusive.adapters.add('requiredif', ['dependentproperty', 'desiredvalue'], function (options) {
    options.rules['requiredif'] = options.params;
    options.messages['requiredif'] = options.message;
});

$.validator.addMethod('requiredif', function (value, element, parameters) {
    var desiredvalue = parameters.desiredvalue;
    desiredvalue = (desiredvalue == null ? '' : desiredvalue).toString();
    var controlType = $("input[id$='" + parameters.dependentproperty + "']").attr("type");
    var actualvalue = {}
    if (controlType == "checkbox" || controlType == "radio") {
        var control = $("input[id$='" + parameters.dependentproperty + "']:checked");
        actualvalue = control.val();
    } else {
        actualvalue = $("#" + parameters.dependentproperty).val();
    }
    if ($.trim(desiredvalue).toLowerCase() === $.trim(actualvalue).toLocaleLowerCase()) {
        var isValid = $.validator.methods.required.call(this, value, element, parameters);
        return isValid;
    }
    return true;
});

You need obviously the unobstrusive validate jQuery to be included as requirement