rbr
rbr

Reputation: 11

Razor Pages Multiple CheckBoxes with Model Binding and At least one option must be checked Server/Client Validation

I would like to have multiple checkboxes with validation where at least one is selected. Ideally, these checkboxes should bind to my page model.

I seem to continuously be running into different issues when I take different approaches. The following method, the client-side validation works, but then the model binding doesn't (and therefore server validation doesn't work). If I ignore client-side validation (remove javascript and the hidden asp-for="ProductLines" input), then I can get the server-side validation, and model binding to work. Ideally, I'd like to have model binding and client/server-side validation working.

I am surprised this isn't easily possible with razor pages. But, perhaps you can help? Otherwise, I may have better luck with a client-side and a server-side framework in the future. I am trying to avoid that because it seems overkill for simpler applications.

Checkbox Class:

    public class Checkbox
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public bool IsSelected { get; set; }
    }

Validation Attribute

    public class OneCheckboxMustBeChecked : ValidationAttribute, IClientModelValidator
    {

        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {

            var checkboxOptions = validationContext.ObjectInstance as List<Checkbox>;


            foreach (var option in checkboxOptions)
            {

                if (option.IsSelected)
                {

                    return ValidationResult.Success;

                }

            }

            return new ValidationResult(GetErrorMessage());

        }

        public void AddValidation(ClientModelValidationContext context)
        {
            MergeAttribute(context.Attributes, "data-val", "true");
            MergeAttribute(context.Attributes, "data-val-OneCheckboxMustBeChecked", GetErrorMessage());
        }

        private string GetErrorMessage()
        {
            return "A checkbox option must be selected";
        }

        private bool MergeAttribute(IDictionary<string, string> attributes, string key, string value)
        {
            if (attributes.ContainsKey(key))
            {
                return false;
            }

            attributes.Add(key, value);
            return true;
        }


    }

Page Model (cshtml.cs):

        [BindProperty]
        [Required]
        [DisplayName("Product Lines:")]
        [OneCheckboxMustBeChecked]
        public List<Checkbox> ProductLines { get; set; }

cshtml

<div class="form-row row">

    <label class="col-form-label pt-0" asp-for="ProductLines"></label>
    <input type="hidden" asp-for="ProductLines" />

    <div id="ProductLineCheckBoxes">
        @for (int i = 0; i < Model.ProductLines.Count; i++)
        {
            <div class="form-check form-check-inline">
                <input type="hidden" asp-for="@Model.ProductLines[i].Id" />
                <input type="hidden" asp-for="@Model.ProductLines[i].Name" />
                <input asp-for="@Model.ProductLines[i].IsSelected" class="form-check-input" />
                <label class="form-check-label" asp-for="@Model.ProductLines[i].IsSelected">
                    @Model.ProductLines[i].Name
                </label>
            </div>
        }
    </div>

    <span class="text-danger" asp-validation-for="ProductLines"></span>
</div>

Unobtrusive Jquery Validation

//This is used to include hidden input tags into our unobtrusive jquery validation. 
//hidden input fields are not included in validation by default
$.validator.setDefaults({
    ignore: ""
});

$.validator.addMethod('OneCheckboxMustBeChecked',
    function(value, element) {

        var checked = $(element.nextElementSibling).find('input:checked');

        if (checked.length === 0) {
            return false;
        }

        return true;

    });

$.validator.unobtrusive.adapters.add('OneCheckboxMustBeChecked',
    [],
    function (options) {
        options.rules['OneCheckboxMustBeChecked'] = [];
        options.messages['OneCheckboxMustBeChecked'] = options.message;
    });

Upvotes: 1

Views: 2002

Answers (1)

Zhi Lv
Zhi Lv

Reputation: 21581

I seem to continuously be running into different issues when I take different approaches. The following method, the client-side validation works, but then the model binding doesn't (and therefore server validation doesn't work). If I ignore client-side validation (remove javascript and the hidden asp-for="ProductLines" input), then I can get the server-side validation, and model binding to work. Ideally, I'd like to have model binding and client/server-side validation working.

Based on your code, I have reproduced the problem. The issue is related the hidden filed:

<input type="hidden" asp-for="ProductLines" />

If adding the above code, it will add the client validation method to the hidden field, the Unobtrusive Jquery Validation method will execute. After validation success, if you set a debugger in the IsValid method, you can see that the parameter's value is null.

If removing the above code, since the web page doesn't contain the ProductLines element, the client validation attributes (such as "data-val" or "data-val-OneCheckboxMustBeChecked") are not added to the element. so the Unobtrusive Jquery Validation will not execute.

To solve this issue, in the OneCheckboxMustBeChecked method, after validation successful, remove the "ProductLines" hidden file.

$.validator.addMethod('OneCheckboxMustBeChecked',
    function(value, element) {

        var checked = $(element.nextElementSibling).find('input:checked');

        if (checked.length === 0) {
            return false;
        }
            
        //remove the hidden files
        $("input[name='ProductLines']").remove();

        return true;

    });

Upvotes: 0

Related Questions