Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

A GridView with Multiple Selection

3.40/5 (9 votes)
28 Sep 2008GPL37 min read 1   1.8K  
Expanding upon GridView to allow the selection of multiple rows across sevaral pages.

Introduction

Having spent two years dabbling with C# ASP.NET programming, I have picked up countless insights, many of which I always wish I had a chance to write down and share, for my benefit as much as for others, as I often forget a few months down the line what it is that actually got me wanting to write something down in the first place. In the end, though, I never have the time or the inclination, or it would just take too long to explain the background behind the insight to make it worth the effort.

Occasionally, a really nice challenge will crop up for which the solution is entirely self-contained and re-useable, and maybe, it is just worth sharing my solution with others.

Take the GridView control in ASP.NET 2.0. We use it all the time to bind to data sources, and represent the data to the end user in a clean and tidy way, with built-in paging, sorting, selecting, editing, and deleting. It is the corner-stone of most modern-day web applications written in .NET. It is probably because it is used so much that we sometimes wish it could do just that little bit more. Why, for instance, can we not use the GridView to select multiple records from the data out of the box? You can set the DataKeyNames property to contain the names of the primary key fields for the data items, and retrieve the DataKey object for a single selected row. Multi row selections like those of a ListBox, however, are not supported.

Background

Several people have come up with solutions to this problem, both client-side and server side. Dino Esposito expands on GridView by adding a checkbox to each row to allow the user to select multiple rows, and exposes a method to retrieve the SelectedIndices by looping through the rendered rows and returning the indexes of those with checked checkboxes. For my solution, however, I didn’t find this wholly satisfactory. It only retrieves the selected indices from the current page of data, with the first record of the page being 0, just like the existing SelectedIndex property. I wished to be able to select records over multiple pages and retrieve all of the selected DataKeys instead of just the indices. This is my solution.

Using the code

Not one to reinvent the wheel, I opted to base my code on Dino’s, defining a new class to inherit from GridView.

MultiSelectGridView

C#
namespace blive.Controls
{
    public class MultiSelectGridView : GridView
    {...
    }
}

I then implemented Dino’s two public properties, a boolean to specify whether or not to add a checkbox to each row, and an integer to specify a column index to place it in.

Public properties

C#
public bool AutoGenerateCheckboxColumn
{
    get
    {
        return (null != ViewState["AutoGenerateCheckboxColumn"]) ?
            (bool)ViewState["AutoGenerateCheckboxColumn"] : false;
    }
    set { ViewState["AutoGenerateCheckboxColumn"] = value; }
}

public int CheckboxColumnIndex
{
    get
    {
        return (null != ViewState["CheckboxColumnIndex"]) ?
            (int)ViewState["CheckboxColumnIndex"] : 0;
    }
    set { ViewState["CheckboxColumnIndex"] = (value < 0) ? 0 : value; }
}

The other functionality I shamelessly stole from Dino is the code to actually add the checkbox fields. Like Dino, I defined a class called InputCheckBoxField inheriting CheckBoxField to the GridView columns, and this is a carbon copy of his code converted to C#.

InputCheckBoxField

C#
 1: internal sealed class InputCheckBoxField : CheckBoxField
 2: {
 3:
 4:     public InputCheckBoxField()
 5:     {
 6:     }
 7:
 8:     public const string CheckBoxID = "CheckBoxButton";
 9:
10:     protected override void InitializeDataCell(
11:         DataControlFieldCell cell,
12:         DataControlRowState rowState)
13:     {
14:         base.InitializeDataCell(cell, rowState);
15:
16:         if ((cell.Controls.Count == 0))
17:         {
18:             CheckBox chk = new CheckBox();
19:             chk.ID = InputCheckBoxField.CheckBoxID;
20:             cell.Controls.Add(chk);
21:         }
22:     }
23:}

And finally, the methods within the new GridView class to actually do the adding. Like Dino, we override the base CreateColumns() method and use a method called AddCheckboxColumn() to add our field to the collection it returns.

