Checkbox Grids in ASP.NET

A simple checkbox grid is often the best user interface for mapping multiple selections in multiple categories:

Food Cabernet Zinfandel Pinot
Salmon
Steak
Chicken
Chocolate

You'd think this would be easy to do with a GridView and some CheckBoxField columns, but the GridView control's underlying assumption is that you'll only be editing one row at a time. Because of that, all the checkboxes are disabled by default; only the row which has been set to "Edit" mode can be checked and unchecked.

Aside - CheckBoxField only binds to bit fields; it doesn't bind to string values which are convertible to booleans. If you're binding to a field which isn't a bit, you'll need to convert to a bit. If you have a non-bit numeric field, you can likely use (CONVERT(bit,0) as Selected).

So we can't a CheckBoxField, what do we do?

I think the best solution is to ditch the CheckBoxField control for a simple CheckBox control, either in a GridView TemplateField or a Repeater with a Checkbox control in the ItemTemplate. The CheckBox has an OnCheckedChanged event which fires for each bound control which changes status (from checked to unchecked, or from unchecked to checked).

My rule of thumb with GridView vs. Repeater: use the GridView if the GridView will do everything I need to do without tweaking it.  Once I start having to have to start hacking the GridView, I switch to the Repeater. Usually once a grid needs something a little out of the ordinary, there's a good chance I'll need to make further changes to it, and customized GridViews tend to get complicated quickly. So, here's the sample code I'd use for the above grid:

<table>
<asp:Repeater ID="FoodPairings" runat="server" 
		OnItemDataBound="FoodPairings_ItemDataBound" >
  <HeaderTemplate>
    <thead>
      <tr>
        <th>Food</th>
        <th>Cabernet</th>
        <th>Zinfandel</th>
        <th>Pinot</th>
      </tr>
    </thead>
  </HeaderTemplate>
  <ItemTemplate>
    <tr>
      <td>
        <asp:Label ID="Food" runat="server" Text='<%# Eval("Food") %>' />
      </td>
      <td>
        <asp:CheckBox ID="Cabernet" OnCheckedChanged="OnCheckChangedEvent" 
			runat="server" Checked='<%# Eval("Cabernet") %>' />
      </td>
      <td>
        <asp:CheckBox ID="Zinfandel" OnCheckedChanged="OnCheckChangedEvent" 
			runat="server" Checked='<%# Eval("Zinfandel") %>' />
      </td>
      <td>
        <asp:CheckBox ID="Pinot" OnCheckedChanged="OnCheckChangedEvent" 
			runat="server" Checked='<%# Eval("Pinot") %>' />
      </td>
    </tr>
  </ItemTemplate>
</asp:Repeater>
</table>

But how do I determine the ID for the selected checkbox?

The CheckBox OnCheckedChanged event doesn't work too well when it's databound, because the OnCheckedChanged event EventArgs doesn't have an ID which corresponds to the row and column. Handling the save requires you to know both the row and column; it's easy to determine the column by the name of CheckBox control, but there's no simple way to determine the row. The OnCheckedChanged event EventArgs Sender argument passes in the Checkbox control, but the Checkbox control doesn't give you any properties which let you stash an ID field.

If we were only working with a single Checkbox per row it wouldn't be a problem, but you've got a grid of checkboxes, there's no built-in way to get the id of the selected checkbox. CheckBox doesn't have anywhere to stash a value. I thought about some goofy hack in which I hid the ID in the Text or Tooltip, but both of those values are displayed by default.

I think a better solution, though, is to add an attribute to the checkbox control. You can add an attribute to any control using the ControlAttributes.Add(name,value) syntax, and that's a handy way to associate these values.

public void OnCheckChangedEvent(object sender, EventArgs e) { CheckBox c = (CheckBox)sender; string wineID = ((Control)c).ID; int FoodID = int.Parse(c.Attributes["FoodID"]); if (c.Checked) { //Add pairing } else { //Remove pairing } } protected void FoodPairings_ItemDataBound( object sender, RepeaterItemEventArgs e) { if (e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem) { DbDataRecord row = e.Item.DataItem as DbDataRecord; CheckBox checkbox; checkbox = e.Item.FindControl("Cabernet") as CheckBox; checkbox.Attributes.Add("FoodID", row["FoodID"].ToString()); checkbox = e.Item.FindControl("Zinfandel") as CheckBox; checkbox.Attributes.Add("FoodID", row["FoodID"].ToString()); checkbox = e.Item.FindControl("Pinot") as CheckBox; checkbox.Attributes.Add("FoodID", row["FoodID"].ToString()); } }

Is this the best solution?

I'm not sure. There are a few different ways to go about this. My approach worked for me, but I'm not sure it's the best. Here are pro's and con's to the approach I used:

Pro:

  • Reasonably simple
  • Fits with the general control approach you'd expect
  • Handling the OnCheckedChanged event keeps your save logic really simple, because you don't need to implement any special logic to check if the checkbox state has changed.

Cons:
The generated HTML is a little wacky. The checkbox is wrapped in a <span>:

<span FoodID="2"><input id="..." type="checkbox" name="..." checked="checked" /></span>

Here are some other ideas I considered:

  1. Use an AJAX save method which is called for each checkbox click.
  2. Hide the row id in a hidden label and get to it via the CheckBox's NamingContainer.

What do you think?

10 Comments

  • I guess it depends on when you need to know that a check occurred. If you can wait 'til a full postback occurs it's easy to loop through the Items collection, get each data item and the checkbox value at which point you can assign easily without having to explicitly adding the FOODID attribute. This is sort of a full unbinding where every item is touched and updated.

    But for individual updates via AJAX your trick of adding an attribute is a great idea to let the client get the food context...

  • When I want to place additional information inside a repeater, I usually put a HIDDEN field with those information (e.g. an ID) inside the ItemTemplate for a RepeaterItem.

  • Great write up!

    I love ASP.NET because of tricks like this that the developer can use.

  • hi,
    anybody can help me?
    when i check the one check box i want to choose all the templte check box items?

  • how to store checkbox selected value in sqlserver?

  • im unable to understand it .. :(

  • your article helped me a lot.

  • Two years later... this blog still helps ppl... nicely done

  • I've been using the folowing solution. Just another hack using the validationgroup field where I bound the data ID
    ValidationGroup=''

    and on the OnCheckedChanged event something like this....

    CheckBox mycheckbox = (CheckBox)sender;
    string myID = mycheckbox.ValidationGroup;



  • this article is amazing! I originally used an asp.net hiddenfield to do this but had issues with other events within and outside the repeater being replaced with the checkbox changed event on postback, which was frustrating!! this is the answer!

Comments have been disabled for this content.