MVC3 unobtrusive validation group of inputs
Asked Answered
L

4

44

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

Laboured answered 28/4, 2011 at 9:15 Comment(1)
You will have to write your custom validator along with some js start here haacked.com/archive/2009/11/19/…Failsafe
C
86

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.

Critta answered 28/4, 2011 at 11:37 Comment(6)
this helped out tremendously. Thanks!Laboured
For some reason Client validation does not fire :(.??Laboured
@Shane Km, is the ClientValidationEnabled property set to true in web.config? Also ensure that you are using a jquery.validate plugin version that is compatible with jquery. There was an older version which was not compatible with jQuery 1.5.Critta
The validation here is attached only to the Email. How does validation fire if only Fax or Phone are entered? If Fax was filled in and then removed, the validation would not fire.Byroad
I'm not too sure how the implementation is working in the example here. The only way I could get this to work is to add the attribute above one of the properties like so [MultiFieldRequired(new string[3]{"Phone1", "Phone2", "Phone3"}, ErrorMessage ="Phone# required")] ------The code only accepts an array. I'm not sure how people are using these comma separated strings and having it work.Thibaud
Just adding to the answer: You should be able to add the attrbiute to all properties that you want to highlight in case you want to highlight all of them.Atomize
F
5

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.

Flush answered 2/9, 2011 at 12:46 Comment(0)
G
2

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

Gastrovascular answered 28/4, 2011 at 10:11 Comment(0)
A
2

@Darin Dimitrov 's solution is probably the standard of creating a custom validation attribute that works with unobtrusive validation. However, using custom validation attributes for unobtrusive validation have some disadvantages such as:

  • The custom validation attribute is only attached to one properties, so client validation will not work if there's a change event on the other two inputs.
  • The error message works fine with ValidationSummary, but if you want to display 1 error message for the whole group (which I think is the norm), it would be nearly impossible.
  • To avoid the first problem, we can add custom validation attribute to each element in the group, which will cause another problem: we have to validate all elements of the group, instead of stopping at the first invalid group element. And of course, the second problem - separate error messages for each element - still remains.

There's another way to handle client side validation of group of inputs, using groups setting in jquery validator (https://jqueryvalidation.org/validate/#groups). The only problem (and a big one) is that unobtrusive validation doesn't support jquery validation's groups by default, so we have to customize a little bit.

Although this answer is hardly "unobtrusive", in my opinion, it is worth trying to get rid of unnecessary complication of code, if your final goal is to validate a group of inputs while using Microsoft's unobtrusive validator library.

First, because groups settings of default jquery validator is not available in jquery unobtrusive validator, we have to override unobtrusive settings (ref. How can I customize the unobtrusive validation in ASP.NET MVC 3 to match my style?)

$("form").on('submit', function () {
    var form = this;
    var validator = $(this).data("validator");

    if (validator.settings && !validator.settings.submitHandler) {
        $.extend(true, validator.settings.rules, validationSettings.rules);
        $.extend(true, validator.settings.groups, validationSettings.groups);
        initGroups(validator);

        var fnErrorReplacement = validator.settings.errorPlacement;
        validator.settings.errorPlacement = function (error, element) {
            validationSettings.errorPlacement(error, element, fnErrorReplacement, form);
        }
        validator.settings.submitHandler = formSubmitHandler;
    }
});

function formSubmitHandler(form) {
    form.submit();
}

After that, override unobtrusive validator's groups, rules and errorPlacement settings.

var validationSettings = {
groups: {
    checkboxgroup: "Email Fax Phone"
},
rules: {
    Email: {
        required: function () {
            return validateCheckboxGroup(["#Email", "#Fax", "#Phone"]);
        }
    },
    Fax: {
        required: function () {
            return validateCheckboxGroup(["#Email", "#Fax", "#Phone"]);
        }
    },
    Phone: {
        required: function () {
            return validateCheckboxGroup(["#Email", "#Fax", "#Phone"]);
        }
    }
}
,
errorPlacement: function (error, element, fnUnobtrusive, form) {
    switch (element.attr("name")) {
        case "Email":
        case "Fax":
        case "Phone":
            onGroupError(error, "CheckBoxGroup", form);
            break;
        default:
            fnUnobtrusive(error, element);
            break;
    }
}
}

function validateCheckboxGroup(names) {
var result = true;
$.each(names, function (index, value) {
    if ($(value).is(":checked")) {
        result = false;
    }
});
return result;
}

Because unobtrusive validator does not implement groups setting of jquery validator, we need to reuse two functions from the two libraries to: (1).split group names (reusing code from jquery validator) and (2) append error element without remove 'input-validation-error' class (reusing function onError from unobtrusive library).

function initGroups(validators) {
validators.groups = {};
$.each(validators.settings.groups,
    function (key, value) {
        if (typeof value === "string") {
            value = value.split(/\s/);
        }
        $.each(value,
            function (index, name) {
                validators.groups[name] = key;
            });
    });
}

function onGroupError(error, inputElementName, form) {
var container = $(form).find("[data-valmsg-for='" + inputElementName + "']"),
replaceAttrValue = container.attr("data-valmsg-replace"),
replace = replaceAttrValue ? $.parseJSON(replaceAttrValue) !== false : null;

container.removeClass("field-validation-valid").addClass("field-validation-error");
error.data("unobtrusiveContainer", container);

if (replace) {
    container.empty();
    error.appendTo(container);
}
else {
    error.hide();
}
}

Finally, use HtmlExtensions.ValidationMessage to create error span of the checkbox group.

@Html.ValidationMessage("CheckBoxGroup", new { @class = "text-danger" }) 

The keeping of "input-validation-error" class is necessary, so that jquery validator will validate all 3 elements (Email, Phone, Fax) of checkbox group as a whole, instead of validating one by one. The unobtrusive validation library remove this class by default on function onError, so we have to customize this as shown in function onGroupError above.

Angelineangelique answered 22/8, 2016 at 19:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.