CheckboxLists in MVC3

by MikeHogg2. August 2012 09:48

 

MVC does not really model bind checkboxes. The problem is html based; input checkboxes do not post in the form unless actually checked. MVC gets around this with their helper by creating a hidden for every checkbox, and I assume unobstrusively sets the hidden input whenever the associated checkbox is toggled.

 

This means that we cannot validate the checkbox as required, which isn't an easy case anyway since a bool defaults to false. It’s not a nullable type.  And we can't readily access the hidden input to validate that.  People have created several roll-your-own extensions and helpers out there… but I couldn’t find one that fit my needs exactly.

 

My main requirement was a list of boxes (a grouping so to speak) requiring at least one to be checked. With client validation (most examples on the web missed this one).  SO I found myself a little library project.

 

Two parts to this-

  • - [In your Model]: add your list of checkboxes like this: IEnumerable<MH.Mvc.Checkbox> { new {Name="Bank"} new {Name="Stocks", IsChecked?="true"}}
    • decorate your checkboxlist with [MH.Attributes.CheckBoxListServerValidator], and you will get server validation that at least one box is checked
    • In your View, use the HtmlHelper.CheckBoxListFor(m=>m.checkboxlist) which gives you the proper model binding
  • - [In your Model]: add a bool named something like this: CheckboxClientValidator
    • decorate your bool with [MH.Attributes.CheckBoxListClientValidator(listname)], and
    • add the MH/Content/scripts/custom-validators js to your page and you will get client side validation that at least one box is checked

Here is what my Model looks like:

 

 

[MH.Web.Mvc3.Attributes.CheckBoxListClientValidator("LiquidAssetSourceList")]
public bool LiquidAssetClientValidator { get; set; } 
        [Display(Name = "Where do you have your liquid assets? (Check all that apply.)*")]
        [MH.Web.Mvc3.Attributes.CheckBoxListServerValidator]
public IEnumerable<MH.Web.Mvc3.Models.CheckBox> LiquidAssetSourceList { get; set; }

 

