为什么 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 DispatcherObject
。 DispatcherObject
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
但不一定拥有 dataGrid
和 Columns
集合(我不太确定但是你有代码所以你可以简单地尝试一下)。因此,您需要这样做:
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));
}));
反过来,你会得到
- 您的视图与 ViewModel 和未绑定到具体视图实现的 ViewModel 分离,
- 您可以确定您的 DataGridColumns 总是在 UI 线程
上创建
如果你有兴趣,我可以详细说明一下
简单来说,在 UI 线程上创建 DataGridColumn
个实例。
如果您想从 async/await 中获益,我会采用对 MVVM 更友好的方法:
而不是使用 ObservableCollection<DataGridColumn>
我会使用 "describe" 所需列的模型列表,例如 ObservableCollection<MyColumnDefinition>
其中 MyColumnDefinition
包含用于创建 DataGridColumn
的属性,例如:映射到列类型、绑定路径、宽度等的值
然后在PropertyChanged
处理程序
中进行从MyColumnDefinition
到DataGridColumn
的转换
创建 DataGridColumn
实例并将其添加到 DataGrid
的 Columns
属性
时,您仍应使用调度程序
这样,您应该可以毫无问题地使用类似以下内容创建列:
await Task.Run(() => new MyColumnDefinition { ... });
我认为可能存在时间问题。您正在使用异步 BeginInvoke
。也许两个 Dispatched Actions 会同时 运行 并且你得到这个异常。
您可以尝试改用 Dipatcher.Invoke
或在 DataGrid.Columns
调用周围添加一些 Lock
。
我有一个 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 DispatcherObject
。 DispatcherObject
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
但不一定拥有 dataGrid
和 Columns
集合(我不太确定但是你有代码所以你可以简单地尝试一下)。因此,您需要这样做:
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));
}));
反过来,你会得到
- 您的视图与 ViewModel 和未绑定到具体视图实现的 ViewModel 分离,
- 您可以确定您的 DataGridColumns 总是在 UI 线程 上创建
如果你有兴趣,我可以详细说明一下
简单来说,在 UI 线程上创建 DataGridColumn
个实例。
如果您想从 async/await 中获益,我会采用对 MVVM 更友好的方法:
而不是使用
ObservableCollection<DataGridColumn>
我会使用 "describe" 所需列的模型列表,例如ObservableCollection<MyColumnDefinition>
其中
MyColumnDefinition
包含用于创建DataGridColumn
的属性,例如:映射到列类型、绑定路径、宽度等的值然后在
PropertyChanged
处理程序 中进行从创建
时,您仍应使用调度程序DataGridColumn
实例并将其添加到DataGrid
的Columns
属性
MyColumnDefinition
到DataGridColumn
的转换
这样,您应该可以毫无问题地使用类似以下内容创建列:
await Task.Run(() => new MyColumnDefinition { ... });
我认为可能存在时间问题。您正在使用异步 BeginInvoke
。也许两个 Dispatched Actions 会同时 运行 并且你得到这个异常。
您可以尝试改用 Dipatcher.Invoke
或在 DataGrid.Columns
调用周围添加一些 Lock
。