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.****