Extend CheckBoxList to Bind Check Boxes
The Asp.Net CheckBoxList is a data bound control that allows you to display a list of text rows, with a checkbox next to each item. This provides a simple way to show the user a list of items and have them check all that apply. I don't use it often, but sometimes it's nice to have a simple interface like this rather than using a grid to accomplish the same thing.
The CheckBoxList allows you to bind the list source to data, just like any other control that is derived from ListControl. You can set the DataTextField and DataValueField properties to the names of database columns that contain the display, and underlying values. On the page, the user checks one or more items. Then when the page posts, you can use some code-behind to iterate through the list items and save the user's entries back to your database.
All sounds good, right? But wait, what if the user has already checked (or "selected") some items, and we want to cause the CheckBoxList to show which items are checked as it binds to the data and creates the list? How do we do that? It seems like Microsoft forgot all about that scneario and didn't provide any easy means to accomplish it. Seems pretty obvious to me that this would be an issue, but as of now, it still doesn't have that capability.
It wouldn't be so bad if the CheckBoxList had an ItemDataBound event that fires every time a ListItem is created. This would allow you to have access to the data source and set the Selected property as appropriate. But ItemDataBound doesn't exist for the CheckBoxList control, and the Databound event doesn't provide you with anything useful in the event arguments to work with.
I looked around to see what others have done with this problem. Most of the solutions involve hooking an event and going back over the data and setting the Selected property of each ListItem. In some cases, they even re-do a call to the database! Very inefficient and not a great approach. A better solution would be to extend the control and override the code that builds the list items. But in order to do that, you need to get a look at the control's source.
One of the most useful tools I have for ASP.Net control problems is the Telerik JustDecompile tool. This nifty (and free!) tool decompiles .Net assemblies back into source code, either VB or C#. I use this to look around "under the hood" of ASP.Net controls frequently. After studying the CheckBoxList code (and its base classes) I came up with this solution to the problem of binding the checkbox values.
First create a new class and inherit from CheckBoxList.
Imports System.Globalization Imports System.ComponentModel Namespace DNH Public Class CheckBoxListEx Inherits System.Web.UI.WebControls.CheckBoxList End Class End Namespace
Next, add a new property that will inform the control which data field holds the values for the checkboxes. This property mirrors DataTextField syntax.
Public Property DataCheckedField() As String Get Dim item As Object = Me.ViewState("DataCheckedField") If (item Is Nothing) Then Return String.Empty End If Return CStr(item) End Get Set(ByVal value As String) Me.ViewState("DataCheckedField") = value If (MyBase.Initialized) Then MyBase.RequiresDataBinding = True End If End Set End Property
The CheckBoxList is derived from the ListControl class. This class is the basis for several ASP.Net web controls including the DropDownList. In ListControl there is a procedure called PerformDataBinding where the list items are actually created from the data, so this seems like the best place to jump in and modify things.
Take a look at the source for PerformDataBinding.
Protected Friend Overrides Sub PerformDataBinding(ByVal dataSource As IEnumerable) MyBase.PerformDataBinding(dataSource) If (dataSource IsNot Nothing) Then Dim flag As Boolean = False Dim flag1 As Boolean = False Dim dataTextField As String = Me.DataTextField Dim dataValueField As String = Me.DataValueField Dim dataTextFormatString As String = Me.DataTextFormatString If (Not Me.AppendDataBoundItems) Then Me.Items.Clear() End If Dim collections As ICollection = TryCast(dataSource, ICollection) If (collections IsNot Nothing) Then Me.Items.Capacity = collections.Count + Me.Items.Count End If If (dataTextField.Length <> 0 OrElse dataValueField.Length <> 0) Then flag = True End If If (dataTextFormatString.Length <> 0) Then flag1 = True End If For Each obj As Object In dataSource Dim listItem As System.Web.UI.WebControls.ListItem = New System.Web.UI.WebControls.ListItem() If (Not flag) Then If (Not flag1) Then listItem.Text = obj.ToString() Else Dim currentCulture As CultureInfo = CultureInfo.CurrentCulture Dim objArray() As Object = { obj } listItem.Text = String.Format(currentCulture, dataTextFormatString, objArray) End If listItem.Value = obj.ToString() Else If (dataTextField.Length > 0) Then listItem.Text = DataBinder.GetPropertyValue(obj, dataTextField, dataTextFormatString) End If If (dataValueField.Length > 0) Then listItem.Value = DataBinder.GetPropertyValue(obj, dataValueField, Nothing) End If End If Me.Items.Add(listItem) Next End If If (Me.cachedSelectedValue Is Nothing) Then If (Me.cachedSelectedIndex <> -1) Then Me.SelectedIndex = Me.cachedSelectedIndex Me.cachedSelectedIndex = -1 End If Return End If Dim num As Integer = -1 num = Me.Items.FindByValueInternal(Me.cachedSelectedValue, True) If (-1 = num) Then Dim d() As Object = { Me.ID, "SelectedValue" } Throw New ArgumentOutOfRangeException("value", SR.GetString("ListControl_SelectionOutOfRange", d)) End If If (Me.cachedSelectedIndex <> -1 AndAlso Me.cachedSelectedIndex <> num) Then Dim objArray1() As Object = { "SelectedIndex", "SelectedValue" } Throw New ArgumentException(SR.GetString("Attributes_mutually_exclusive", objArray1)) End If Me.SelectedIndex = num Me.cachedSelectedValue = Nothing Me.cachedSelectedIndex = -1 End Sub
This procedure loops through the supplied data source, creates the ListItem objects, sets their properties and adds them to the CheckBoxList. If you copy this code into your class, you will get several lines of code flagged as errors in the second section of code starting with the line: If (Me.cachedSelectedValue Is Nothing) Then
. There are problems here because some of the referenced variables are Friend, so you can't refer to them in your code because they are in a different assembly. At first I thought this was going to be a show stopper, but turns out, it isn't.
If you notice, the first part of the code is wrapped in an If statement to make sure the dataSource variable has something in it. So the strategy is to copy that section of code to the CheckBoxListEx class, modify it, then call the base method, passing Nothing, so only the second part of the original procedure will get run, which is exactly what we need here.
Here is the overridden procedure.
If (dataSource IsNot Nothing) Then Dim dataTextField As String = Me.DataTextField Dim dataValueField As String = Me.DataValueField Dim dataTextFormatString As String = Me.DataTextFormatString Dim dataCheckedString As String = Me.DataCheckedField If (Not Me.AppendDataBoundItems) Then Me.Items.Clear() End If Dim collections As ICollection = TryCast(dataSource, ICollection) If (collections IsNot Nothing) Then Me.Items.Capacity = collections.Count + Me.Items.Count End If Dim currentCulture As CultureInfo = CultureInfo.CurrentCulture For Each obj As Object In dataSource Dim listItem As System.Web.UI.WebControls.ListItem = New System.Web.UI.WebControls.ListItem() 'If the DataTextField and DataValueField are both not set, then the text and value of the ListItem is obj.ToString() If (dataTextField.Length = 0 AndAlso dataValueField.Length = 0) Then 'If format string is supplied If (dataTextFormatString.Length <> 0) Then Dim objArray() As Object = {obj} listItem.Text = String.Format(currentCulture, dataTextFormatString, objArray) Else listItem.Text = obj.ToString() End If listItem.Value = obj.ToString() Else If (dataTextField.Length > 0) Then listItem.Text = DataBinder.GetPropertyValue(obj, dataTextField, dataTextFormatString) End If If (dataValueField.Length > 0) Then listItem.Value = DataBinder.GetPropertyValue(obj, dataValueField, Nothing) End If End If 'Set check box of selected items Dim bSelected As Boolean If dataCheckedString.Length > 0 AndAlso Boolean.TryParse(DataBinder.GetPropertyValue(obj, dataCheckedString, Nothing), bSelected) Then listItem.Selected = bSelected End If Me.Items.Add(listItem) Next End If 'This will pick up the last part of the original method having to do with caching variables MyBase.PerformDataBinding(Nothing)
This procedure does the same as the original code, but in addition, checks the data to see whether to set the ListItem.Selected property, which means that the checkbox will be checked for that item when the control renders. You may also notice I made some other modifications. While looking at the original procedure I couldn't help gagging at those two "flag" variables. Not just because of their names either. Why declare a variable that holds an expression value, then only use it one time in the code? Amateurish. In my updated procedure, I got rid of the "flags" and put the expressions directly in the If statements where they belong. Less code, fewer variables, no loss of clarity. Also moved the declaration of the currentCulture variable out of the loop.
After you create this class in your project, you need to do a build so that Visual Studio has knowledge of it. Then, to use the extended control, add a directive at the top of the page to establish your namespace and prefix:
<%@ Register Namespace="DNH" TagPrefix="DNH" %>
and then use the extended control exactly as you would the CheckBoxList, adding the new DataCheckedField property.
<DNH:CheckBoxListEx ID="WorkItemsCheckBoxList" runat="server" DataSourceID="WorkItemsSqlDataSource" DataTextField="WorkItemName" DataValueField="WorkItemID" DataCheckedField="Enabled"> </DNH:CheckBoxListEx>
Now you can bind your list to a data source that has the text value, the underlying data value, AND a boolean value for the checkbox. You will still need to loop through the ListItem collection to update the database.****