Override CreateColumns

C#
1: protected override System.Collections.ICollection CreateColumns(
2:     PagedDataSource dataSource, bool useDataSource)
3: {
4:     ICollection ret = base.CreateColumns(dataSource, useDataSource);
5:     if (AutoGenerateCheckboxColumn)
6:         ret = AddCheckboxColumn(ret);
7:     return ret;
8: }

AddCheckboxColumn

C#
 1: protected virtual ArrayList AddCheckboxColumn(ICollection columns)
 2: {
 3:     ArrayList ret = new ArrayList(columns);
 4:     InputCheckBoxField fldCheckBox = new InputCheckBoxField();
 5:     fldCheckBox.HeaderText = "<input type="checkbox"/>";
 6:     fldCheckBox.ReadOnly = true;
 7:
 8:     if (CheckboxColumnIndex > ret.Count)
 9:         CheckboxColumnIndex = ret.Count;
10:
11:     ret.Insert(CheckboxColumnIndex, fldCheckBox);
12:
13:     return ret;
14: }

Now, we have a GridView with checkbox columns, just like Dino.

MultiSelectGridView1

We just need a way of retrieving the selected DataKeys. This is the tricky bit as I have already stated that I wished to be able to select over multiple pages of data. I am not sure that the solution I came up with is the best one, but it does achieve this goal.

I started by defining two private properties which I would use to persist the selected DataKeys in the viewstate.

private DataKey collection

C#
 1: private ArrayList _selectedDataKeysArrayList;
 2: private ArrayList SelectedDataKeysArrayList
 3: {
 4:     get
 5:     {
 6:         if (null == _selectedDataKeysArrayList)
 7:             _selectedDataKeysArrayList = new ArrayList();
 8:         return _selectedDataKeysArrayList;
 9:     }
10:     set
11:     {
12:         _selectedDataKeysArrayList = value;
13:     }
14: }
15:
16: private DataKeyArray _selectedDataKeysViewstate;
17: private DataKeyArray SelectedDataKeysViewstate
18: {
19:     get
20:     {
21:         _selectedDataKeysViewstate =
                  new DataKeyArray(SelectedDataKeysArrayList);
22:         return _selectedDataKeysViewstate;
23:     }
24: }

Nothing terribly exciting. SelectedDataKeysArrayList holds our DataKeys, and SelectedDataKeysViewstate is a DataKeyArray wrapped around this collection, which I will use to do the actually persisting. We wish to store a collection of DataKeys in the viewstate, and DataKeyArray is a strongly typed collection of DataKey objects, which implements the IStateManager interface. Why rewrite code?!

Now, we need logic to add to our collection of selected DataKeys any rows the user selects. The function to handle this is called CalculateSelectedDataKeys().

CalculateSelectedDataKeys

C#
 1: private void CalculateSelectedDataKeys()
 2: {
 3:     // Make sure the GirdView has rows so that we can look for the checkboxes
 4:     EnsureChildControls();
 5:
 6:     // Add the DataKeys of any selected rows to the SelectedDataKeysArrayList
 7:     // collection and remove those of any unselected rows
 8:     for (int i = 0; i < Rows.Count; i++)
 9:     {
10:         if (IsRowSelected(Rows[i]))
11:         {
12:             bool contains = false;
13:             foreach (DataKey k in SelectedDataKeysArrayList)
14:             {
15:                 if (CompareDataKeys(k, DataKeys[i]))
16:                 {
17:                     contains = true;
18:                     break;
19:                 }
20:             }
21:             if (!contains)
22:                 SelectedDataKeysArrayList.Add(DataKeys[i]);
23:         }
24:         else
25:         {
26:             int removeindex = -1;
27:             for (int j = 0; j < SelectedDataKeysArrayList.Count; j++)
28:             {
29:                 if (CompareDataKeys((DataKey)SelectedDataKeysArrayList[j],
                        DataKeys[i]))
30:                 {
31:                     removeindex = j;
32:                     break;
33:                 }
34:             }
35:             if (removeindex > -1)
36:                 SelectedDataKeysArrayList.RemoveAt(removeindex);
37:         }
38:     }
39: }

