asp.net-mvcasp.net-mvc-3

MVC3 unobtrusive validation group of inputs


I need to validate 3 or more input fields (required at lest one). For example I have Email, Fax, Phone.

I require at least ONE to be filled in. I need both server and client 'unobtrusive validation'. please help. I looked into "Compare" method and tried modifying it but no luck. please help. thanks


Solution

  • 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.