MVC3 unobtrusive validation group of inputs

You could write a custom attribute:

public class AtLeastOneRequiredAttribute : ValidationAttribute, IClientValidatable
{
    private readonly string[] _properties;
    public AtLeastOneRequiredAttribute(params string[] properties)
    {
        _properties = properties;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        if (_properties == null || _properties.Length < 1)
        {
            return null;
        }

        foreach (var property in _properties)
        {
            var propertyInfo = validationContext.ObjectType.GetProperty(property);
            if (propertyInfo == null)
            {
                return new ValidationResult(string.Format("unknown property {0}", property));
            }

            var propertyValue = propertyInfo.GetValue(validationContext.ObjectInstance, null);
            if (propertyValue is string && !string.IsNullOrEmpty(propertyValue as string))
            {
                return null;
            }

            if (propertyValue != null)
            {
                return null;
            }
        }

        return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
    }

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        var rule = new ModelClientValidationRule
        {
            ErrorMessage = ErrorMessage,
            ValidationType = "atleastonerequired"
        };
        rule.ValidationParameters["properties"] = string.Join(",", _properties);

        yield return rule;
    }
}

which could be used to decorate one of your view model properties (the one you want to get highlighted if validation fails):

public class MyViewModel
{
    [AtLeastOneRequired("Email", "Fax", "Phone", ErrorMessage = "At least Email, Fax or Phone is required")]
    public string Email { get; set; }
    public string Fax { get; set; }
    public string Phone { get; set; }
}

and then a simple controller:

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

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

Rendering the following view which will take care of defining the custom client side validator adapter:

@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(
        'atleastonerequired', ['properties'], function (options) {
            options.rules['atleastonerequired'] = options.params;
            options.messages['atleastonerequired'] = options.message;
        }
    );

    jQuery.validator.addMethod('atleastonerequired', function (value, element, params) {
        var properties = params.properties.split(',');
        var values = $.map(properties, function (property, index) {
            var val = $('#' + property).val();
            return val != '' ? val : null;
        });
        return values.length > 0;
    }, '');
</script>

@using (Html.BeginForm())
{
    @Html.ValidationSummary(false)

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

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

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

    <input type="submit" value="OK" />
}

Of course the custom adapter and validator rule should be externalized into a separate javascript file to avoid mixing script with markup.


I spent more than 36 hours why the code did not work for me.. At the end , I found out that in my case , I was not supposed to use the property names in this line of code

[AtLeastOneRequired("Email", "Fax", "Phone", ErrorMessage = "At least Email, Fax or Phone is required")]

But I had to use the HTMl element Ids in place of the property names and it worked like magic.

Posting this here if it might help somebody.


Since you are using MVC 3, take a look at great video Brad Wilson had on mvcConf. There's everything you need to create client + server Unobtrusive Validation