可以排序的DataGridView数据源SortableBindingList<T>

本文介绍了一种使DataGridView支持排序的方法,通过创建SortableBindingList&lt;T&gt;类,该类继承自BindingList&lt;T&gt;并增加了排序功能,解决了DataGridView默认不支持排序的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一般情况下,DataGridView绑定的数据源都是BindingList<T>,但是BindingList<T>不支持排序,所以造成了DataGridView也是不能排序的,改进的SortableBindingList<T>增加了排序功能。代码如下

    public class SortableBindingList<T> : BindingList<T>
    {
        private readonly Dictionary<Type, PropertyComparer<T>> comparers;
        private bool isSorted;
        private ListSortDirection listSortDirection;
        private PropertyDescriptor propertyDescriptor;

        public SortableBindingList()
            : base(new List<T>())
        {
            this.comparers = new Dictionary<Type, PropertyComparer<T>>();
        }

        public SortableBindingList(IEnumerable<T> enumeration)
            : base(new List<T>(enumeration))
        {
            this.comparers = new Dictionary<Type, PropertyComparer<T>>();
        }

        protected override bool SupportsSortingCore
        {
            get { return true; }
        }

        protected override bool IsSortedCore
        {
            get { return this.isSorted; }
        }

        protected override PropertyDescriptor SortPropertyCore
        {
            get { return this.propertyDescriptor; }
        }

        protected override ListSortDirection SortDirectionCore
        {
            get { return this.listSortDirection; }
        }

        protected override bool SupportsSearchingCore
        {
            get { return true; }
        }

        protected override void ApplySortCore(PropertyDescriptor property, ListSortDirection direction)
        {
            List<T> itemsList = (List<T>)this.Items;

            Type propertyType = property.PropertyType;
            PropertyComparer<T> comparer;
            if (!this.comparers.TryGetValue(propertyType, out comparer))
            {
                comparer = new PropertyComparer<T>(property, direction);
                this.comparers.Add(propertyType, comparer);
            }

            comparer.SetPropertyAndDirection(property, direction);
            itemsList.Sort(comparer);

            this.propertyDescriptor = property;
            this.listSortDirection = direction;
            this.isSorted = true;

            this.OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
        }

        protected override void RemoveSortCore()
        {
            this.isSorted = false;
            this.propertyDescriptor = base.SortPropertyCore;
            this.listSortDirection = base.SortDirectionCore;

            this.OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
        }

        protected override int FindCore(PropertyDescriptor property, object key)
        {
            int count = this.Count;
            for (int i = 0; i < count; ++i)
            {
                T element = this[i];
                if (property.GetValue(element).Equals(key))
                {
                    return i;
                }
            }

            return -1;
        }
    }

    public class PropertyComparer<T> : IComparer<T>
    {
        private readonly IComparer comparer;
        private PropertyDescriptor propertyDescriptor;
        private int reverse;

        public PropertyComparer(PropertyDescriptor property, ListSortDirection direction)
        {
            this.propertyDescriptor = property;
            Type comparerForPropertyType = typeof(Comparer<>).MakeGenericType(property.PropertyType);
            this.comparer = (IComparer)comparerForPropertyType.InvokeMember("Default", BindingFlags.Static | BindingFlags.GetProperty | BindingFlags.Public, null, null, null);
            this.SetListSortDirection(direction);
        }

        #region IComparer<T> Members

        public int Compare(T x, T y)
        {
            return this.reverse * this.comparer.Compare(this.propertyDescriptor.GetValue(x), this.propertyDescriptor.GetValue(y));
        }

        #endregion

        private void SetPropertyDescriptor(PropertyDescriptor descriptor)
        {
            this.propertyDescriptor = descriptor;
        }

        private void SetListSortDirection(ListSortDirection direction)
        {
            this.reverse = direction == ListSortDirection.Ascending ? 1 : -1;
        }

        public void SetPropertyAndDirection(PropertyDescriptor descriptor, ListSortDirection direction)
        {
            this.SetPropertyDescriptor(descriptor);
            this.SetListSortDirection(direction);
        }
    }

 

