VB.NET-使用后台工作程序填充DataGridView而不冻结表单

VB.NET-使用后台工作程序填充DataGridView而不冻结表单,vb.net,winforms,asynchronous,datagridview,backgroundworker,Vb.net,Winforms,Asynchronous,Datagridview,Backgroundworker,我的程序包含一个部分,用户可以通过单击按钮用数据库中的内容填充DataGridView。我通过使用后台工作程序来实现这一点,总而言之,我的代码似乎工作得很好,但我不确定我是否按照预期的方式实现了后台工作程序。 问题是,尽管使用了RunWorkerAsync()方法,我的整个表单仍然会冻结10秒钟左右,这大约是处理数据和向DataGridView添加新行所需的时间。我想要实现的是一个解决方案,其中表单保持功能,进度显示在进度条中 以下是我目前正在做的一些(伪)代码: private myList

我的程序包含一个部分,用户可以通过单击按钮用数据库中的内容填充DataGridView。我通过使用后台工作程序来实现这一点,总而言之,我的代码似乎工作得很好,但我不确定我是否按照预期的方式实现了后台工作程序。 问题是,尽管使用了RunWorkerAsync()方法,我的整个表单仍然会冻结10秒钟左右,这大约是处理数据和向DataGridView添加新行所需的时间。我想要实现的是一个解决方案,其中表单保持功能,进度显示在进度条中

以下是我目前正在做的一些(伪)代码:

private myList as new List(of Object)

Private Sub btnClick(sender As Object, e As EventArgs) Handles myButton.Click   
    myBgWorker.RunWorkerAsync()
End Sub

Private Sub doWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles myBgWorker.DoWork
    execute some SQL()
    
    for each SQL-result row
        myList.add(rowData)
        myBgWorker.ReportProgress()
    Loop
End Sub

Private Sub reportProgress(sender As Object, e As System.ComponentModel.ProgressChangedEventArgs) Handles myBgWorker.ProgressChanged
    pgrogressBar.Value = e.ProgressPercentage
End Sub

