Search code examples
c#asp.net-mvcrazorasp.net-core-mvc

ASP.NET MVC Data Annotation validate a group checkboxes


I am new to ASP.NET MVC and working on the form validation. I am stuck with a group checkboxes validation. Basically, I am trying to make the user select at least one hobby in a group of checkboxes.

The model below. you can see that I tried to implement AtLeastOneCheckedAttribute, but couldn't get it to work properly.

public class EmployeeModel2
{
    [Required(AllowEmptyStrings = false, ErrorMessage = "Please Provide First Name")]
    [StringLength(20, MinimumLength = 5, ErrorMessage = "First Name Should be min 5 and max 20 length")]
    public string FirstName { get; set; }

    [Required(AllowEmptyStrings = false, ErrorMessage = "Please Provide Last Name")]
    [StringLength(20, MinimumLength = 5, ErrorMessage = "First Name Should be min 5 and max 20 length")]
    public string LastName { get; set; }

    [Required(AllowEmptyStrings = false, ErrorMessage = "Please Provide Eamil")]
    [RegularExpression("^[a-zA-Z0-9_\\.-]+@([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}$", ErrorMessage = "Please Provide Valid Email")]
    public string Email { get; set; }

    [Required(AllowEmptyStrings = false, ErrorMessage = "Please Provide Age")]
    [Range(25, 45, ErrorMessage = "Age Should be min 25 and max 45")]
    public int? Age { get; set; }

    [Required(ErrorMessage = "Please Provide Gender")]
    public string Gender { get; set; }

    //[DisplayFormat(DataFormatString = "{0:dd.MM.yyyy}")]
    [DisplayFormat(DataFormatString = "{0:MM/dd/yyyy}")]
    [Required(ErrorMessage = "Please enter Date of Birth")]
    public DateTime DOB { get; set; }

    [DataType(DataType.Password)]
    [Required(ErrorMessage = "Please enter password")]
    public string Password { get; set; }

    [Required(ErrorMessage = "Please enter ConfirmPassword")]
    [DataType(DataType.Password)]
    [Compare("Password", ErrorMessage = "Password not matching")]
    public string ConfirmPassword { get; set; }
    
    public List<HobbyMV> Hobbies { get; set; }

    [AtLeastOneChecked(ErrorMessage = "Please select at least one checkbox")]
    public int[] selectedHobbies { get; set; }
}

// I tried this code, but it is not working properly
public class AtLeastOneCheckedAttribute : ValidationAttribute
{
    public override bool IsValid(object value)
    {
        var instance = value as IEnumerable<HobbyMV>;

        if (instance != null)
        {
            return true;
        }

        return false;
    }
}

The form in the view. The issue is that selectedHobbies group checkboxes keeps prompting "Please select at least one checkbox" even I selected a few checkboxes.

@using (Html.BeginForm("Validation2", "XForm", FormMethod.Post, htmlAttributes: new
{@class = "form"}))
{
    @Html.AntiForgeryToken()

    <div class="form-group">
        @Html.LabelFor(model => model.FirstName)
        @Html.TextBoxFor(model => model.FirstName, new { @class = "form-control", title = "Enter your first name" })
        @Html.ValidationMessageFor(m => m.FirstName, "", new {@class = "text-danger"})
    </div>

    <div class="form-group">
        @Html.LabelFor(model => model.LastName)
        @Html.TextBoxFor(model => model.LastName, new { @class = "form-control", title = "Enter your last name" })
        @Html.ValidationMessageFor(m => m.LastName, "", new {@class = "text-danger"})
    </div>

    <div class="form-group">
        @Html.LabelFor(model => model.Email)
        @Html.TextBoxFor(model => model.Email, new { @class = "form-control", title = "Enter your email address" })
        @Html.ValidationMessageFor(m => m.Email, "", new {@class = "text-danger"})
    </div>

    <div class="form-group">
        @Html.LabelFor(model => model.Age)
        @Html.TextBoxFor(model => model.Age, new { @class = "form-control", title = "Enter your age" })
        @Html.ValidationMessageFor(m => m.Age, "", new {@class = "text-danger"})
    </div>
 
    <div class="editor-field">
        @Html.EditorFor(model => model.DOB)
        @Html.ValidationMessageFor(model => model.DOB, "", new {@class = "text-danger"})
    </div>

    <div class="editor-label">
        @Html.LabelFor(model => model.Password)
    </div>

    <div class="editor-field">
        @Html.EditorFor(model => model.Password)
        @Html.ValidationMessageFor(model => model.Password, "", new {@class = "text-danger"})
    </div>

    <div class="editor-label">
        @Html.LabelFor(model => model.ConfirmPassword, "", new {@class = "text-danger"})
    </div>

    <div class="editor-field">
        @Html.EditorFor(model => model.ConfirmPassword)
        @Html.ValidationMessageFor(model => model.ConfirmPassword, "", new {@class = "text-danger"})
    </div>

    <div class="form-group" >
        <label><b>Gender</b></label>
        <div> 
            @Html.RadioButtonFor(m => m.Gender, "Male") 
            <label>Male</label>
            @Html.RadioButtonFor(m => m.Gender, "Female") 
            <label> Female </label>
            @Html.ValidationMessageFor(m => m.Gender, "", new {@class = "text-danger"}) 
        </div>
    </div>

    <div class="form-group" >
        <label><b>Hobby</b></label><br/>
        @foreach (var item in Model.Hobbies)
        {
            <span style="padding:10px;">
                <label><input type="checkbox" value="@item.HobbyId" checked="@item.IsSelected" name="selectedHobbies" id="@item.HobbyId" /> @Html.Raw(@item.Description)</label>
            </span>
        }
        @Html.ValidationMessageFor(m => m.selectedHobbies, "", new {@class = "text-danger"})
    </div>

    <div class="form-group"> 
         <input type = "submit" value="Submit" class="btn-success" /> 
     </div>
}

If I selected some checkboxes for the hobby and click Submit, it did not pass the validation (screenshot below) and the message "Please select at least one checkbox" does not go away.

enter image description here

Please help. Thanks in advance.


Solution

  • Try to use indexing to make list binding for the Hobbies to work correct:

    <div class="form-group" >
        <label><b>Hobby</b></label><br/>   
        @for (int i = 0; i < Model.Hobbies.Count; i++)
        {
            <span style="padding:10px;">      
                @Html.CheckBoxFor(r => Model.Hobbies[i].IsSelected)
                @Html.Raw(@Model.Hobbies[i].Description)
                @Html.HiddenFor(h => @Model.Hobbies[i].HobbyId)
                @Html.HiddenFor(h => @Model.Hobbies[i].Description)
            </span>          
        }
        @Html.ValidationMessageFor(m => m.Hobbies, "", new { @class = "text-danger" })
    </div>
    

    Specify the AttributeTargets.Property for the custom attribute:

    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public class AtLeastOneCheckedAttribute : ValidationAttribute
    {
        public AtLeastOneCheckedAttribute(string errorMessage) : base(errorMessage)
        {
        }
    
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)    
        {
            // Check that list doesn't empty and at least one item is selected
            if (value is IEnumerable<HobbyMV> list && list.Any(h => h.IsSelected))
            {
                return ValidationResult.Success;
            }
            return new ValidationResult(ErrorMessage); 
        }
    }
    

    And apply the AtLeastOneChecked attribute to the Hobbies list:

    [AtLeastOneChecked("Please select at least one checkbox")]
    public List<HobbyMV> Hobbies { get; set; }