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)