为什么 Dispatcher.BeginInvoke 在重新创建我的 WPF DataGrid 列时不起作用?

Why isn't Dispatcher.BeginInvoke working when re-creating my WPF DataGrid columns?

我有一个 DataGrid,其中包含一组为日期范围动态生成的列,并使用自定义依赖项绑定到网格 属性、

public static readonly DependencyProperty BindableColumnsProperty =
    DependencyProperty.RegisterAttached("BindableColumns",
        typeof(ObservableCollection<DataGridColumn>),
        typeof(DataGridAttachedProperties),
        new UIPropertyMetadata(null, BindableColumnsPropertyChanged));

BindableColumnsPropertyChanged 包含出现问题的以下代码:

else if (ne.Action == NotifyCollectionChangedAction.Add)
{
    foreach (DataGridColumn column in ne.NewItems)
    {
        dataGrid.Columns.Add(column);
    }
}
else if (ne.Action == NotifyCollectionChangedAction.Remove)
{
    foreach (DataGridColumn column in ne.OldItems)
    {
        dataGrid.Columns.Remove(column);
    }
}

当我从 RefreshCommand 代码调用我的 InitColumns 方法时,在 dataGrid.Columns.Remove(column) 我收到错误:

The calling thread cannot access this object because a different thread owns it. I fixed that by changing the Remove code to:

Application.Current.Dispatcher.BeginInvoke((Action)(() =>
{
    dataGrid.Columns.Remove(column);
}));

然后我再试一次,Remove 代码工作了,但我显然在 dataGrid.Columns.Add(column) 行得到了同样的错误。我也改变了:

Application.Current.Dispatcher.BeginInvoke((Action)(() =>
{
    dataGrid.Columns.Add(column);
}));

然后我再次尝试 运行 刷新命令,新调度的 Remove 仍然有效,但现在我得到了同样的错误,但是使用 BeginInvoke 委托 dataGrid.Columns.Add(column) 呼叫:

这个我不明白。肯定是同一个线程成功删除了该列,但现在看起来像是某个新的幻影线程正在尝试添加该列。这是什么原因造成的?

试试这个代码

dataGrid.Dispatcher.BeginInvoke((Action)(() =>
{
    dataGrid.Columns.Add(column);
}));

而不是您的代码。

我认为您正在使用多个 UI 线程,所以您会收到此错误。

据我了解,您正在从非 UI 线程操纵 UI 绑定 ObservableCollection<DataGridColumn>。因此,在处理 CollectionChanged(以及 PropertyChanged)时使用 Dispatcher 并将更改应用到 DataGrid 是正确的方法。

但为什么 Remove 有效而 Add 无效?

它没有在 post 中显示,但我怀疑您不仅要将 DataGridColumn 个后代添加到您的可观察集合中,而且您还在 创建 (并在非 UI 线程上初始化)它们,这将导致所描述的行为。

因为DataGridColumn class inherits (through DependencyObject) the DispatcherObjectDispatcherObject class(因此派生的 classes)将当前线程 Dispatcher 存储在其 构造函数 中,然后使用该调度程序验证 each 依赖性 属性 访问是从创建对象的线程完成的。

很快,整个 DataGridColumn 创建和操作应该发生在 UI 线程上。找到创建列的代码,并确保它通过使用 Application.Current.Dispatcher 和一些 Dispatcher.Invoke 重载在 UI 线程上执行。

P.S。虽然以上是我能想到的最合乎逻辑的解释,但您可以通过修改posted代码如下来验证假设是否正确:

Application.Current.Dispatcher.BeginInvoke((Action)(() =>
{
    if (!column.CheckAccess())
    {
        // Houston, we've got a problem!
    }
    dataGrid.Columns.Add(column);
}));

并在 if 语句中放置一个断点。

Application.Current.Dispatcher 将为您获取当前应用程序的 Dispatcher。所以调用 BeginInvoke 并不一定意味着它会起作用:如果不是那个线程不是控件的所有者怎么办?毕竟,任何线程都可以创建控件。因此,您应该尝试获取控件的 Dispatcher,然后在其上调用 BeginInvoke

YourControl.Dispatcher.Invoke();

这并不意味着您可以简单地这样做:

if (column.CheckAccess())
{
    // Great this thread has access so I will remove this column.
    dataGrid.Columns.Add(column);
}

那个检查只告诉你当前线程拥有 column 但不一定拥有 dataGridColumns 集合(我不太确定但是你有代码所以你可以简单地尝试一下)。因此,您需要这样做:

if (column.CheckAccess())
{
    if (dataGrid.CheckAccess())
    {
        // Code here. You may need to check `Columns` as well but like I said I am not sure about this one.
    }
    else
    {
        dataGrid.Dispatcher.BeginInvoke(/* code here */);
    }
}
else
{
    column.Dispatcher.BeginInvoke(/* code here */);
}

现在您明白这会让您原地踏步,因此,最好在同一个线程上创建父 ui 控件及其子控件。至少这将帮助您找出问题,并且将来您知道不要使用 Application.Current.Dispatcher.

您收到此错误是因为您在非 ui 线程上创建了 DataGridColumn。

当执行 dataGrid.Columns.Add(column); 时,DataGrid 内部正在尝试获取新项目的 DataGridColumn.DisplayIndex 依赖项 属性 值,并且在执行此操作时 DataGridColumn 调用 CheckAccess 来验证当前thread 是创建它的线程,此测试失败并导致抛出异常。

所以是 DataGridColumn 引发了异常。

我创建了一个小测试项目来检查这个 (see github),特别是创建了两个仅与线程 DataGridColumns 不同的方法:

这个导致异常(在线程池线程上创建列):

await Task.Delay(1).ConfigureAwait(false);    //detaching to thread pool thread

ViewModel.Columns = new ObservableCollection<DataGridColumn>(); //initializing colleciton in thread pool, but this gets marshalled by WPF

Dispatcher.Invoke(() => { }, DispatcherPriority.ApplicationIdle);  //wait for dispatched PropertyChanged to take effect

//creating cols in thread pool thread
var cols = new List<DataGridColumn>
{
    new DataGridTextColumn {Header = "test col 1", Binding = new Binding()},
    new DataGridTextColumn {Header = "test col 2", Binding = new Binding()}
};

//adding previously created columns - should throw
foreach (var dataGridColumn in cols)
    ViewModel.Columns.Add(dataGridColumn);

这个工作正常(在 UI 线程上创建列):

//creating cols in UI thread
var cols = new List<DataGridColumn>
{
    new DataGridTextColumn {Header = "test col 1", Binding = new Binding()},
    new DataGridTextColumn {Header = "test col 2", Binding = new Binding()}
};

await Task.Delay(1).ConfigureAwait(false);    //detaching to thread pool thread

ViewModel.Columns = new ObservableCollection<DataGridColumn>(); //initializing colleciton in thread pool

Dispatcher.Invoke(() => { },DispatcherPriority.ApplicationIdle);  //wait for dispatched PropertyChanged to take effect

//adding previously created columns - should succeed
foreach (var dataGridColumn in cols)
    ViewModel.Columns.Add(dataGridColumn);

我建议您使用更多的 MVVM 方式,例如创建一个视图模型 class,例如具有视图不可知属性的 ColumnViewModel,绑定 ObservableCollection,然后在附加的 INotifyCollectionChanged 实现中创建实际的 DataGridColumns,例如而不是

Application.Current.Dispatcher.BeginInvoke((Action)(() =>
{
    dataGrid.Columns.Add(column);
}));

你应该这样做

Application.Current.Dispatcher.BeginInvoke((Action)(() =>
{   
     dataGrid.Columns.Add(CreateColumnFromViewModel(columnVm));
}));

反过来,你会得到

  1. 您的视图与 ViewModel 和未绑定到具体视图实现的 ViewModel 分离,
  2. 您可以确定您的 DataGridColumns 总是在 UI 线程
  3. 上创建

如果你有兴趣,我可以详细说明一下

简单来说,在 UI 线程上创建 DataGridColumn 个实例。

如果您想从 async/await 中获益,我会采用对 MVVM 更友好的方法:

  • 而不是使用 ObservableCollection<DataGridColumn> 我会使用 "describe" 所需列的模型列表,例如 ObservableCollection<MyColumnDefinition>

  • 其中 MyColumnDefinition 包含用于创建 DataGridColumn 的属性,例如:映射到列类型、绑定路径、宽度等的值

  • 然后在PropertyChanged处理程序

  • 中进行从MyColumnDefinitionDataGridColumn的转换
  • 创建 DataGridColumn 实例并将其添加到 DataGridColumns 属性

    时,您仍应使用调度程序

这样,您应该可以毫无问题地使用类似以下内容创建列:

await Task.Run(() => new MyColumnDefinition { ... });

我认为可能存在时间问题。您正在使用异步 BeginInvoke。也许两个 Dispatched Actions 会同时 运行 并且你得到这个异常。 您可以尝试改用 Dipatcher.Invoke 或在 DataGrid.Columns 调用周围添加一些 Lock