How to validate one field related to another's value in ASP .NET MVC 3

One possibility is to write a custom validation attribute:

public class RequiredIfOtherFieldIsNullAttribute : ValidationAttribute, IClientValidatable
{
    private readonly string _otherProperty;
    public RequiredIfOtherFieldIsNullAttribute(string otherProperty)
    {
        _otherProperty = otherProperty;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var property = validationContext.ObjectType.GetProperty(_otherProperty);
        if (property == null)
        {
            return new ValidationResult(string.Format(
                CultureInfo.CurrentCulture, 
                "Unknown property {0}", 
                new[] { _otherProperty }
            ));
        }
        var otherPropertyValue = property.GetValue(validationContext.ObjectInstance, null);

        if (otherPropertyValue == null || otherPropertyValue as string == string.Empty)
        {
            if (value == null || value as string == string.Empty)
            {
                return new ValidationResult(string.Format(
                    CultureInfo.CurrentCulture,
                    FormatErrorMessage(validationContext.DisplayName),
                    new[] { _otherProperty }
                ));
            }
        }

        return null;
    }

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        var rule = new ModelClientValidationRule
        {
            ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()),
            ValidationType = "requiredif",
        };
        rule.ValidationParameters.Add("other", _otherProperty);
        yield return rule;
    }
}

which you would apply to one of the properties of your view model:

public class MyViewModel
{
    [RequiredIfOtherFieldIsNull("Mobile")]
    public string Phone { get; set; }

    public string Mobile { get; set; }
}

then you could have a controller:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View(new MyViewModel());
    }

    [HttpPost]
    public ActionResult Index(MyViewModel model)
    {
        return View(model);
    }
}

and finally a view in which you will register an adapter to wire the client side validation for this custom rule:

@model MyViewModel

<script src="@Url.Content("~/Scripts/jquery.validate.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.js")" type="text/javascript"></script>
<script type="text/javascript">
    jQuery.validator.unobtrusive.adapters.add(
        'requiredif', ['other'], function (options) {

            var getModelPrefix = function (fieldName) {
                return fieldName.substr(0, fieldName.lastIndexOf('.') + 1);
            }

            var appendModelPrefix = function (value, prefix) {
                if (value.indexOf('*.') === 0) {
                    value = value.replace('*.', prefix);
                }
                return value;
            }

            var prefix = getModelPrefix(options.element.name),
                other = options.params.other,
                fullOtherName = appendModelPrefix(other, prefix),
                element = $(options.form).find(':input[name="' + fullOtherName + '"]')[0];

            options.rules['requiredif'] = element;
            if (options.message) {
                options.messages['requiredif'] = options.message;
            }
        }
    );

    jQuery.validator.addMethod('requiredif', function (value, element, params) {
        var otherValue = $(params).val();
        if (otherValue != null && otherValue != '') {
            return true;
        }
        return value != null && value != '';
    }, '');
</script>

@using (Html.BeginForm())
{
    <div>
        @Html.LabelFor(x => x.Phone)
        @Html.EditorFor(x => x.Phone)
        @Html.ValidationMessageFor(x => x.Phone)
    </div>

    <div>
        @Html.LabelFor(x => x.Mobile)
        @Html.EditorFor(x => x.Mobile)
        @Html.ValidationMessageFor(x => x.Mobile)
    </div>

    <button type="submit">OK</button>
}

Pretty sick stuff for something so extremely easy as validation rule that we encounter in our everyday lives. I don't know what the designers of ASP.NET MVC have been thinking when they decided to pick a declarative approach for validation instead of imperative.

Anyway, that's why I use FluentValidation.NET instead of data annotations to perform validations on my models. Implementing such simple validation scenarios is implemented in a way that it should be - simple.


I know this question is not so hot, because it was asked relatively long time ago, nevertheless I'm going to share with a slightly different idea of solving such an issue. I decided to implement mechanism which provides conditional attributes to calculate validation results based on other properties values and relations between them, which are defined in logical expressions.

Your problem can be defined and automatically solved by the usage of following annotations:

[RequiredIf("Mobile == null",
    ErrorMessage = "At least email or phone should be provided.")]
public string Phone{ get; set; }

[RequiredIf("Phone == null",
    ErrorMessage = "At least email or phone should be provided.")]
public string Mobile { get; set; }

If you feel it would be useful for your purposes, more information about ExpressiveAnnotations library can be found here. Client side validation is also supported out of the box.