Not logged in - Login

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