4

Is it possible to have unobtrusive validation to make a field required but only if other properties change?

For Example

[Required]
public Decimal Income {get; set;}
[Required]
public Decimal Tax {get; set;}
//Required if tax or income changes
public string ChangeReason {get; set;}

I thought about having multiple backing store fields and writing a Custom Validator to compare these, but wondered if anyone had a better suggestion?

naim shaikh
  • 1,103
  • 2
  • 9
  • 20
Cookie
  • 594
  • 1
  • 12
  • 34
  • 1
    Here's a similar question with a solution: http://stackoverflow.com/questions/2417113/asp-net-mvc-conditional-validation – JP _ Jun 15 '12 at 14:34

2 Answers2

10

Custom Validator is the way to go. I had to build something similar a while back.

I'd set up a hidden value - "Changed" - set it to true whenever the user mods the fields of interest.

Set a RequiredIf validator on the 2 properties of interest:

 [RequiredIf("Changed", true, ErrorMessage = "Required")]

The code for a RequiredIf validator is shown below:

public class RequiredIfAttribute : ValidationAttribute, IClientValidatable
{
    private RequiredAttribute _innerAttribute = new RequiredAttribute();

    public string DependentProperty { get; set; }
    public object TargetValue { get; set; }

    public RequiredIfAttribute(string dependentProperty, 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 upon
        var containerType = validationContext.ObjectInstance.GetType();
        var field = containerType.GetProperty(this.DependentProperty);

        if (field != null)
        {
            // get the value of the dependent property
            var dependentvalue = field.GetValue(validationContext.ObjectInstance, null);

            // compare the value against the target value
            if ((dependentvalue == null && this.TargetValue == null) ||
                (dependentvalue != null && dependentvalue.Equals(this.TargetValue)))
            {
                // match => means we should try validating this field
                if (!_innerAttribute.IsValid(value))
                    // validation failed - return an error
                    return new ValidationResult(this.ErrorMessage, new[] { validationContext.MemberName });
            }
        }

        return null;
    }

    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!)
        string targetValue = (this.TargetValue ?? "").ToString();
        if (this.TargetValue.GetType() == typeof(bool))
            targetValue = targetValue.ToLower();

        rule.ValidationParameters.Add("dependentproperty", depProp);
        rule.ValidationParameters.Add("targetvalue", targetValue);

        yield return rule;
    }

    private string BuildDependentPropertyId(ModelMetadata metadata, ViewContext viewContext)
    {
        // build the ID of the property
        string 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 again
            depProp = depProp.Substring(thisField.Length);
        return depProp;
    }
}

Javascript:

/// <reference path="jquery-1.4.4-vsdoc.js" />
/// <reference path="jquery.validate.unobtrusive.js" />

$.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();

    // get the actual value of the target control
    // note - this probably needs to cater for more 
    // control types, e.g. radios
    var control = $(id);
    var controltype = control.attr('type');
    var actualvalue =
        controltype === 'checkbox' ?
        control.attr('checked').toString() :
        control.val();

    // if the condition is true, reuse the existing 
    // required field validator functionality
    if (targetvalue === 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;
});
BonyT
  • 10,750
  • 5
  • 31
  • 52
  • 1
    Clientside validation will fail if you're using nested EditorFor helpers with this code. The id of the dependent property will be prefixed with the higher property names and this line here: `if (depProp.StartsWith(thisField))` will always be false. As such, the dependent property passed through as validation parameters will be incorrect and will likely throw an exception when it tries to inspect the value of the dependent control. – ajbeaven Dec 02 '13 at 20:41
  • I fixed this by replacing `depProp = depProp.Substring(thisField.Length);` with `depProp = depProp.Replace(thisField, "");` – blues_driven Jun 06 '16 at 14:27
  • Have you found a way to use this with radio buttons? This has problems as MVC renders duplicate IDs for radio buttons. The fix for that is to manually specify the IDs on the View. That change causes this validator to fail as it can't find the expected control using the ID generated in `BuildDependentPropertyId`... hrm :/ – ajbeaven Aug 24 '16 at 00:38
3

It is possible. You can write your own attribute, to do this exactly.
It basically requires two steps:

  1. Write your own attribute, make it inherit ValidationAttribute amd implement IClientValidatable
  2. Write a Jquery validation adapter to support it

A good working sample is described in this post.
I used a similar approach to create a dependency validation (one field could have values only if another was filled)

YavgenyP
  • 2,113
  • 14
  • 11