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

时间:2016-12-10 12:12:52

标签: c# wpf multithreading datagrid begininvoke

我有一个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);
    }
}

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

  

调用线程无法访问此对象,因为它不同   线程拥有它。   我通过将Remove代码更改为:

来解决这个问题
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)调用:(at [A-Z](?:\w+)?|in [A-Z](?:\w+)?|of [A-Z](?:\w+)?)

enter image description here

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

6 个答案:

答案 0 :(得分:3)

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

但为什么Remove有效且Add没有?

帖子中没有显示,但我怀疑你不只是将DataGridColumn个后代添加到你的可观察集合中,而且你还上创建(并初始化)它们UI线程,会导致所描述的行为。

这是因为DataGridColumn类继承了(DependencyObjectDispatcherObjectDispatcherObject类(因此派生类)将当前线程Dispatcher存储在其构造函数中,然后使用该调度程序验证每个依赖项属性访问是从创建对象的线程完成的。

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

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

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

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

答案 1 :(得分:1)

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

答案 2 :(得分:1)

我认为可能存在时间问题。您正在使用异步BeginInvoke。也许两个Dispatched Actions将同时运行并且您得到此异常。 您可以尝试使用Dipatcher.Invoke,或在Lock来电时添加一些DataGrid.Columns

答案 3 :(得分:0)

试试此代码

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

而不是你的代码。

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

答案 4 :(得分:0)

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

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

因此引发异常的是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方式,例如创建一个viewView模型,如具有视图无关属性的ColumnViewModel,绑定ObservableCollection,然后在附加的INotifyCollectionChanged实现中创建实际的DataGridColumns,例如:而不是

Application.Current.Dispatcher.BeginInvoke((Action)(() =>
{
    dataGrid.Columns.Add(column);
}));
你应该做点什么

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

反过来,你会得到

  1. 您的View与未绑定到具体View实现的ViewModel和ViewModel分离,
  2. 您可以确定您的DataGridColumns始终在UI线程上创建
  3. 如果你有兴趣,我可以详细说明这个

答案 5 :(得分:0)

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

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

  • 我不会使用ObservableCollection<DataGridColumn>而是使用“描述”所需列的模型列表,例如ObservableCollection<MyColumnDefinition>

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

  • 然后在MyColumnDefinition处理程序

  • 内进行从DataGridColumnPropertyChanged的转换
  • DataGridColumn的{​​{1}}属性

  • 创建和添加DataGrid个实例时,您仍应使用调度程序

这样,使用以下内容创建列应该没有问题:

Columns