优化与 DataGridView 绑定的 DataTable 的更新

10

我的应用程序中有一个表单显示一些数据。当我第一次显示表单时,我将一些数据加载到DataTable中,然后将DataTable绑定到DataGridView。我还启动了一个异步方法来执行一些较慢的数据库查询。当这些缓慢的查询完成时,我需要更新DataTable中的几百行,填充从缓慢查询返回的值,就像这样:

foreach (DataRow row in data.Rows)
{
    SlowLoadingData slow_stuff = slow_query_results[(int)row["id"]];

    row.BeginEdit();
    row[column_one] = slow_stuff.One;
    row[column_two] = slow_stuff.Two;
    row[column_three] = slow_stuff.Three;
    row.EndEdit();
}

这个操作非常慢,会导致UI线程挂起一分钟或更长时间,可能是因为每一行都触发了重绘。

经过一些研究,我找到了一种方法使其快速。首先,将DataGridView绑定到一个绑定到DataTable的BindingSource,而不是直接绑定到DataTable。然后,在对DataTable进行更改时,请按以下方式操作:

binding_source.SuspendBinding();
binding_source.RaiseListChangedEvents = false;
// foreach (DataRow in Data.Rows) ... code above
binding_source.RaiseListChangedEvents = true;
binding_source.ResumeBinding();
grid.Refresh();

不过存在一个大问题:上面的代码阻止了DataGridView检测添加到DataTable中的新行。 表格中不会显示任何新添加的行。 如果你使用箭头键将当前单元格选择移动到网格底部之外,那么网格也可能抛出异常,因为基础数据源有更多的行,但是网格没有创建用于显示它们的网格行。

所以,我能想到两个可能的解决方案:

  1. 是否有更好的方法来禁止绑定更新同时对基础DataTable进行更改?

  2. 是否有一种简单的方法可以告诉DataGridView优雅地刷新其网格行集合以匹配基础DataTable的行数? (注意:我尝试调用BindingSource.ResetBindings,但如果您从DataTable中删除行,则似乎会触发更多异常!)


我不太了解如何使用绑定源,但在将raiselistchangedevents设置为true之前执行resumebinding是否会有任何区别? - eglasius
我已经尝试了所有可能的调用方法顺序。唯一需要让网格出错的条件是在添加新行时RaiseListChangedEvents为false。 - Will Rogers
调用名为 ResetBindings 的函数以强制更新用户界面。 - CodingBarfield
7个回答

6
你可以尝试在DataTable上使用合并方法。我会尝试创建一个简单的演示应用并在此发布,但是这个想法很简单。当你想要更新网格时,将查询结果查询到一个新的DataTable中,然后将旧表与新表合并。只要两个表都有主键(如果它们没有从数据库返回,可以在内存中创建它们),它就应该跟踪更改并无缝地更新DataGridView。它还具有不丢失用户在网格上位置的优点。
好的,这里是一个样例。我创建了一个带有两个按钮和一个dataGridView的表单。在button1点击时,我用一些数据填充主表,并将网格绑定到它。然后,在第二次点击时,我创建了另一个具有相同模式的表格。添加数据到它(一些具有相同的主键,一些是新的)。然后,它们将它们合并回原始表格。它会按预期更新网格。
    public partial class Form1 : Form
    {
        private DataTable mainTable;
        public Form1()
        {
            InitializeComponent();
            this.mainTable = this.CreateTestTable();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            for (int i = 1; i <= 10; i++)
            {
                this.mainTable.Rows.Add(String.Format("Person{0}", i), i * i);
            }

            this.dataGridView1.DataSource = this.mainTable;
        }

        private void button2_Click(object sender, EventArgs e)
        {
            DataTable newTable = this.CreateTestTable();
            for (int i = 1; i <= 15; i++)
            {
                newTable.Rows.Add(String.Format("Person{0}", i), i + i);
            }
            this.mainTable.Merge(newTable);
        }

        private DataTable CreateTestTable()
        {
            var result = new DataTable();
            result.Columns.Add("Name");
            result.Columns.Add("Age", typeof(int));
            result.PrimaryKey = new DataColumn[] { result.Columns["Name"] };

            return result;

        }
    }

这看起来很高效,解决了新行困境,但我看到两个缺点:网格似乎会将滚动条重置为选定行(而不是与“用户位置”相同),正确合并需要创建整个目标表的副本(可能很大)。 - Will Rogers
当然可以复制。您需要创建一个新表来合并新数据。这个新表必须具有与旧表相同的数据 + 您想要合并的更改。 - Will Rogers
Henk是对的。您不必在新表中保留旧值。我只是这样做是为了演示如果有具有相同主键但其中一个列的值不同的行,则它会正确地执行更新,但您不需要复制来自第一个表的数据。 - BFree
在我的主要场景中,我需要更新表中的所有行。因此,我的合并表将是原始表的副本,并对每行应用所需的更改。我相当确定这一点,因为我已经测试了它的工作方式。 - Will Rogers

