Monday, June 18, 2007

Checkboxes in Repeaters (ASP.Net 2.0)

Every 3 or 4 months I build a website that uses a list of check boxes to allow the users to make a selection from a list of items.

Every 3 or 4 months I fight, curse, and swear at ASP.Net and its databound controls (okay, I don't curse or swear but I do fight).

So, I've just finished doing it all again and this time I'm going to write it down so I don't forget how it's "supposed to be done".

Scenario

I have a list of thingies that I want the user to choose from. I list the thingies out in a repeating control (I use the asp:repeater but a asp:datalist behaves the same).

The user checks off the items they want and clicks a button firing off a server side onClick event.

The Problem

You loop through all the check box controls and none of them are checked. A secondary problem is that the asp:checkbox control does not have a "Value" property to store useful information about what the checkbox might be tied to.

"Wrong" Solutions

After searching the Google for help and only finding posts from 2003 I figured that either everyone gave up or this isn't really a problem for anyone but me.

One search hit suggested that I make sure I DataBind my repeater on every post back (in other words, don't put my data binding inside the !Page.IsPostback block of my Page_Load). This didn't work.

Someone suggested creating a hidden control to store a Value (since asp:checkbox doesn't have a value property), but doesn't really seem to address the issue of all the checkbox check states missing when you post back.

I found a suggestion that said making the itemcommand event trigger and then looping through the all items on the repeater would work. I think this method would allow you to get the dataitem associated with the checkbox and might preserve the check states. However, I wasn't able to get something workable using this technique. And it feels a bit wrong to use itemcommand which is intended for a single "row" or item and apply it for the entire repeater.

"Right" Solution

So the solution I found is to use the HtmlInputCheckbox control instead of the asp:Checkbox control. It really is that simple. It has a Value property and it maintains its checked value after the postback.

To retrieve the check states and values simply loop through each item in the repeater and get the html input check box using FindControl. Do this before re-binding the repeater.

Example (ASP.Net 2.0)

HTML (test.aspx)

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="test.aspx.cs" Inherits="test" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<h2>Which are your favorite Beatles?</h2>
<table cellspacing="1">
<tr >
<td>&nbsp;</td>
<td>Name</td>
</tr>
<asp:Repeater ID="repList" runat="server">
<ItemTemplate>
<tr>
<td>
<input type="checkbox" id="cbItem" runat="server" />
</td>
<td>
<asp:Literal ID="litName" runat="server" />
</td>
</tr>
</ItemTemplate>
</asp:Repeater>
</table>
<p><asp:button ID="btnVote" text="Vote" onclick="btnVote_Click" runat="server" /></p>
<p><asp:Label ID="labResult" runat="server" /></p>
</form>
</body>
</html>


Codebehind (test.aspx.cs)

using System;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using System.Collections.Generic;

public partial class test : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
this.repList.ItemDataBound += new RepeaterItemEventHandler(repList_ItemDataBound);

// Load the class data
if (!Page.IsPostBack)
{
this.BindRepeater();
}
}

protected void BindRepeater()
{
List<ListItem> items = new List<ListItem>();
items.Add(new ListItem("John", "1"));
items.Add(new ListItem("Paul", "2"));
items.Add(new ListItem("George", "3"));
items.Add(new ListItem("Ringo", "4"));

this.repList.DataSource = items;
this.repList.DataBind();
}

protected void btnVote_Click(object sender, EventArgs e)
{
string vote_results = string.Empty;

foreach (RepeaterItem ri in this.repList.Items)
{
HtmlInputCheckBox hcb = ri.FindControl("cbItem") as HtmlInputCheckBox;
if (hcb != null)
{
if (hcb.Checked)
{
if (hcb != null)
{
int id = Convert.ToInt32(hcb.Value);
vote_results += id + " ";
}
}
}
}

this.labResult.Text = "You voted for " + vote_results;
this.BindRepeater();
}

protected void repList_ItemDataBound(object sender, RepeaterItemEventArgs e)
{
ListItem item = e.Item.DataItem as ListItem;

if (item == null) return;

Literal litName = e.Item.FindControl("litName") as Literal;
HtmlInputCheckBox cbItem = e.Item.FindControl("cbItem") as HtmlInputCheckBox;

litName.Text = item.Text;
cbItem.Value = item.Value;
}
}

2 comments:

  1. Anonymous2:55 PM

    What about the checkboxlist control? I think this might do what you want without the headache.

    ReplyDelete
  2. The issue is that a checkbox list control (which I do use frequently) builds a pre-constructed table with a single column. It's just a like a datalist control except it has checkboxes.

    The datalist and checkbox list just don't cut it when I want to something a little more advanced.

    ReplyDelete