This function first ensures that it will find a collection of rows by calling EnsureChildControls(). It then loops through the rows, checking whether each one is selected, using the private function below called IsRowSelected(), adding the DataKeys of any checked rows if an equivalent DataKey isn’t already added, and removing any DataKeys equivalent to those of unselected rows. I use another function called CompareDataKeys() to check for matches in the collection, as I couldn’t find a more efficient way.

IsRowSelected

C#
1: private bool IsRowSelected(GridViewRow r)
2: {
3:     CheckBox cbSelected =
4:         r.FindControl(InputCheckBoxField.CheckBoxID) as CheckBox;
5:     return (null != cbSelected && cbSelected.Checked);
6: }

CompareDataKeys

C#
 1: private bool CompareDataKeys(DataKey objA, DataKey objB)
 2: {
 3:     // If the number of values in the key is different
 4:     // then we already know they are not the same
 5:     if (objA.Values.Count != objB.Values.Count)
 6:         return false;
 7:
 8:     // Continue to compare each DataKey value until we find
 9:     // one which isn’t equal, keeping a count of matches
10:     int equalityIndex = 0;
11:     while (equalityIndex < objA.Values.Count &&
12:         objA.Values[equalityIndex].Equals(objB.Values[equalityIndex]))
13:         equalityIndex++;
14:
15:     // if every value was equal, return true
16:     return equalityIndex == objA.Values.Count;
17: }

And, a public property called SelectedDataKeys so that the functionality is actually useable. The property makes use of a private boolean field to track whether CalculateSelectedDataKeys() has been called, and if it hasn’t, then it calls it to add the selected rows to the collection before returning our SelectedDataKeysViewstate DataKeyArray.

SelectedDataKeys

C#
 1: private bool _hasCalculatedSelectedDataKeys = false;
 2: public DataKeyArray SelectedDataKeys
 3: {
 4:     get
 5:     {
 6:         if (false == _hasCalculatedSelectedDataKeys)
 7:         {
 8:             CalculateSelectedDataKeys();
 9:             _hasCalculatedSelectedDataKeys = true;
10:         }
11:         return SelectedDataKeysViewstate;
12:     }
13: }

We are now almost done. Our next challenge is to persist the SelectedDataKeys collection across postbacks using the viewstate. Saving to the viewstate is easy. Using the fact that DataKeyArray implements IStateManager to our advantage, we can leverage this functionality while overriding the SaveViewState function in our control to add a viewstate-ready representation of SelectedDataKeys to the StateBag.

Override SaveViewState

C#
1: protected override object SaveViewState()
2: {
3:     object[] ret = new object[2];
4:     ret[0] = base.SaveViewState();
5:     ret[1] = ((IStateManager)SelectedDataKeys).SaveViewState();
6:     return ret;
7: }

And to restore it:

Override LoadViewState

C#
 1: protected override void LoadViewState(object savedState)
 2: {
 3:     object[] stateArray = (object[])savedState;
 4:     base.LoadViewState(stateArray[0]);
 5:
 6:     if (null != stateArray[1])
 7:     {
 8:         int capacity = ((ICollection)stateArray[1]).Count;
 9:         for (int i = 0; i < capacity; i++)
10:             SelectedDataKeysArrayList.Add(
11:                 new DataKey(new OrderedDictionary(),DataKeyNames));
12:
13:         ((IStateManager)SelectedDataKeysViewstate).LoadViewState(
14:             stateArray[1]);
15:     }
16: }

Finally, we override the OnDataBound() and OnDataBinding() methods to check any rows which have previously been selected when they were databound and store the keys of any selected rows before new ones take their place.

Override OnDataBound