4

你是否考虑在填充表格时断开dataGrid或bindingSource的连接,然后再重新连接?这样可能看起来有些丑陋,但应该会更快。


1
这个方法可以运行,但用户体验不太好,主要是因为当您取消绑定并重新绑定表格时,网格将滚动回顶部。也许我只是一个乐观主义者,但我希望找到更优雅的解决方案。 - Will Rogers
你可以重新定位行,并稍微调整一下,以最小化屏幕重绘。最后一步可能是将网格重新连接到源。 - H H

3
我遇到了类似的问题。这里有一个更简单的解决方案(虽然不够优雅)。
我发现这个:
dataGridView.DataSource = null;
dataTable.BeginLoadData();
foreach(DataRow row in dataTable.Rows){
    //modify row
}
dataTable.EndLoadData();
dataGridView.DataSource = dataTable;

这种方式比这种方式快得多:

dataTable.BeginLoadData();
foreach(DataRow row in dataTable.Rows){
    //modify row
}
dataTable.EndLoadData();

祝福您——DC


3
如果您正在使用BindingSource进行复杂数据绑定,那么了解SuspendBindingResumeBinding仅暂停和恢复当前项的绑定是很重要的。这使您可以禁用当前项的绑定并更改其属性而不将任何单个属性更改推送到绑定控件中。(这在BindingSource的文档中没有解释,但在CurrencyManager的文档中有说明。)
对列表中其他项目所做的任何更改(即除当前项之外的所有内容)都会触发ListChanged事件。如果禁用这些事件,则BindingSource停止向绑定控件传递有关列表的更改,直到重新启用它们。这就是您看到的结果:您将所有行添加到底层DataTable中,但由于已关闭ListChanged事件,因此BindingSource不会告诉DataGridView有关它们,因此DataGridView保持为空。
正确的解决方案是调用ResetBindings,它强制BindingSource使用其绑定列表中的当前值刷新所有绑定到它的控件。
在调用ResetBindings后您遇到了什么样的异常?因为在我这里它完全正常,无论我是添加、编辑、删除还是从底层DataTable中删除行。

与我的同事的代码进行测试后,我们发现在调用ResetBindings之后出现了很多奇怪的问题,包括幻影网格行和网格抛出的IndexOutOfRange异常。今天将进行更多实验以查看是否还有其他问题。 - Will Rogers

1

我想把这个作为解决方案发出来:对评论和已有帖子进行一些注释。 BFree提到的Merge Table方法是一个非常好的方法,我认为这是正确的方法,而且非常简单优雅。 以下是我的笔记,为什么选择这种方法,还有一个非常重要的问题是我不确定是否有人注意到了查询对服务器的影响。 Op在他对BFree的评论中说,他需要复制表格才能做他需要做的事情,当然我不确定是哪个表格,因为他的代码:

foreach (DataRow row in data.Rows)

这些行来自他名为"data"的表格,需要复制的内容已经存在。

然后,在每次循环中,以下内容都会显而易见:

SlowLoadingData slow_stuff = slow_query_results[(int)row["id"]];

请问OP每次迭代这些行是否真的在查询数据库(如果是大表,我们说的是10万行+)。

请考虑服务器的负载(他的应用程序也必须生成此查询请求!),以及执行此操作所需的网络流量量!即使它是唯一的应用程序,也可能没问题,但即使如此,如果想要高效率,这也不是我想做的事情。

如果从数据库中收集数据的一个查询看起来太多了-那么也许更好的方法是分页获取数据并进行合并。

SlowLoadingData Page1_SlowLoadingData = slow_query_results[Page1] as DataTable;
data.Merge(Page1_SlowLoadingData);

SlowLoadingData Page2_SlowLoadingData = slow_query_results[Page2] as DataTable;
data.Merge(Page2_SlowLoadingData);

0

我发现Ravi LVS在codeproject上提供的解决方案很有效:

BindingSource bs = new BindingSource();
DataTable dt = new DataTable();

bs.DataSource = dt;
bs.SuspendBinding();
bs.RaiseListChangedEvents = false; 
bs.Filter = "1=0"; 
dt.BeginLoadData(); 

//== some modification on data table

dt.EndLoadData();
bs.RaiseListChangedEvents = true;
bs.Filter = "";

原始页面链接:http://www.codeproject.com/Tips/55730/Achieve-performance-while-updating-a-datatable-bou


0
我发现使用resetBindings似乎会移动滚动条,用户会想“我做了什么?”。我发现使用bindingList作为数据源,使用INotifyPropertyChanged的对象,然后当我编辑行时(绑定到一个对象),直到在表单上进行单击或选择更改之前,该行才会被更新。
但是调用dgv.Refresh()似乎解决了问题,而不会改变滚动条。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接