填充动态 DataGrid 时,BackgroundWorker 无法防止 ProgressBar 冻结

BackgroundWorker is not able to prevent ProgressBar freezing when populating dynamic DataGrid

1- 将以下代码复制并粘贴到 MainWindow.xaml 文件中。

<Window x:Class="WpfApplication1.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525" Loaded="Window_Loaded">
<Grid>
    <DataGrid x:Name="DataGrid1"/>
</Grid>
</Window>

2- 将以下代码复制并粘贴到 MainWindow.xaml.cs 文件中。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.ComponentModel;
using System.Threading;

namespace WpfApplication1
{

public partial class MainWindow : Window
{
    BackgroundWorker BackgroundWorker1 = new BackgroundWorker();
    BackgroundWorker BackgroundWorker2 = new BackgroundWorker();
    System.Data.DataTable DataTable1 = new System.Data.DataTable();

    public MainWindow()
    {
        InitializeComponent();
        BackgroundWorker1.DoWork += BackgroundWorker1_DoWork;
        BackgroundWorker2.DoWork += BackgroundWorker2_DoWork;
    }

    void Window_Loaded(object sender, RoutedEventArgs e)
    {
        BackgroundWorker1.RunWorkerAsync();
        BackgroundWorker2.RunWorkerAsync();
    }

    private void BackgroundWorker1_DoWork(System.Object sender, System.ComponentModel.DoWorkEventArgs e)
    {
        Dispatcher.Invoke(() =>
        {
            Window1 myWindow1 = new Window1();
            myWindow1.ShowDialog();
        });
    }

    private void BackgroundWorker2_DoWork(System.Object sender, System.ComponentModel.DoWorkEventArgs e)
    {
        for (int i = 1; i <= 7; i++)
            DataTable1.Columns.Add();
        for (int i = 1; i <= 1048576; i++)
            DataTable1.Rows.Add(i);
        Dispatcher.Invoke(() =>
        {
            DataGrid1.ItemsSource = DataTable1.DefaultView;
        });
    }

}
}

3-新建一个Window并命名为Window1.

4- 将以下代码复制并粘贴到 Window1.xaml 文件中。

<Window x:Class="WpfApplication1.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="1000" ContentRendered="Window_ContentRendered">
<Grid>
    <ProgressBar x:Name="ProgressBar1" Height="25" Width="850"/>
</Grid>
</Window>

5- 将以下代码复制并粘贴到 Window1.xaml.cs 文件中。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;

namespace WpfApplication1
{
public partial class Window1 : Window
{
    public Window1()
    {
        InitializeComponent();
    }

    private void Window_ContentRendered(object sender, EventArgs e)
    {
        ProgressBar1.IsIndeterminate = true;
    }
}
}

6- 当你 运行 这个项目时你会看到 ProgressBar1 冻结两三秒,而下一行 运行s 因为向 DataGrid 添加了 1048576 行。 (大行)

DataGrid1.ItemsSource = DataTable1.DefaultView;

我不想 ProgressBar1 冻结。

那么为什么BackgroundWorker不能防止ProgressBar冻结?

首先尝试将项目放入 ObservableCollection 中。停顿要短得多。它不知道你可以完全消除它,因为网格需要绑定在 UI 线程上。

private void BackgroundWorker2_DoWork(System.Object sender, System.ComponentModel.DoWorkEventArgs e)
{
    for (int i = 1; i <= 7; i++)
        DataTable1.Columns.Add();
    for (int i = 1; i <= 1048576; i++)
        DataTable1.Rows.Add(i);
    var col = new ObservableCollection<MyItem>();
    foreach (DataRow row in DataTable1.Rows) col.Add(new MyItem(row));
    Dispatcher.Invoke(() =>
    {
        DataGrid1.ItemsSource = col;
    });
}

public class MyItem
{
    public MyItem() { }
    public MyItem(DataRow row)
    {
        int.TryParse(row[0].ToString(),out int item1);
        int.TryParse(row[1].ToString(), out int item2);
        int.TryParse(row[2].ToString(), out int item3);
        int.TryParse(row[3].ToString(), out int item4);
        int.TryParse(row[4].ToString(), out int item5);
        int.TryParse(row[5].ToString(), out int item6);
        int.TryParse(row[6].ToString(), out int item7);
    }
    public int item1 { get; set; }
    public int item2 { get; set; }
    public int item3 { get; set; }
    public int item4 { get; set; }
    public int item5 { get; set; }
    public int item6 { get; set; }
    public int item7 { get; set; }
}

当唯一的 UI 线程忙于将数据绑定到 DataGrid 时,UI 将出现冻结。除了避免绑定大量数据或使用数据 virtualization 之外,没有什么可以解决的。但是,您仍然可以通过使事情异步来优化此代码。

private Task<DataView> GetDataAsync()
    {
        return Task.Run(() =>
        {
            for (int i = 1; i <= 7; i++)
                DataTable1.Columns.Add();
            for (int i = 1; i <= 1048576; i++)
                DataTable1.Rows.Add(i);
            return DataTable1.DefaultView;
        });
    }

 private  void BackgroundWorker2_DoWork(System.Object sender, System.ComponentModel.DoWorkEventArgs e)
    {
        Dispatcher.Invoke((Action)(async () =>
        {
            DataGrid1.ItemsSource = await GetDataAsync();
        }));

    }

在你的情况下,我认为问题是在 GUI 线程上生成 DefaultView。将其移至 BG-Worker:

private void BackgroundWorker2_DoWork(System.Object sender, System.ComponentModel.DoWorkEventArgs e)
{
    for (int i = 1; i <= 7; i++)
        DataTable1.Columns.Add();
    for (int i = 1; i <= 1048576; i++)
        DataTable1.Rows.Add(i);

    var dv = DataTable1.DefaultView; //generating the default view takes ~ 2-3 sec.

    Dispatcher.Invoke(() =>
    {
        DataGrid1.ItemsSource = dv;
    });
}