<think>我们正在处理一个List<string>,并希望将其绑定到DataGridView以实时显示变化。根据之前的讨论,直接使用List<string>绑定到DataGridView不会自动刷新。因此,我们需要使用BindingList<string>。但是请注意:BindingList<string>本身已经是一个可绑定的列表,它支持增删操作的通知,但是元素是string(值类型)所以不需要实现INotifyPropertyChanged(因为string是不可变的,更改元素相当于替换整个元素,而替换元素在BindingList中是通过ListChanged事件来通知的,但替换操作在BindingList中不会自动触发事件,除非我们使用其他方法)。然而,对于List<string>,我们通常不会更改单个元素(即不会修改字符串的某一部分,因为字符串不可变),而是替换整个元素。在BindingList中,替换元素(通过索引器设置)会触发ListChanged事件,事件类型为ItemChanged,这样DataGridView就会更新。因此,对于List<string>,我们可以使用BindingList<string>来绑定,这样当添加、删除元素或者替换某个位置的元素时,DataGridView会自动更新。但是,请注意:如果我们在其他地方持有对同一个字符串的引用,并改变该引用,这不会反映到列表中的元素。因为列表中的元素是值类型(字符串虽然是引用类型,但具有值语义,即当我们修改一个字符串变量时,实际上是创建了一个新的字符串对象)。所以,我们只能通过列表的索引器来替换元素,而不能通过修改变量的值来更新。因此,解决方案如下:1.使用BindingList<string>作为数据源。2.当添加、删除元素时,使用BindingList的方法,这样会自动通知。3.当修改某个元素时,使用索引器直接替换,例如:bindingList[index]=newValue;这也会触发通知。此外,如果我们需要在修改元素后希望DataGridView立即更新,那么BindingList<string>已经可以满足需求。步骤:a.创建BindingList<string>实例。b.将DataGridView的DataSource设置为这个BindingList。c.当对列表进行增删改操作时,使用BindingList的方法(Add,Remove,RemoveAt等)或者通过索引器赋值(对于修改)。注意:DataGridView默认会为每个元素显示在一列中,并且列标题是自动生成的(默认是列索引)。如果我们希望自定义列标题,可以手动添加列并绑定。但是,如果我们想显示一个简单的列表(只有一列),我们可以这样:DataGridView设置为只显示一列,并且将自动生成列设置为true,它会自动生成一列,列标题可能是默认的,我们可以设置列标题。或者,我们可以先添加列并设置DataPropertyName。这里我们使用自动生成列,然后设置列标题。示例代码:初始化:BindingList<string>stringList=newBindingList<string>();dataGridView1.AutoGenerateColumns=true;//默认就是true,但如果我们手动添加列,可能会设置为falsedataGridView1.DataSource=stringList;添加数据:stringList.Add("Hello");stringList.Add("World");修改数据:stringList[0]="Hi";//这样会触发通知,DataGridView更新删除数据:stringList.RemoveAt(1);//删除第二项但是,自动生成的列名可能不是我们想要的。在自动生成列的情况下,列标题默认为"Data"(因为BindingList<string>的每个元素就是一个字符串,所以生成的列会显示为数据,但列标题可能是空或者类型名称?)实际测试:自动生成的列标题是空字符串,因为列绑定的是整个字符串,而列名默认为空。为了更好的控制,我们可以手动添加列:dataGridView1.AutoGenerateColumns=false;DataGridViewTextBoxColumncol=newDataGridViewTextBoxColumn();col.DataPropertyName="Value";//这里注意,没有属性名,因为字符串本身没有属性。实际上,在BindingList<string>中,每个元素就是一个字符串,那么DataGridView会将其视为一个对象,它只有一个属性就是它自己。因此,我们可以将列的DataPropertyName设置为空字符串,或者使用一些技巧。但是,标准做法是:我们不需要设置DataPropertyName,因为字符串本身只有一个值。实际上,DataGridViewTextBoxColumn的默认行为就是显示整个对象。然而,在绑定到字符串列表时,我们可以设置DataPropertyName为null(或者不设置)?但这样可能不会显示任何内容。其实,在自动生成列的情况下,DataGridView会为每个元素生成一个列,并将该列的数据属性设置为绑定对象的默认属性(如果有的话)。但是,string没有默认属性(在C#中,没有类似VB的DefaultMemberAttribute),所以它可能会调用ToString(),但是默认情况下,它会生成一列,这一列会显示字符串内容。因此,我们不必担心,自动生成的列会正确显示字符串。但为了设置列标题,我们可以在生成列后设置列标题,或者在CellFormatting事件中设置。更简单的方法是在绑定后设置列的标题:在绑定后,如果自动生成了列,我们可以通过:if(dataGridView1.Columns.Count>0)dataGridView1.Columns[0].HeaderText="字符串列表";另一种方法:如果我们想要自己控制,可以创建一个包含一个属性的类,然后绑定BindingList<这个类>。这样我们就可以明确地绑定到属性上。但是这样对于简单的字符串列表显得有点重。因此,我们决定采用自动生成列,然后设置列标题的方法。但是,如果我们使用手动添加列,并且想绑定到字符串上,我们可以这样做:DataGridViewTextBoxColumncol=newDataGridViewTextBoxColumn();col.HeaderText="字符串内容";col.DataPropertyName="String";//这里不设置?因为字符串没有属性,但是我们可以利用一个事实:DataGridView会将整个对象作为数据源,然后使用单元格的Value属性来显示。实际上,当我们给DataGridView绑定一个列表时,每一行绑定到一个对象(这里是string),那么单元格显示的是这个对象的ToString()的结果。所以,我们可以设置列的数据属性为String.Empty,因为实际上不需要绑定到特定的属性(因为字符串没有子属性)。但是,我们可以通过以下方式:col.ValueType=typeof(string);//这样,在单元格中显示时,它会直接使用行对象(string)作为值。但是,这并不需要显式绑定,因为默认单元格就会使用整个对象作为值。所以,我们不需要设置DataPropertyName。所以手动添加列时,我们可以不设置DataPropertyName,或者设置为空字符串。示例手动添加列:dataGridView1.AutoGenerateColumns=false;DataGridViewColumncolumn=newDataGridViewTextBoxColumn();column.HeaderText="字符串";//不设置DataPropertyName,那么默认单元格的值就是整个对象(该行的数据对象),即字符串。dataGridView1.Columns.Add(column);然后绑定数据源dataGridView1.DataSource=stringList;这样,每个单元格就会显示字符串。但是,为了确保,我们也可以这样设置:将DataPropertyName设置为空字符串,但测试发现这样并不会显示内容。所以正确的做法是:我们不设置DataPropertyName,因为这样单元格的值就会使用整个对象(即该行绑定的对象)。或者我们可以使用以下方法:dataGridView1.Columns.Add("columnName","HeaderText");然后绑定数据源,这样会自动绑定(但这是另一种添加列的方式)。或者,我们可以通过事件来设置单元格的值(但这样复杂)。因此,为了简单,我们使用自动生成列,然后修改列标题。或者,我们可以在数据绑定后处理DataBindingComplete事件,设置列标题。以下提供一个完整的示例:窗体上有一个DataGridViewdataGridView1),一个按钮(btnAdd)用于添加数据,一个按钮(btnUpdate)用于修改选中的数据,一个按钮(btnDelete)用于删除选中的数据,一个文本框(txtInput)用于输入新数据或修改数据。代码:privateBindingList<string>stringList=newBindingList<string>();privatevoidForm1_Load(objectsender,EventArgse){//绑定数据源dataGridView1.DataSource=stringList;//初始化列(在自动生成列之后设置列标题)//注意:在绑定数据源后,列才会生成,所以在Load事件中,数据绑定还未发生,所以列还没有生成。//因此,我们可以在DataBindingComplete事件中设置列标题。dataGridView1.DataBindingComplete+=(s,ev)=>{if(dataGridView1.Columns.Count>0)dataGridView1.Columns[0].HeaderText="字符串列表";};}privatevoidbtnAdd_Click(objectsender,EventArgse){if(!string.IsNullOrEmpty(txtInput.Text)){stringList.Add(txtInput.Text);txtInput.Clear();}}privatevoidbtnUpdate_Click(objectsender,EventArgse){if(dataGridView1.SelectedCells.Count>0){intindex=dataGridView1.SelectedCells[0].RowIndex;stringList[index]=txtInput.Text;//修改选中的行}}privatevoidbtnDelete_Click(objectsender,EventArgse){if(dataGridView1.SelectedCells.Count>0){intindex=dataGridView1.SelectedCells[0].RowIndex;if(index>=0&&index< stringList.Count)stringList.RemoveAt(index);}}注意:如果DataGridView允许选择整行,那么我们可以使用SelectedRows来获取选中的行。另外,修改操作:注意字符串是不可变的,所以直接通过索引器修改,会触发BindingList的ListChanged事件,类型为ItemChanged,从而刷新DataGridView。但是,我们必须注意:直接替换元素(通过索引器赋值)会在BindingList中触发通知,但是如果我们使用相同的字符串值(比如原字符串是"abc",我们又赋值为"abc")也会触发通知,尽管在界面上看起来没有变化。另外,如果列表很大,频繁更新可能会有性能问题,但这在普通应用中可以接受。因此,对于List<string>,使用BindingList<string>即可实现实时显示。注意:如果我们在列表外部修改了某个字符串(例如,我们有一个变量指向列表中的某个字符串,然后我们修改了这个变量),由于字符串的不可变性,实际上该变量指向了新的字符串,但列表中的引用并没有改变。所以,只有通过列表的索引器去修改元素,才能触发更新。最后,如果我们需要在窗体初始化时添加一些数据,可以在绑定前添加:stringList=newBindingList<string>(newList<string>{"one","two","three"});dataGridView1.DataSource=stringList;这样,初始数据也会显示。因此,总结:-使用BindingList<string>而不是List<string>-通过Add/Remove/索引器赋值来修改列表,以实现实时刷新。这样就能满足需求。注意:如果我们要绑定多个列(即每个字符串元素要拆分成多个部分显示),那么我们就需要创建一个类,并将每个字符串解析成该类的属性。但这是另一个问题。现在,我们就按照用户的要求,给出一个简单的显示List<string>的示例。因此,我们写一个示例代码。</think> 为了在 `DataGridView` 中实时显示 `List<string>` 的变化值,需要使用支持变更通知的绑定集合。以下是完整实现方案和代码示例: ### 解决方案(使用 `BindingList<string>`) 1. **核心机制**: - `BindingList<T>` 提供集合级别的变更通知(增删操作)[^1] - 字符串元素替换时会触发值变更通知(无需实现 `INotifyPropertyChanged`) 2. **实现步骤**: ```csharp // 1. 声明BindingList作为数据源 private BindingList<string> stringList = new BindingList<string>(); // 2. 绑定DataGridView(在窗体构造函数中) public Form1() { InitializeComponent(); // 配置DataGridView dataGridView1.AutoGenerateColumns = false; // 手动添加列(设置HeaderText) DataGridViewTextBoxColumn col = new DataGridViewTextBoxColumn(); col.DataPropertyName = "Value"; // 固定值 col.HeaderText = "字符串内容"; dataGridView1.Columns.Add(col); // 绑定数据源 dataGridView1.DataSource = stringList; } // 3. 添加元素 private void btnAdd_Click(object sender, EventArgs e) { stringList.Add(txtInput.Text); // 自动刷新DataGridView } // 4. 修改元素(替换字符串) private void btnUpdate_Click(object sender, EventArgs e) { if (dataGridView1.CurrentRow?.Index >= 0) { int index = dataGridView1.CurrentRow.Index; stringList[index] = txtInput.Text; // 自动刷新显示 } } // 5. 删除元素 private void btnDelete_Click(object sender, EventArgs e) { if (dataGridView1.CurrentRow?.Index >= 0) { stringList.RemoveAt(dataGridView1.CurrentRow.Index); } } ``` ### 关键说明 1. **为什么用 `BindingList` 而不是 `List`**: - 普通 `List<string>` 无变更通知机制,`DataGridView` 无法自动刷新 - `BindingList` 在以下操作时自动触发界面更新: - 添加元素 ➔ 新增行 - 删除元素 ➔ 删除行 - 替换元素 ➔ 更新单元格值 2. **数据列配置技巧**: ```csharp DataGridViewTextBoxColumn col = new DataGridViewTextBoxColumn(); col.DataPropertyName = "Value"; // 绑定到字符串值 col.HeaderText = "自定义列标题"; ``` 3. **实时数量显示**: ```csharp // 在标签显示实时数量 lblCount.Text = $"总数: {stringList.Count}"; ``` ### 操作效果对比 | **操作类型** | **普通 List<string>** | **BindingList<string>** | |--------------|-----------------------|-------------------------| | 添加元素 | ❌ 不刷新 | ✅ 自动显示新行 | | 删除元素 | ❌ 不刷新 | ✅ 自动删除行 | | 修改元素值 | ❌ 不刷新 | ✅ 自动更新单元格 | > 注意:字符串在.NET中是**不可变对象**,必须通过替换整个元素(而非修改字符串内容)来触发更新[^1] ### 动态生成列简化版 若不需要自定义列标题,可使用更简化的自动生成列模式: ```csharp // 简化绑定(自动生成列) dataGridView1.AutoGenerateColumns = true; // 默认即为true dataGridView1.DataSource = stringList; // 设置列标题(可选) if (dataGridView1.Columns.Count > 0) { dataGridView1.Columns[0].HeaderText = "文本内容"; } ``` ### 性能提示 - 当处理超过10,000条数据时: ```csharp // 批量操作前暂停重绘 dataGridView1.SuspendLayout(); // 执行批量操作(如加载大量数据) for (int i = 0; i < 10000; i++) stringList.Add(i.ToString()); // 恢复重绘 dataGridView1.ResumeLayout(); ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值