Private Sub done(sender As Object, e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles myBgWorker.RunWorkerCompleted    
    For Each entry in myList
        Dim rowIdx As Integer = myDGV.Rows.Add()
        Dim dgvRow As DataGridViewRow = myDGV.Rows(rowIdx)

        dgvRow.Cells(0).Value = "something"
        dgvRow.Cells(1).Value = "something else"
        dgvRow.Cells(2).Value = "even more..."
    Next
    
    MsgBox("I'm done.")
End Sub
因此,基本上工人执行一些SQL,循环接收到的行并填充列表,同时在进度条中表示进度。到现在为止,一直都还不错。但一旦它执行RunWorkerCompleted代码,表单就会冻结,我必须等待所有行都添加到DataGridView


我还尝试在reportProgress方法中分别添加每一行,但结果是相同的,而且我理解这些后台工作人员的方式是相同的,此时您不需要更新UI。

您是对的,您没有正确使用BackgroundWorker。您应该知道在哪个线程上调用了哪些事件

您有两个线程:负责所有更新的UI线程和负责所有冗长处理的BackgroundWorker线程

后台工作人员的标准用法

(1) UI线程创建类BackGroundWorker的对象,并设置一些属性。最重要的是:WorkerReportsProgress和WorkerSupportsScanCellation

(2) UI类向BackgroundWorker添加了三个事件处理程序:

  • BackGroundWorker.DoWork
    附加将执行后台工作的方法。此工作将由BackgroundWorker线程完成
  • BackGroundWorker.ProgressChanged
    。附加更新UI的方法,以通知操作员进度。此方法将由UI线程执行
  • BackGroundWorker.runworker已完成
    。将BackgroundWorker完成时必须执行的方法附加到DoWork结束时。此方法也将由UI线程执行
(3) 一段时间后,UI线程决定BackgroundWorker应该开始工作:

对不起,直到现在我还可以避免使用代码。我的VB有点生疏,所以你得应付C。我相信你会明白要点:

this.backgroundWorker.RunWorkerAsync();
如果需要传递参数,请执行以下操作:

MyParam myParam = new MyParam() {...}
this.backgroundWorker.RunWorkerAsync(myParam);
将创建一个新线程,它将开始执行附加到event
BackGroundWorker.DoWork的方法

Sub doWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs)
Handles myBgWorker.DoWork
{
    BackgroundWorker backgroundWorker = sender as BackgroundWorkder
    do some work that doesn't take too long, because you want to report progress
    while work not finished and not backGroundWorker.CancellationRequested
    {
        create a progress report, put it in an object
        if possible: calculate an estimation about your progress in %;

        // report progress, don't use variable backgroundWorkder, use the sender
        (sender as BackgroundWorker).ReportProgress(sender, progressPercentage, progressReport);
    }

    // if here: finished with your calculations, create a result report:
    MyResultReport myReport = ...
    e.Result = myReport;

    // if you promised to support cancellation or errors:
    e.Error = ...
    e.Cancelled = backgroundWorker.CancellationRequested;
    e.State = ...
End Sub
如果你想报告进度,你必须在进度中放入一些数据,否则你的进度接收者对你的进度没有任何线索

您可以使用ProgressBar或类似的百分比。progressReport可用于显示UI线程希望了解的其他信息

顺便说一下:如果您的backgroundWorker在其过程之外不使用任何变量,这是明智的。使用发件人获取BackGroundWorker。将backgroundWorker需要的所有值作为输入参数传递。这样,您的后台工作人员就可以完全不知道是谁发起的。这使得重用它更容易,更改工作线程的所有者也更容易,因此单元测试也更容易

考虑通过创建一个单独的类来完成后台工作,从而强制实现这种分离。这几乎不是更多的工作。如果可重用性、单元测试和可维护性对您不重要,则无需这样做

当backgroundWorker完成繁重的计算后,您可以将计算结果放入e.result中

每次BackgroundWorker报告进度时,UI线程都会在附加到
ProgressChanged

void ProgressReported(object sender, ProgressChangedEventArgs e)
{
    // the designer of the worker thread promised to put his progress in a class MyProgressReport:
    int progressPercentage = e.progressPercentage;
    MyProgressReport progressReport = e.UserState as MyProgressReport;
    this.ProgressReported(progressPercentage, progressReport);
}
顺便说一下:如果您计划对几个后台工作人员使用此事件处理程序,您应该检查
发送者
,以确定谁报告进度以及如何处理进度报告

void ProgressReported(int progressPercentage, MyProgressReport progressReport)
{
    this.ProgressBar1.Value = progressPercentage;
    this.ProgressTextBox.Text = progressReport.ToString();
}
最后,如果工作线程已完成繁重的处理,则调用RunworkerCompleted的EventHandler。UI线程将处理它:

你应该告诉后台工作人员的设计师他应该在结果中加入什么。它可能会删除从数据库中获取的所有数据

public RunWorkerReportsCompletion(object sender, RunWorkderCompletedEventArgs e)
{
    MyResultReport result = e.Result as MyResultReport;
    ProcessResult(result);
}
如果您预期有问题或取消,请考虑检查属性<代码>取消< /代码>和<代码>错误< /代码> .< 总结如下:

  • DoWork中执行所有冗长的处理
  • ProgressChanged和RunWorkerCompleted由UI线程执行。它们应该很轻
  • BackgroundWorker不应使用表单中的任何值;使用事件中的参数进行通信
  • 考虑支持取消。如果窗体正在关闭,而工作线程仍在工作,则可以取消并等待后台工作线程完成

“一旦执行RunWorkerCompleted代码,表单就会冻结”。当然会,因为事件处理程序是在UI线程上执行的。这就是重点。您别无选择,因为必须在UI线程上修改UI。当然,您应该将列表绑定到网格,而不是手动添加行。这将更快,尽管可能仍会暂停。这是不可避免的。你最大的问题是你正在使用一个
列表(对象)
手动向DGV添加行。这并不是使用该控件的真正方式。您可以使用类对象来描述数据并使用BindingList作为容器,然后将其设置为DGV的数据源,或者填充数据表并将其设置为数据源