MultiSelect GridView Control

September 17, 2008 09:18 by wjchristenson2

In this post, I am going to create an ASP .NET GridView that supports multiple selection.  This is a common feature that is desired by many web developers.  The new GridView control I will create will support the following: selection of 1 row at a time, toggling of all rows to be selected or deselected at once, coloring of what row(s) are selected, what column index we want our selection checkboxes to be put in, and a way to access what DataKeys are selected on PostBack.  Here is what our final control will look like.

The first step to creating our control is setting up our solution.  You can download the solution in its entirety below.  I have a project for my new control and a web project to test it in.  I am using the Northwind SQL database for my DataSource.  Now we are ready to start programming our MultiSelectGridView control.  We first setup our class-wide variables and properties.

<ToolboxData("<{0}:MultiSelectGridView runat=server></{0}:MultiSelectGridView>")> _
  Public Class MultiSelectGridView
    Inherits GridView

    Private _tfMultiSelect As TemplateField = Nothing

#Region "Public Properties"
    <DefaultValue(False), Category("MultiSelect"), Description("Indicates whether or not multiselection is enabled.")> _
    Public Property EnableMultiSelect() As Boolean
        Get
            If Not ViewState("EnableMultiSelect") Is Nothing Then
                Return DirectCast(ViewState("EnableMultiSelect"), Boolean)
            Else
                Return False
            End If
        End Get
        Set(ByVal value As Boolean)
            ViewState("EnableMultiSelect") = value
        End Set
    End Property

    <Bindable(True), Category("MultiSelect"), TypeConverter(GetType(WebColorConverter)), Description("Specifies the background color of a selected row.")> _
    Public Property MultiSelectColor() As Color
        Get
            If ViewState("MultiSelectColor") Is Nothing Then
                ViewState("MultiSelectColor") = System.Drawing.ColorTranslator.FromHtml("#FFCC99")
            End If
            Return DirectCast(ViewState("MultiSelectColor"), Color)
        End Get

        Set(ByVal value As Color)
            ViewState("MultiSelectColor") = value
        End Set
    End Property

    <Bindable(True), Category("MultiSelect"), Description("Specifies the where the multiselection column should be placed.")> _
    Public Property MultiSelectColumnIndex() As Integer
        Get
            If Not ViewState("MultiSelectColumnIndex") Is Nothing Then
                Return DirectCast(ViewState("MultiSelectColumnIndex"), Integer)
            Else
                Return -1
            End If
        End Get
        Set(ByVal value As Integer)
            ViewState("MultiSelectColumnIndex") = value
        End Set
    End Property

    Public ReadOnly Property SelectedDataKeys() As DataKeyArray
        Get
            Return GetSelectedDataKeys()
        End Get
    End Property
#End Region

We want to extend the GridView control so we first inherit from it.  I've defined 4 properties.  First is to toggle whether or not we want to enable multiselection or not.  The second is used to define what color we want to use to delineate the selected state of a row.  The third property is used to allow the developer to specify where they wish to put the multiple selection column.  The final property (SelectedDataKeys) is an array of selected DataKeys that can be accessed programmatically when a PostBack occurs.

A problem I had at first, was how to add columns in a GridView control.  At first, I tried to add them in the Init and the CreateChildControls and then work with them later on in the control lifecycle.  This *can* work, however you'll soon find out that if you try and access the Columns property, you may find that there are no other columns outside the one you added if you set AutoGenerateColumns = True.  For instance, if you are trying to do a GridView.Columns.Count, you may have more columns than what the property returns.  The Columns property only returns what columns are defined.  So if you are trying to programmatically insert columns or move them around, you may run into issues if you are trying to do that in the Init or PreRender or Load events of your control lifecycle.  The key to adding our column is to Override the CreateColumns function.  We want to let the GridView create its normal columns and then we want to insert our multiselect column wherever the column index tell us to.

    Protected Overrides Function CreateColumns(ByVal dataSource As System.Web.UI.WebControls.PagedDataSource, ByVal useDataSource As Boolean) As System.Collections.ICollection
        'let the GridView create the default set of columns
        Dim columnList As ICollection = MyBase.CreateColumns(dataSource, useDataSource)
        Dim extendedColumnList As ArrayList = New ArrayList(columnList)

        If Me.EnableMultiSelect Then
            'add our multi-select checkbox column
            _tfMultiSelect = New TemplateField()
            With _tfMultiSelect
                .HeaderTemplate = New MultiSelectColumnTemplate(DataControlRowType.Header)
                .ItemTemplate = New MultiSelectColumnTemplate(DataControlRowType.DataRow)
            End With

            If Me.MultiSelectColumnIndex < 0 Or Me.MultiSelectColumnIndex > extendedColumnList.Count Then
                extendedColumnList.Add(_tfMultiSelect)
            Else
                extendedColumnList.Insert(Me.MultiSelectColumnIndex, _tfMultiSelect)
            End If
        End If

        Return extendedColumnList
    End Function

