【问题标题】:BackgroundWorker to read database and update GUIBackgroundWorker 读取数据库和更新 GUI
【发布时间】:2017-11-08 14:31:41
【问题描述】:

当我查询数据库并更新 DoWork 中的 DataGridBackgroundWorker 时,我试图让我不确定的 ProgressBar 动画顺利运行。我知道我无法更新 DoWork 中的 UIThread,所以我使用 Dispatcher.BeginInvoke 访问 DataGrid,但我仍然收到 own-by-another-thread 异常。

问题一

为什么会这样? DoWork 应该打开 BackgroundWorker 的线程归 UIThread 拥有,Dispatcher 应该 允许我的 DoWork 访问我的 GUI。

进一步研究 BackgroundWorker 建议 UI 操作应在 ProgressChanged() 中处理。但是,如果我将所有数据库/GUI 操作移至 ProgressChanged() 并在 DoWork() 上放置 Thread.Sleep(5000) 来模拟一些工作,以便为 ProgressChanged() 运行,GUI 没有更新,尽管ProgressBar 继续其不确定的平滑动画。

问题 2

我在 BackgroundWorker 线程上调用了 Thread.Sleep(5000)。 ProgressChanged() 应该花 5 秒查询数据库和 更新图形用户界面。为什么不呢?

XAML:

<DataGrid x:Name="myDataGrid" ScrollViewer.CanContentScroll="True" 
          EnableRowVirtualization="True" EnableColumnVirtualization="True"
          VirtualizingPanel.IsContainerVirtualizable="True" 
          VirtualizingPanel.IsVirtualizing="True"
          VirtualizingPanel.IsVirtualizingWhenGrouping="True"
          Height="500" MaxHeight="500" />

<ProgressBar x:Name="myProgressBar" Height="20" Width="400"
             IsIndeterminate="True" Visibility="Visible" />

<Button x:Name="mySearch" Click="btnSearch_Click">Search</Button>

C#

private BackgroundWorker bgw = new BackgroundWorker();

private void btnSearch_Click(object sendoer, RoutedEventArgs e)
{
    bgw.WorkerReportsProgress = true;
    bgw.ProgressChanged += ProgressChanged;
    bgw.DoWork += DoWork;
    bgw.RunWorkerCompleted += BGW_RunWorkerCompleted;

    bgw.RunWorkerAsync();
}

private void DoWork(object sender, DoWorkEventArgs e)
{
    // Thread.Sleep(5000);
    using (SqlConnection conn = new SqlConnection(connStr))
    {
        conn.Open();
        using (SqlCommand cmd = new SqlCommand("SELECT * FROM " + MyTableName, conn))
        {
            using (SqlDataReader rdr = cmd.ExecuteReader())
            {
                while (rdr.Read())
                {
                    Dispatcher.BeginInvoke(new Action(() =>
                    {
                        myDataGrid.Items.Add(new
                        {
                            Id = rdr.GetInt32(0),
                            Name = rdr.GetString(1).ToString(),
                        });
                    }));
                }
            }
        }
    }
}

private void ProgressChanged(object sender, ProgressChangedEventArgs e)
{
   // Paste code from DoWork and uncomment Thread.Sleep(5000)
}

private void BackgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
}

【问题讨论】:

  • GUI(即 DataGrid)更新时在 Dispatcher 中引发异常。如果操作在 DoWork() 中,我会得到“调用线程无法访问此对象,因为不同的线程拥有它。”例外。
  • 请参考我的回答。
  • 哪个对象拥有您正在使用的Dispatcher?它是否与myDataGrid 所在的线程相关联?您可以尝试使用与您尝试访问的对象关联的调度程序,这是一种很好的做法。在你的情况下,这将是myDataGrid.Dispatcher.BeginInvoke(...)
  • 在您的 DoWork 方法中使用 Invoke 而不是 BeginInvoke。这样,它将触发 UI 线程上的操作。但是,我可能会将结果缓存到 100 个结果,然后调用 UI 线程,因为您已经在使用 Virtualization
  • 请解释反对意见,以及为什么不鼓励提出这个问题?我没有发现类似的重复,我相信我以寻求理解而不是寻求解决方案的方式正确地表达了我的问题。

标签: c# wpf backgroundworker


【解决方案1】:

在您的 DoWork 事件处理程序中创建一组项目,然后在后台线程上获取数据后在 RunWorkerCompleted 事件处理程序中将 DataGridItemsSource 属性设置为该属性:

private void btnSearch_Click(object sendoer, RoutedEventArgs e)
{
    bgw.WorkerReportsProgress = true;
    bgw.ProgressChanged += ProgressChanged;
    bgw.DoWork += DoWork;
    bgw.RunWorkerCompleted += BGW_RunWorkerCompleted;

    myProgressBar.Visibility = Visibility.Visible;
    bgw.RunWorkerAsync();
}

private void DoWork(object sender, DoWorkEventArgs e)
{
    List<object> results = new List<object>();
    using (SqlConnection conn = new SqlConnection(connStr))
    {
        conn.Open();
        using (SqlCommand cmd = new SqlCommand("SELECT * FROM " + MyTableName, conn))
        {
            using (SqlDataReader rdr = cmd.ExecuteReader())
            {
                while (rdr.Read())
                {
                    results.Add(new
                    {
                        Id = rdr.GetInt32(0),
                        Name = rdr.GetString(1).ToString(),
                    });
                }
            }
        }
    }

    e.Result = results;
}

private void BackgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    myDataGrid.ItemsSource = e.Result as List<object>;
    myProgressBar.Visibility = Visibility.Collapsed;
}

您需要调用BackgroundWorkerReportProgress 方法来报告任何进度,但是报告数据库检索操作的进度是毫无意义的,因为您对实际进度一无所知。您最好只使用不确定的ProgressBar,并在开始操作时显示它,并在操作完成时隐藏它,如上面的示例代码所示。

Thread.Sleep 会阻塞当前线程。如果您阻止 UI 线程,您的 UI(包括 ProgressBar)将无法更新,因此您不想这样做。

【讨论】:

  • 感谢您提供可能的解决方案,但我仍然想了解我的问题。我的进度条已经不确定,但即使在我的理解中,动画冻结也在不同的线程上运行
  • 您传递给 Dispatcher.BeginInvoke 的委托始终在调度程序线程上执行。因此,如果您在紧密循环中调用 BeginInvoke,UI 线程将冻结。
  • 我逐行复制您的代码,但不确定的 ProgressBar 动画冻结。我猜想在 RunWorkerCompleted() 中更新 ItemsSource 会冻结 UI。
  • 同时使用 ItemsSource 而不是 SqlDataReader 逐行添加 DataGrid 最多可增加 30% 的内存使用量并相应地减慢加载时间。因此,我仍然想了解这些问题,以便了解如何处理
  • “也使用 ItemsSource 而不是 SqlDataReader...”。不。当您将在 DataGrid 中看到的项目添加到 Items 集合时,您认为它们存储在哪里?与直接将项目添加到 Items 属性相比,将 ItemsSource 设置为 IEnumerable 肯定不会影响性能。您应该更喜欢前一种方法。事实上,如果您希望能够编辑项目,它是必需的。如果您在设置 ItemsSource 属性时冻结,您可能以某种方式禁用了 UI 虚拟化。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-01-10
  • 1970-01-01
  • 1970-01-01
  • 2013-02-13
  • 2013-10-26
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多