C#
 1: protected override void OnDataBound(EventArgs e)
 2: {
 3:     base.OnDataBound(e);
 4:     EnsureChildControls();
 5:     for (int i = 0; i < Rows.Count; i++)
 6:     {
 7:         bool isSelected = false;
 8:         foreach (DataKey k in SelectedDataKeysArrayList)
 9:         {
10:             if (CompareDataKeys(k, DataKeys[i]))
11:             {
12:                 isSelected = true;
13:                 break;
14:             }
15:         }
16:         SetRowSelected(Rows[i], isSelected);
17:     }
18: }

When new rows have been databound, we loop through them all and then look for a matching DataKey in SelectedDataKeysArrayList. We then use another private function called SetRowSelected() to set the checkbox on the row to the correct value.

SetRowSelected

C#
1: private void SetRowSelected(GridViewRow r, bool isSelected)
2: {
3:     CheckBox cbSelected =
           r.FindControl(InputCheckBoxField.CheckBoxID) as CheckBox;
4:     if (null != cbSelected)
5:         cbSelected.Checked = isSelected;
6: }

Finally, we just need to make sure that we calculate all selected rows before a DataBind is called so that we can store them before the rows disappear. To do this, we override OnDataBinding(). There is one other thing we need to do, however. The internal ClearDataKeys() function of the base GridView is called on a Page or Sort event before OnDataBinding, and this function clears the internal _dataKeysArrayList. We can bypass this issue by pulling the contents of this collection into the built-in DataKeys property before this stage, by simply referencing it somewhere previously. Hence, my rather pathetic OnLoad() override.

Override OnDataBinding and override OnLoad

C#
 1: protected override void OnDataBinding(EventArgs e)
 2: {
 3:     CalculateSelectedDataKeys();
 4:     base.OnDataBinding(e);
 5: }
 6:
 7: protected override void OnLoad(EventArgs e)
 8: {
 9:     DataKeys.ToString();
10:     base.OnLoad(e);
11: }

And, we’re done. We now have a GridView which enables us to select multiple rows and allows us to page or sort the data while retaining the selection.

MultiSelectGridView2

And, I suppose that’s it for my first ever programming article. Hopefully, it’s of use, feel free to expand upon it!

Points of interest

You would hope that restoring the collection would be simpler than it proved. I was stumped then, when I received an Index Out Of Range exception when I attempted the following:

C#
1: protected override void LoadViewState(object savedState)
2: {
3:     object[] stateArray = (object[])savedState;
4:     base.LoadViewState(stateArray[0]);
5:     ((IStateManager)SelectedDataKeysViewstate).LoadViewState(
6:         stateArray[1]);
7: }

A quick peek into the internals of the DataKeyArray class using the .NET Reflector, however, showed me what needed to be done. The implementation of IStateManager.LoadViewState in DataKeyArray is as follows:

C#
 1: void IStateManager.LoadViewState(object state)
 2: {
 3:     if (state != null)
 4:     {
 5:         object[] objArray = (object[]) state;
 6:         for (int i = 0; i < objArray.Length; i++)
 7:         {
 8:             if (objArray[i] != null)
 9:             {
10:                 ((IStateManager) this._keys[i]).LoadViewState(
11:                     objArray[i]);
12:             }
13:         }
14:     }
15: }

_keys is the internal ArrayList of DataKeys. In our case, it is equal to SelectedDataKeysArrayList, and as I had yet to populate it then, line 10 was throwing the out of range index expression.

You may also be interested in delving further into the IStateManager implementation of the DataKey class itself. For some reason, if the _keyNames property is set, then it will only persist the values of the internal dictionary instead of Pair objects, and expects the _keyNames to be set again for the DataKey it is loaded back into. This isn’t a problem really, but it was the cause of a few more interesting exceptions before I arrived at my final solution of pre-populating SelectedDataKeysArrayList with the correct number of blank DataKeys with the _keyNames specified.

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)