不要忘记在 XAML 中为 DataGrid 设置 EnableRowVirtualization="True"MaxHeight

您的方法的问题是 DataTable 没有实现 INotifyPropertyChanged。因此添加一行不会更新视图(更准确地说是绑定)。要强制刷新,您必须在每次添加行时重置 ItemsSource 或在创建所有 n 行后重置它。

这会导致 UI 线程忙于绘制,例如,一次绘制 1,048,576 行 * 7 列 - 没有资源可用于绘制 ProgressBar,它将冻结。
这使得 DataTable 在处理大量数据且无法忍受冻结时间时成为一个糟糕的选择。

解决方案是选择一个允许逐行添加数据而无需强制重绘完整视图的数据源。

只要启用虚拟化(默认情况下 DataGrid 是正确的)并且列数不超过临界计数(虚拟化仅适用于行和不是列):

解决方案 1

ObservableCollection 只允许绘制 new/changed 行。它会提高 INotifyCollectionChanged.CollectionChanged 触发 DataGrid 到 add/remove/move 只有改变的项目:

MainWindow.xaml

<Window>
  <StackPanel>
    <DataGrid x:Name="DataGrid1" 
              AutoGenerateColumns="False" 
              ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=local:MainWindow}, Path=DataTableRowCollection}"/>
  </StackPanel>
</Window>

MainWindow.xaml.cs

public ObservableCollection<RowItem> DataTableRowCollection { get; } = new ObservableCollection<RowItem>();

private async void BackgroundWorker2_DoWork(System.Object sender, System.ComponentModel.DoWorkEventArgs e)
{
  for (int i = 1; i <= 1048576; i++)
  {
    // Use Dispatcher because 
    // INotifyCollectionChanged.CollectionChanged is not raised on the UI thread 
    // (opposed to INotifyPropertyChanged.PropertyChanged)
    await Application.Current.Dispatcher.InvokeAsync(
      () => this.DataTableRowCollection.Add(new RowItem(i)),
      DispatcherPriority.Background);
  }
}

RowItem.cs

public class RowItem
{
  public RowItem(int value)
  {
    this.Value = value;
  }

  public int Value { get; set; }
}

备注

缺点是您的列数与数据模型耦合。在运行时向 DataGrid 添加列是不可能的,除非您也在运行时动态创建类型(使用反射)或使用嵌套数据集合来表示列。
但是添加一列总是会导致重新绘制完整的 table(最好是新添加列的所有新单元格),除非使用虚拟化。


解决方案 2

当需要动态列计数时,您可以使用封装在 DataGrid 的扩展 class 或宿主控件的代码隐藏中的 C# 直接处理 DataGrid。但是您绝对不应该处理视图模型中的列或行容器。

想法是手动将 DataGridColumn 个元素添加到 DataGrid.Columns 集合。下面的示例一次只绘制新添加的列的所有单元格。

以下示例使用 Button 在每次按下时动态添加新列(在 DataGrid 初始化为 1,048,576 行之后):

MainWindow.xaml

<Window>
  <StackPanel>
    <Button Content="Add Column" Click="AddColumn_OnClick"/>
    <DataGrid x:Name="DataGrid1" 
              AutoGenerateColumns="False" 
              ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=local:MainWindow}, Path=DataTableRowCollection}"/>
  </StackPanel>
</Window>

MainWindow.xaml.cs

private async void BackgroundWorker2_DoWork(System.Object sender, System.ComponentModel.DoWorkEventArgs e)
{
  // Create 7 columns in the view
  for (int columnIndex = 0; columnIndex < 7; columnIndex++)
  {
    await Application.Current.Dispatcher.InvokeAsync(
      () =>
      {
        var textColumn = new DataGridTextColumn
        {
          Header = $"Column {columnIndex + 1}", 
          Binding = new Binding($"ColumnItems[{columnIndex}].Value")
        };
        this.DataGrid1.Columns.Add(textColumn);
      },
      DispatcherPriority.Background);
  }

  // Create the data models for 1,048,576 rows with 7 columns
  for (int rowCount = 0; rowCount < 1048576; rowCount++)
  {
    int count = rowCount;
    await Application.Current.Dispatcher.InvokeAsync(() =>
    {
      var rowItem = new RowItem();
      for (; count < 7 + rowCount; count ++)
      {
        rowItem.ColumnItems.Add(new ColumnItem((int) Math.Pow(2, count)));
      }
      this.DataTableRowCollection.Add(rowItem);
    }, DispatcherPriority.Background);
  }
}

private void AddColumn_OnClick(object sender, RoutedEventArgs e)
{
  int newColumnIndex = this.DataTableRowCollection.First().ColumnItems.Count;
  this.DataGrid1.Columns.Add(
    new DataGridTextColumn()
    {
      Header = $"Dynamically Added Column {newColumnIndex}",
      Binding = new Binding($"ColumnItems[{newColumnIndex}].Value")
    });

  int rowCount = 0;

  // Add a new column data model to each row data model
  foreach (RowItem rowItem in this.DataTableRowCollection)
  {
    var columnItem = new ColumnItem((int) Math.Pow(2, newColumnIndex + rowCount++);
    rowItem.ColumnItems.Add(columnItem);
  }
}

RowItem.cs

public class RowItem
{
  public RowItem()
  {
    this.ColumnItems = new ObservableCollection<ColumnItem>();
  }
  public ObservableCollection<ColumnItem> ColumnItems { get; }
}

ColumnItem.cs

public class ColumnItem
{
  public ColumnItem(int value)
  {
    this.Value = value;
  }

  public int Value { get; }
}