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.
1: <ToolboxData("<{0}:MultiSelectGridView runat=server></{0}:MultiSelectGridView>")> _
2: Public Class MultiSelectGridView
3: Inherits GridView
4:
5: Private _tfMultiSelect As TemplateField = Nothing
6:
7: #Region "Public Properties"
8: <DefaultValue(False), Category("MultiSelect"), Description("Indicates whether or not multiselection is enabled.")> _
9: Public Property EnableMultiSelect() As Boolean
10: Get
11: If Not ViewState("EnableMultiSelect") Is Nothing Then
12: Return DirectCast(ViewState("EnableMultiSelect"), Boolean)
13: Else
14: Return False
15: End If
16: End Get
17: Set(ByVal value As Boolean)
18: ViewState("EnableMultiSelect") = value
19: End Set
20: End Property
21:
22: <Bindable(True), Category("MultiSelect"), TypeConverter(GetType(WebColorConverter)), Description("Specifies the background color of a selected row.")> _
23: Public Property MultiSelectColor() As Color
24: Get
25: If ViewState("MultiSelectColor") Is Nothing Then
26: ViewState("MultiSelectColor") = System.Drawing.ColorTranslator.FromHtml("#FFCC99")
27: End If
28: Return DirectCast(ViewState("MultiSelectColor"), Color)
29: End Get
30:
31: Set(ByVal value As Color)
32: ViewState("MultiSelectColor") = value
33: End Set
34: End Property
35:
36: <Bindable(True), Category("MultiSelect"), Description("Specifies the where the multiselection column should be placed.")> _
37: Public Property MultiSelectColumnIndex() As Integer
38: Get
39: If Not ViewState("MultiSelectColumnIndex") Is Nothing Then
40: Return DirectCast(ViewState("MultiSelectColumnIndex"), Integer)
41: Else
42: Return -1
43: End If
44: End Get
45: Set(ByVal value As Integer)
46: ViewState("MultiSelectColumnIndex") = value
47: End Set
48: End Property
49:
50: Public ReadOnly Property SelectedDataKeys() As DataKeyArray
51: Get
52: Return GetSelectedDataKeys()
53: End Get
54: End Property
55: #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.
1: Protected Overrides Function CreateColumns(ByVal dataSource As System.Web.UI.WebControls.PagedDataSource, ByVal useDataSource As Boolean) As System.Collections.ICollection
2: 'let the GridView create the default set of columns
3: Dim columnList As ICollection = MyBase.CreateColumns(dataSource, useDataSource)
4: Dim extendedColumnList As ArrayList = New ArrayList(columnList)
5:
6: If Me.EnableMultiSelect Then
7: 'add our multi-select checkbox column
8: _tfMultiSelect = New TemplateField()
9: With _tfMultiSelect
10: .HeaderTemplate = New MultiSelectColumnTemplate(DataControlRowType.Header)
11: .ItemTemplate = New MultiSelectColumnTemplate(DataControlRowType.DataRow)
12: End With
13:
14: If Me.MultiSelectColumnIndex < 0 Or Me.MultiSelectColumnIndex > extendedColumnList.Count Then
15: extendedColumnList.Add(_tfMultiSelect)
16: Else
17: extendedColumnList.Insert(Me.MultiSelectColumnIndex, _tfMultiSelect)
18: End If
19: End If
20:
21: Return extendedColumnList
22: 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.
1: Private Sub MultiSelectGridView_PreRender(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.PreRender
2: If Me.EnableMultiSelect Then
3: 'declare variables used to initialize onclick client scripts
4: Dim cbxMultiSelect As CheckBox = Nothing
5: Dim cbxSingleSelect As CheckBox = Nothing
6: Dim hidCheckBoxIDs As HiddenField = Nothing
7: Dim checkboxIDs As ArrayList = New ArrayList()
8: Dim selectColor As String = System.Drawing.ColorTranslator.ToHtml(Me.MultiSelectColor)
9:
10: 'set header checkbox onclick
11: If Me.ShowHeader And Not Me.HeaderRow Is Nothing Then
12: cbxMultiSelect = CType(Me.HeaderRow.FindControl("cbxMultiSelect"), CheckBox)
13: hidCheckBoxIDs = CType(Me.HeaderRow.FindControl("hidCheckBoxIDs"), HiddenField)
14: cbxMultiSelect.Attributes.Add("onClick", "toggleCheckBox(this, '" & cbxMultiSelect.ClientID & "', '" & hidCheckBoxIDs.ClientID & "', '" & selectColor & "');")
15: End If
16:
17: 'set data row checkbox onclick
18: For Each gvr As GridViewRow In Me.Rows
19: cbxSingleSelect = CType(gvr.FindControl("cbxMultiSelect"), CheckBox)
20:
21: If cbxMultiSelect Is Nothing Or hidCheckBoxIDs Is Nothing Then
22: cbxSingleSelect.Attributes.Add("onClick", "toggleCheckBox(this, '', '', '" & selectColor & "');")
23: Else
24: cbxSingleSelect.Attributes.Add("onClick", "toggleCheckBox(this, '" & cbxMultiSelect.ClientID & "', '" & hidCheckBoxIDs.ClientID & "', '" & selectColor & "');")
25: End If
26:
27: If cbxSingleSelect.Checked Then
28: gvr.Style.Add("background-color", selectColor)
29: Else
30: gvr.Style.Add("background-color", "")
31: End If
32: checkboxIDs.Add(cbxSingleSelect.ClientID)
33: Next
34:
35: 'set hidden field value w/ checkbox client ids
36: If Not hidCheckBoxIDs Is Nothing Then
37: hidCheckBoxIDs.Value = Join(checkboxIDs.ToArray(), ",")
38: End If
39:
40: RegisterClientScriptBlock()
41: End If
42: 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.
1: var lastColorUsed;
2: function toggleCheckBox(thisCheckBox, multiSelectID, checkBoxIDs, selectedColor) {
3: var arrayIDs;
4: if (checkBoxIDs.length > 0)
5: arrayIDs = document.getElementById(checkBoxIDs).value.split(',');
6: var cbxMultiSelect = document.getElementById(multiSelectID);
7: if (thisCheckBox == cbxMultiSelect) {
8: var cbx;
9: for (var i = 0; i < arrayIDs.length; i++) {
10: cbx = document.getElementById(arrayIDs[i]);
11: if (cbx) {
12: if (!cbx.disabled) {
13: cbx.checked = thisCheckBox.checked;
14: if (cbx.checked) {
15: cbx.parentNode.parentNode.parentNode.style.backgroundColor = selectedColor;
16: lastColorUsed = selectedColor;
17: } else {
18: cbx.parentNode.parentNode.parentNode.style.backgroundColor = '';
19: lastColorUsed = '';
20: }
21: }
22: }
23: }
24: } else {
25: if (thisCheckBox.checked) {
26: thisCheckBox.parentNode.parentNode.parentNode.style.backgroundColor = selectedColor;
27: lastColorUsed = selectedColor;
28:
29: if (cbxMultiSelect) {
30: var allChecked = true;
31: for (var i = 0; i < arrayIDs.length; i++) {
32: allChecked = document.getElementById(arrayIDs[i]).checked;
33: if (!(allChecked))
34: break;
35: }
36: if (allChecked)
37: cbxMultiSelect.checked = true;
38: }
39: } else {
40: thisCheckBox.parentNode.parentNode.parentNode.style.backgroundColor = '';
41: lastColorUsed = '';
42: if (cbxMultiSelect)
43: cbxMultiSelect.checked = false;
44: }
45: }
46: }
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.
1: Private Function GetSelectedDataKeys() As DataKeyArray
2: Dim keys As ArrayList = New ArrayList()
3:
4: If Me.EnableMultiSelect Then
5: Dim cbxMultiSelect As CheckBox = Nothing
6:
7: If Me.DataKeys.Count > 0 Then
8: For Each gvr As GridViewRow In Me.Rows
9: cbxMultiSelect = CType(gvr.FindControl("cbxMultiSelect"), CheckBox)
10:
11: If Not cbxMultiSelect Is Nothing AndAlso cbxMultiSelect.Checked Then
12: keys.Add(Me.DataKeys(gvr.RowIndex))
13: End If
14: Next
15: End If
16: End If
17:
18: Return New DataKeyArray(keys)
19: 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.
1: <cc1:MultiSelectGridView
2: ID="MultiSelectGridView1"
3: runat="server"
4: DataSourceID="SqlDataSource1"
5: DataKeyNames="RegionID"
6: EnableMultiSelect="True"
7: MultiSelectColor="#9EC630"
8: 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.
1: Protected Sub Button1_Click(ByVal sender As Object, ByVal e As EventArgs) Handles Button1.Click
2: Dim selectedKeys As StringBuilder = New StringBuilder()
3: For Each key As DataKey In Me.MultiSelectGridView1.SelectedDataKeys
4: selectedKeys.Append(", " & key(0).ToString())
5: Next
6:
7: If selectedKeys.Length > 0 Then selectedKeys.Remove(0, 2)
8: lblSelectedItems.Text = selectedKeys.ToString()
9: 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)