65.9K
CodeProject 正在变化。 阅读更多。
Home

支持多选的 GridView

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.40/5 (9投票s)

2008年9月28日

GPL3

7分钟阅读

viewsIcon

86101

downloadIcon

1843

扩展GridView,允许跨多页选择多行。

引言

在涉足 C# ASP.NET 编程两年后,我积累了无数的见解,其中许多我都一直希望能写下来并与大家分享,一来可以巩固自己,二来也能帮助他人,因为几个月后我常常会忘记最初促使我写下这些东西的初衷。但最终,要么是没有时间,要么是没有兴趣,要么是解释其背景会耗费太多精力,以至于不值得付出的努力。

偶尔,会出现一个非常棒的挑战,其解决方案完全独立且可重用,也许,值得我将解决方案分享给他人。

以 ASP.NET 2.0 中的 GridView 控件为例。我们经常使用它来绑定数据源,并以一种清晰、整洁的方式将数据呈现给最终用户,它内置了分页、排序、选择、编辑和删除功能。它是大多数现代 .NET Web 应用程序的基石。可能正是因为它被广泛使用,我们有时会希望它能做得更多一点。例如,为什么我们不能开箱即用地使用 GridView 选择多条记录呢?您可以设置 DataKeyNames 属性来包含数据项的主键字段名称,并检索单个选中行的 DataKey 对象。然而,不支持像 ListBox 那样的多行选择。

背景

许多人已经提出了解决此问题的方法,包括客户端和服务器端。 Dino Esposito 通过在每行添加一个复选框来扩展 GridView,以允许用户选择多行,并提供一个方法来检索 SelectedIndices,该方法通过遍历渲染的行并返回带有选中复选框的索引。然而,我发现我的解决方案并非完全令人满意。它只检索当前数据页面的选定索引,页面的第一个记录索引为 0,就像现有的 SelectedIndex 属性一样。我希望能够跨多页选择记录,并检索所有选定的 DataKey,而不仅仅是索引。这是我的解决方案。

使用代码

我没有闭门造车,而是选择基于 Dino 的代码,定义一个新类继承自 GridView

MultiSelectGridView

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

然后,我实现了 Dino 的两个公共属性:一个布尔值,用于指定是否向每行添加复选框;一个整数,用于指定放置它的列索引。

公共属性

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; }
}

我从 Dino 那里“窃取”的其他功能是实际添加复选框字段的代码。和 Dino 一样,我定义了一个名为 InputCheckBoxField 的类,它继承自 GridView 列中的 CheckBoxField,这段代码是他的 C# 版本拷贝。

InputCheckBoxField

   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:}

最后,是新 GridView 类中实际执行添加操作的方法。和 Dino 一样,我们重写了基类的 CreateColumns() 方法,并使用一个名为 AddCheckboxColumn() 的方法将我们的字段添加到它返回的集合中。

重写 CreateColumns

   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

   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: }

现在,我们有了一个带有复选框列的 GridView,就像 Dino 的一样。

MultiSelectGridView1

我们只需要一种方法来检索选定的 DataKey。这是棘手的部分,因为我之前已经说过,我希望能够跨多页进行选择。我不确定我提出的解决方案是最好的,但它确实实现了这个目标。

我首先定义了两个私有属性,我将使用它们来将选定的 DataKey 持久化到 viewstate 中。

private DataKey collection

   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: }

没什么特别的。SelectedDataKeysArrayList 存储我们的 DataKey,而 SelectedDataKeysViewstate 是一个 DataKeyArray,它包装了这个集合,我将用它来进行实际的持久化。我们希望在 viewstate 中存储 DataKey 的集合,而 DataKeyArrayDataKey 对象的强类型集合,它实现了 IStateManager 接口。为什么要重写代码呢?!

现在,我们需要逻辑来将用户选择的行添加到我们选定的 DataKey 集合中。处理此功能的函数名为 CalculateSelectedDataKeys()

CalculateSelectedDataKeys

   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: }

此函数首先通过调用 EnsureChildControls() 来确保它能找到行的集合。然后,它遍历行,使用下面的私有函数 IsRowSelected() 检查每行是否被选中,将任何选中行的 DataKey 添加到集合中(如果尚未添加等效的 DataKey),并删除与未选中行等效的任何 DataKey。我使用另一个名为 CompareDataKeys() 的函数来检查集合中的匹配项,因为我找不到更有效的方法。

IsRowSelected

   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

   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: }

还有一个名为 SelectedDataKeys 的公共属性,以便该功能实际上可用。该属性使用一个私有的布尔字段来跟踪 CalculateSelectedDataKeys() 是否已被调用,如果尚未调用,则调用它将选定的行添加到集合中,然后返回我们的 SelectedDataKeysViewstate DataKeyArray

SelectedDataKeys

   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: }

我们现在差不多完成了。我们的下一个挑战是使用 viewstate 将 SelectedDataKeys 集合跨 postback 持久化。保存到 viewstate 很简单。利用 DataKeyArray 实现 IStateManager 的事实,我们可以利用此功能,重写我们控件中的 SaveViewState 函数,将 SelectedDataKeys 的一个可用于 viewstate 的表示添加到 StateBag 中。

重写 SaveViewState

   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: }

以及恢复它

重写 LoadViewState

   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: }

最后,我们重写 OnDataBound()OnDataBinding() 方法,在数据绑定时检查任何先前被选中的行,并在新行取代它们之前存储任何选中行的键。

重写 OnDataBound

   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: }

当新行被数据绑定后,我们遍历所有这些行,然后在 SelectedDataKeysArrayList 中查找匹配的 DataKey。然后,我们使用另一个私有函数 SetRowSelected() 来将行上的复选框设置为正确的值。

SetRowSelected

   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: }

最后,我们只需要确保在调用 DataBind 之前计算所有选定的行,以便在行消失之前存储它们。为此,我们重写了 OnDataBinding()。但还有其他事情要做。基类 GridView 的内部函数 ClearDataKeys()PageSort 事件发生时,在 OnDataBinding 之前被调用,该函数会清除内部的 _dataKeysArrayList。我们可以通过在更早的阶段,仅仅引用内置的 DataKeys 属性来绕过这个问题,将此集合的内容拉入其中。因此,我有些笨拙的 OnLoad() 重写。

重写 OnDataBinding 和重写 OnLoad

   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: }

好了,我们完成了。现在我们有了一个 GridView,它允许我们选择多行,并且可以在分页或排序数据时保留选择。

MultiSelectGridView2

我想,这就是我的第一篇编程文章的全部内容了。希望它有所帮助,欢迎大家在此基础上进行扩展!

关注点

您可能希望恢复集合的操作比实际看起来更简单。当时我被难住了,当尝试以下操作时,我收到了一个 Index Out Of Range 异常。

   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: }

通过 .NET Reflector 快速查看 DataKeyArray 类的内部实现,让我明白了需要做什么。DataKeyArrayIStateManager.LoadViewState 的实现如下:

   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 是内部的 ArrayList 集合,包含 DataKey 对象。在我们的例子中,它等于 SelectedDataKeysArrayList,而由于我当时尚未填充它,第 10 行抛出了 Out Of Range Index 异常。

您可能还想深入了解 DataKey 类本身的 IStateManager 实现。出于某种原因,如果设置了 _keyNames 属性,它将仅持久化内部字典的值,而不是 Pair 对象,并且期望 _keyNames 在加载回 DataKey 时再次被设置。这实际上不是一个问题,但它导致了我最终解决方案之前的一些更棘手的异常:预先用正确数量的空白 DataKey 和指定的 _keyNames 填充 SelectedDataKeysArrayList

© . All rights reserved.