In my Model constructor (isn't that where all lists should get instantiated?) I create the actual checkboxes

 

 

LiquidAssetSourceList = new List<MH.Web.Mvc3.Models.CheckBox>
                { new MH.Web.Mvc3.Models.CheckBox{ Name = "Bank" },
 new MH.Web.Mvc3.Models.CheckBox{ Name = "Stocks" },

 

... and Here is what my View looks like:

 

@Html.LabelFor(model => model.LiquidAssetSourceList)
@Html.HiddenFor(m=>m.LiquidAssetClientValidator)
@Html.ValidationMessageFor(m=>m.LiquidAssetClientValidator)
@Html.CheckboxListFor(m=>m.LiquidAssetSourceList)
Here's the simple checkbox object:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MH.Models
{
public class CheckBox
    {
public int ID { get; set; }
public string Name { get; set; }
public bool IsChecked { get; set; } 
    }
}

 

And the HtmlExtension:

public static System.Web.Mvc.MvcHtmlString CheckboxListFor<TModel, TProperty>(this HtmlHelper<TModel> html,
            Expression<Func<TModel, TProperty>> checkboxlist, object htmlattributes = null)
             where TProperty : IEnumerable<Models.CheckBox>
        {
            MemberExpression body = checkboxlist.Body as MemberExpression;
 string checkboxlistname = body.Member.Name;
            //Get currently selected values from the ViewData model
            IEnumerable<Models.CheckBox> cbs = checkboxlist.Compile().Invoke(html.ViewData.Model);
            StringBuilder sb = new StringBuilder();
            int idx = 0;
            foreach (Models.CheckBox cb in cbs)
            {
 string id = String.Format("{0}_{1}__IsChecked", checkboxlistname, idx);
 string name = String.Format("{0}[{1}].IsChecked", checkboxlistname, idx);
                TagBuilder itemdiv = new TagBuilder("div");
                itemdiv.AddCssClass("checkboxlist-itemdiv");
                TagBuilder label = new TagBuilder("label");
                label.AddCssClass("checkboxlist-label");
                label.MergeAttribute("for", id);
                label.InnerHtml = cb.Name;
                itemdiv.InnerHtml += label.ToString(TagRenderMode.Normal);
                TagBuilder inputcheck = new TagBuilder("input");
 if (cb.IsChecked) inputcheck.MergeAttribute("checked", "checked");
                inputcheck.AddCssClass("checkboxlist-checkbox");
                inputcheck.MergeAttribute("type", "checkbox");
                inputcheck.MergeAttribute("id", id);
                inputcheck.MergeAttribute("name", name);
                inputcheck.MergeAttribute("value", "true");
                itemdiv.InnerHtml += inputcheck.ToString(TagRenderMode.Normal);
                TagBuilder hiddencheck = new TagBuilder("input");
                hiddencheck.MergeAttribute("type", "hidden");
                hiddencheck.MergeAttribute("name", name);
                hiddencheck.MergeAttribute("value", "false");  // input checkboxes only post if checked, so we default to hidden with same id... mvc only takes first input of each id
                itemdiv.InnerHtml += hiddencheck.ToString(TagRenderMode.Normal);
                TagBuilder hiddenname = new TagBuilder("input");
                hiddenname.MergeAttribute("type", "hidden");
                hiddenname.MergeAttribute("name", String.Format("{0}[{1}].Name", checkboxlistname, idx));
                hiddenname.MergeAttribute("value", cb.Name);
                itemdiv.InnerHtml += hiddenname.ToString(TagRenderMode.Normal);
                TagBuilder hiddenid = new TagBuilder("input");
                hiddenid.MergeAttribute("type", "hidden");
                hiddenid.MergeAttribute("name", String.Format("{0}[{1}].ID", checkboxlistname, idx));
                hiddenid.MergeAttribute("value", cb.ID.ToString());
                itemdiv.InnerHtml += hiddenid.ToString(TagRenderMode.Normal);
                sb.Append(itemdiv.ToString(TagRenderMode.Normal));
                idx++;
            }
            TagBuilder div = new TagBuilder("div");
            div.AddCssClass("checkboxlist-paneldiv");
            div.MergeAttribute("id", checkboxlistname);
            div.MergeAttribute("name", checkboxlistname);
            div.MergeAttributes(new System.Web.Routing.RouteValueDictionary(htmlattributes));
            div.InnerHtml = sb.ToString();
 return new System.Web.Mvc.MvcHtmlString(div.ToString(TagRenderMode.Normal));
        } 

The attribute for the validator:

 

Namespace Attributes{
[AttributeUsage(AttributeTargets.Property)] 
public class CheckBoxListServerValidator : ValidationAttribute
    { 
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
 if (value != null) 
            { 
 if ((value as IEnumerable<Euro.Web.Mvc3.Models.CheckBox>).Any(l => l.IsChecked)) return ValidationResult.Success;
            }
 return new ValidationResult("Please select an option.");
        } 
    }
    [AttributeUsage(AttributeTargets.Property)]  // validon bool? i dont think it matters
public class CheckBoxListClientValidator : ValidationAttribute, IClientValidatable
    {
public string ListName { get; set; }
public CheckBoxListClientValidator(string listname)
        {
            ListName = listname;
        }
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        { 
 return ValidationResult.Success;
        }
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
        {
            ModelClientValidationRule rule = new ModelClientValidationRule()
                {
                    ValidationType = "requiredcheckbox", // "requiredcheckbox" used in jquery
                    ErrorMessage = "Please select at least one"
                };
            rule.ValidationParameters.Add("listname", ListName);
 return new[] { rule };
        }
    }

and the js for the validator:

In custom-validators.js
/// (do not put validators inside document.ready)
/// these two are for MH.Attributes.CheckBoxListClientValidator 
$.validator.addMethod("requiredcheckbox",
 function (value, element, parameters) {
 return $("#" + parameters.listname + " input:checked").length > 0;
                });
$.validator.unobtrusive.adapters.add("requiredcheckbox", ["listname"], function (options) {
    options.rules["requiredcheckbox"] = options.params;
    options.messages["requiredcheckbox"] = options.message;
});

Tags:

MVC

Add comment

biuquote
  • Comment
  • Preview
Loading

About Mike Hogg

Mike Hogg is a c# developer in Brooklyn.

More Here

Favorite Books

This book had the most influence on my coding style. It drastically changed the way I write code and turned me on to test driven development even if I don't always use it. It made me write clearer, functional-style code using more principles such as DRY, encapsulation, single responsibility, and more.amazon.com

This book opened my eyes to a methodical and systematic approach to upgrading legacy codebases step by step. Incrementally transforming code blocks into testable code before making improvements. amazon.com

More Here