Above is the overridden CreateColumns function.  We first see if multiselection is enabled.  If it is, we insert our multiselect column (template field) into the collection of GridView columns.  We do some logic here to ensure the column is inserted in the appropriate position.  This gets our multiselect column into the GridView, however now we need to wire in some Javascript.

    Private Sub MultiSelectGridView_PreRender(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.PreRender
        If Me.EnableMultiSelect Then
            'declare variables used to initialize onclick client scripts
            Dim cbxMultiSelect As CheckBox = Nothing
            Dim cbxSingleSelect As CheckBox = Nothing
            Dim hidCheckBoxIDs As HiddenField = Nothing
            Dim checkboxIDs As ArrayList = New ArrayList()
            Dim selectColor As String = System.Drawing.ColorTranslator.ToHtml(Me.MultiSelectColor)

            'set header checkbox onclick
            : If Me.ShowHeader And Not Me.HeaderRow Is Nothing Then
                cbxMultiSelect = CType(Me.HeaderRow.FindControl("cbxMultiSelect"), CheckBox)
                hidCheckBoxIDs = CType(Me.HeaderRow.FindControl("hidCheckBoxIDs"), HiddenField)
                cbxMultiSelect.Attributes.Add("onClick", "toggleCheckBox(this, '" & cbxMultiSelect.ClientID & "', '" & hidCheckBoxIDs.ClientID & "', '" & selectColor & "');")
            End If

            'set data row checkbox onclick
            For Each gvr As GridViewRow In Me.Rows
                cbxSingleSelect = CType(gvr.FindControl("cbxMultiSelect"), CheckBox)

                If cbxMultiSelect Is Nothing Or hidCheckBoxIDs Is Nothing Then
                    cbxSingleSelect.Attributes.Add("onClick", "toggleCheckBox(this, '', '', '" & selectColor & "');")
                Else
                    cbxSingleSelect.Attributes.Add("onClick", "toggleCheckBox(this, '" & cbxMultiSelect.ClientID & "', '" & hidCheckBoxIDs.ClientID & "', '" & selectColor & "');")
                End If

                If cbxSingleSelect.Checked Then
                    gvr.Style.Add("background-color", selectColor)
                Else
                    gvr.Style.Add("background-color", "")
                End If
                checkboxIDs.Add(cbxSingleSelect.ClientID)
            Next

            'set hidden field value w/ checkbox client ids
            If Not hidCheckBoxIDs Is Nothing Then
                hidCheckBoxIDs.Value = Join(checkboxIDs.ToArray(), ",")
            End If

            RegisterClientScriptBlock()
        End If
    End Sub

I wire in client-side script in the control's PreRender phase of its lifecycle.  Basically when a checkbox is clicked, I call a Javascipt function to do perform background color changes and toggle the check or uncheck of other checkboxes.  So if I check the toggle all checkbox, all rows will be selected.  If I uncheck the toggle all checkbox, all rows will be deselected.  If I select each row manually 1 at a time and all are checked, I check the "toggle all" checkbox with Javascript and vice versa.  Here's the Javascript code that performs this logic.

  var lastColorUsed;
  function toggleCheckBox(thisCheckBox, multiSelectID, checkBoxIDs, selectedColor) {
    var arrayIDs;
    if (checkBoxIDs.length > 0) 
      arrayIDs = document.getElementById(checkBoxIDs).value.split(',');
    var cbxMultiSelect = document.getElementById(multiSelectID);
    if (thisCheckBox == cbxMultiSelect) {
      var cbx;
      for (var i = 0; i < arrayIDs.length; i++) {
        cbx = document.getElementById(arrayIDs[i]);
        if (cbx) {
          if (!cbx.disabled) {
              cbx.checked = thisCheckBox.checked;
              if (cbx.checked) {
                cbx.parentNode.parentNode.parentNode.style.backgroundColor = selectedColor;
                lastColorUsed = selectedColor;
              } else {
                cbx.parentNode.parentNode.parentNode.style.backgroundColor = '';
                lastColorUsed = '';
              }
          }  
        }
      }
    } else {
      if (thisCheckBox.checked) {
        thisCheckBox.parentNode.parentNode.parentNode.style.backgroundColor = selectedColor;
        lastColorUsed = selectedColor;

        if (cbxMultiSelect) {
          var allChecked = true;
          for (var i = 0; i < arrayIDs.length; i++) {
            allChecked = document.getElementById(arrayIDs[i]).checked;
            if (!(allChecked))
              break;
          }
          if (allChecked) 
            cbxMultiSelect.checked = true;
        }
      } else {
        thisCheckBox.parentNode.parentNode.parentNode.style.backgroundColor = '';
        lastColorUsed = '';
        if (cbxMultiSelect) 
          cbxMultiSelect.checked = false;
      }
    }
  }

Now we need to acquire what rows are selected when the page performs a PostBack.  I've created a public property that returns an array of DataKeys that were selected.  The DataKeys can be 1 or a combination of values to uniquely identify the row.  Make sure you define what column(s) or object(s) of each row make up the DataKey by setting the DataKeyNames property of the GridView.  The SelectedDataKeys property calls our function below.

    Private Function GetSelectedDataKeys() As DataKeyArray
        Dim keys As ArrayList = New ArrayList()

        If Me.EnableMultiSelect Then
            Dim cbxMultiSelect As CheckBox = Nothing

            If Me.DataKeys.Count > 0 Then
                For Each gvr As GridViewRow In Me.Rows
                    cbxMultiSelect = CType(gvr.FindControl("cbxMultiSelect"), CheckBox)

                    If Not cbxMultiSelect Is Nothing AndAlso cbxMultiSelect.Checked Then
                        keys.Add(Me.DataKeys(gvr.RowIndex))
                    End If
                Next
            End If
        End If

        Return New DataKeyArray(keys)
    End Function

What we do here is loop through each row of the GridView and get a handle on the multiselect checkbox.  If it is checked, we get the associated DataKeys for the row and add it to our ArrayList.  We return all selected DataKeys when finished looping through all rows of the GridView.  Now we are ready to use our control.

<cc1:MultiSelectGridView 
  ID="MultiSelectGridView1" 
  runat="server" 
  DataSourceID="SqlDataSource1" 
  DataKeyNames="RegionID" 
  EnableMultiSelect="True" 
  MultiSelectColor="#9EC630" 
  MultiSelectColumnIndex="0" />

Here is a simple use of our MultiSelectGridView control.  Remember I am using the Northwind database via my SqlDataSource1.  What's important here is the MultiSelect properties.  I enable multiselection, set the selected color, and then place the multiselect column on the far left of my GridView.  After I've selected what rows I want, I click a button and display the SelectedDataKeys on my page.  Here is how I use the SelectedDataKeys property programmatically.

    Protected Sub Button1_Click(ByVal sender As Object, ByVal e As EventArgs) Handles Button1.Click
        Dim selectedKeys As StringBuilder = New StringBuilder()
        For Each key As DataKey In Me.MultiSelectGridView1.SelectedDataKeys
            selectedKeys.Append(", " & key(0).ToString())
        Next

        If selectedKeys.Length > 0 Then selectedKeys.Remove(0, 2)
        lblSelectedItems.Text = selectedKeys.ToString()
    End Sub

That's all there is to it.  You now have a GridView that allows you to multiselect rows!


GridViewMultiSelect_Soln.zip (111.25 kb)
Bookmark and Share