WPF:绑定到已异步填充的 ObservableCollection 时出现异常
WPF: Exception when Binding to an ObservableCollection that has been filled asynchronously
我在向通过异步等待在后台线程中创建的 ObservableCollection 添加项目时遇到问题。我将 MenuItemViewModels 绑定到 TreeView ContextMenu 中的分层数据模板,这些模板在为 Treeview 编写的 ViewModel 中动态加载(TreeNodesViewModel - > LoadMenuItemsAsync; UI-Thread):
public class TreeNodesViewModel: BaseViewModel {
private FullyObservableCollection<MenuItemBaseViewModel> menuitems = new FullyObservableCollection<MenuItemBaseViewModel>();
private CollectionViewSource viewsourcemenuitems = new CollectionViewSource();
public FullyObservableCollection<MenuItemBaseViewModel> MenuItems {
get { return menuitems; }
set { SetProperty(ref menuitems, value); }
}
public TreeNodesViewModel() {
GenerateCompleteTree();
}
public void GenerateCompleteTree(bool setSelectLastSelectedItem = true, string path = null) {
LoadMenuItemsAsync();
...
}
public async void LoadMenuItemsAsync() {
Task load = Task.Run(async () => {
foreach (Type type in Assembly.GetExecutingAssembly().GetTypes().Where(x => x.IsClass && x.BaseType == typeof(MenuItemBaseViewModel) && x.Namespace.Equals("***.UI.Tree.MenuItems"))) {
MenuItems.Add((MenuItemBaseViewModel)Activator.CreateInstance(type));
}
});
await load;
}
}
每个 MenuItemViewModel 派生自 MenuItemBaseViewModel,它还包含一个 ObservableCollection 子项,以便使用 HierachicalDataTemplate 创建一个 Hierarchical Menustructure:
public class MenuItemAddNewSystem : MenuItemBaseViewModel {
public MenuItemAddNewSystem() : base(PackIconModernKind.Add, Colors.Green) {
Uid = "...";
Header = "Add new System";
}
#region Overrides
public override bool IsVisible {
get {
return true;
}
}
public override void ExecuteMouseLeftButtonDownCommand(object parameter) {
throw new NotImplementedException();
}
#endregion
}
public abstract class MenuItemBaseViewModel : BaseViewModel {
private FullyObservableCollection<MenuItemBaseViewModel> subitems = new FullyObservableCollection<MenuItemBaseViewModel>();
private CollectionViewSource viewsource = new CollectionViewSource();
public FullyObservableCollection<MenuItemBaseViewModel> SubItems {
get { return subitems; }
set { if (SetProperty(ref subitems, value)) OnPropertyChanged("MenuItemSource"); }
}
public ICollectionView MenuItemSource {
get {
if (SubItems.Count(x => x.IsVisible) == 0) return null;
viewsource.Source = SubItems.Where(x => x.IsVisible); //<--- Exception here!
return viewsource.View;
}
}
public MenuItemBaseViewModel(Enum packiconkind, Color? iconcolor = null) {
SetPackIcon(packiconkind, iconcolor);
AddEventHandler(SubItems);
...
}
#region NotifyChanged-Events
public override void NotifyCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) {
OnPropertyChanged("MenuItemSource");
}
public override void NotifyItemPropertyChanged(object sender, ItemPropertyChangedEventArgs e) {
if (e.PropertyName.Equals("IsVisible")) OnPropertyChanged("MenuItemSource");
}
#endregion
...
}
XAML:
<ContextMenu x:Key="MenuItemContextMenu" ItemsSource="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}, Path=TreeViewModel.MenuItemSource}">
<ContextMenu.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type model:MenuItemBaseViewModel}" ItemsSource="{Binding Path=MenuItemSource, UpdateSourceTrigger=PropertyChanged}">
<TextBlock Text="{Binding Header}"/>
</HierarchicalDataTemplate>
</ContextMenu.ItemTemplate>
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="ToolTip" Value="{Binding ToolTip}"/>
<Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
<Setter Property="Command" Value="{Binding MouseLeftButtonDownCommand}"/>
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
我的问题是,在通过 NotifyPropertyChanged 更新 MenuItemBaseViewModel 中的 MenuItemSource 时,通过异步等待加载 MenuItems 会出现异常:
System.InvalidOperationException:
The calling thread cannot access this object because a different
thread owns it.
(见代码注释)
将 ContextMenu 绑定到 TreeNodesViewModel 中的 MenuItems-Collection 不会出现异常,因为该 Collection 是在 UI-Thread 上创建的,但是绑定到 MenuItemBaseViewModel 中的第二层子项会引发异常。
我尝试了以下选项:
- 在添加或删除操作期间使用 BindingOperations.EnableCollectionSynchronization 和锁定
- 使用 Application.Current.Dispatcher
- 使用viewsource.Dispatcher.BeginInvoke
- 使用 Dispatcher.CurrentDispatcher.BeginInvoke
没有任何效果,而且我不断收到错误消息。
我还尝试实施 AsyncObservableCollection (https://www.thomaslevesque.com/2009/04/17/wpf-binding-to-an-asynchronous-collection/) 但没有成功。谁能帮我解决这个问题?
如评论中Mike Zboray所述,需要在正确的线程中创建 ICollectionView。更改 MenuItemBaseViewModel 中 ICollectionView 的 getter 解决了这个问题,因为之前的 ICollectionView 是在后台线程中创建的。返回新创建的一个解决了我的问题:
public ICollectionView MenuItemSource {
get { return new CollectionViewSource { Source = SubItems.Where(x => x.IsVisible) }.View; }
}
我在向通过异步等待在后台线程中创建的 ObservableCollection 添加项目时遇到问题。我将 MenuItemViewModels 绑定到 TreeView ContextMenu 中的分层数据模板,这些模板在为 Treeview 编写的 ViewModel 中动态加载(TreeNodesViewModel - > LoadMenuItemsAsync; UI-Thread):
public class TreeNodesViewModel: BaseViewModel {
private FullyObservableCollection<MenuItemBaseViewModel> menuitems = new FullyObservableCollection<MenuItemBaseViewModel>();
private CollectionViewSource viewsourcemenuitems = new CollectionViewSource();
public FullyObservableCollection<MenuItemBaseViewModel> MenuItems {
get { return menuitems; }
set { SetProperty(ref menuitems, value); }
}
public TreeNodesViewModel() {
GenerateCompleteTree();
}
public void GenerateCompleteTree(bool setSelectLastSelectedItem = true, string path = null) {
LoadMenuItemsAsync();
...
}
public async void LoadMenuItemsAsync() {
Task load = Task.Run(async () => {
foreach (Type type in Assembly.GetExecutingAssembly().GetTypes().Where(x => x.IsClass && x.BaseType == typeof(MenuItemBaseViewModel) && x.Namespace.Equals("***.UI.Tree.MenuItems"))) {
MenuItems.Add((MenuItemBaseViewModel)Activator.CreateInstance(type));
}
});
await load;
}
}
每个 MenuItemViewModel 派生自 MenuItemBaseViewModel,它还包含一个 ObservableCollection 子项,以便使用 HierachicalDataTemplate 创建一个 Hierarchical Menustructure:
public class MenuItemAddNewSystem : MenuItemBaseViewModel {
public MenuItemAddNewSystem() : base(PackIconModernKind.Add, Colors.Green) {
Uid = "...";
Header = "Add new System";
}
#region Overrides
public override bool IsVisible {
get {
return true;
}
}
public override void ExecuteMouseLeftButtonDownCommand(object parameter) {
throw new NotImplementedException();
}
#endregion
}
public abstract class MenuItemBaseViewModel : BaseViewModel {
private FullyObservableCollection<MenuItemBaseViewModel> subitems = new FullyObservableCollection<MenuItemBaseViewModel>();
private CollectionViewSource viewsource = new CollectionViewSource();
public FullyObservableCollection<MenuItemBaseViewModel> SubItems {
get { return subitems; }
set { if (SetProperty(ref subitems, value)) OnPropertyChanged("MenuItemSource"); }
}
public ICollectionView MenuItemSource {
get {
if (SubItems.Count(x => x.IsVisible) == 0) return null;
viewsource.Source = SubItems.Where(x => x.IsVisible); //<--- Exception here!
return viewsource.View;
}
}
public MenuItemBaseViewModel(Enum packiconkind, Color? iconcolor = null) {
SetPackIcon(packiconkind, iconcolor);
AddEventHandler(SubItems);
...
}
#region NotifyChanged-Events
public override void NotifyCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) {
OnPropertyChanged("MenuItemSource");
}
public override void NotifyItemPropertyChanged(object sender, ItemPropertyChangedEventArgs e) {
if (e.PropertyName.Equals("IsVisible")) OnPropertyChanged("MenuItemSource");
}
#endregion
...
}
XAML:
<ContextMenu x:Key="MenuItemContextMenu" ItemsSource="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}, Path=TreeViewModel.MenuItemSource}">
<ContextMenu.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type model:MenuItemBaseViewModel}" ItemsSource="{Binding Path=MenuItemSource, UpdateSourceTrigger=PropertyChanged}">
<TextBlock Text="{Binding Header}"/>
</HierarchicalDataTemplate>
</ContextMenu.ItemTemplate>
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="ToolTip" Value="{Binding ToolTip}"/>
<Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
<Setter Property="Command" Value="{Binding MouseLeftButtonDownCommand}"/>
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
我的问题是,在通过 NotifyPropertyChanged 更新 MenuItemBaseViewModel 中的 MenuItemSource 时,通过异步等待加载 MenuItems 会出现异常:
System.InvalidOperationException: The calling thread cannot access this object because a different thread owns it.
(见代码注释)
将 ContextMenu 绑定到 TreeNodesViewModel 中的 MenuItems-Collection 不会出现异常,因为该 Collection 是在 UI-Thread 上创建的,但是绑定到 MenuItemBaseViewModel 中的第二层子项会引发异常。 我尝试了以下选项:
- 在添加或删除操作期间使用 BindingOperations.EnableCollectionSynchronization 和锁定
- 使用 Application.Current.Dispatcher
- 使用viewsource.Dispatcher.BeginInvoke
- 使用 Dispatcher.CurrentDispatcher.BeginInvoke
没有任何效果,而且我不断收到错误消息。 我还尝试实施 AsyncObservableCollection (https://www.thomaslevesque.com/2009/04/17/wpf-binding-to-an-asynchronous-collection/) 但没有成功。谁能帮我解决这个问题?
如评论中Mike Zboray所述,需要在正确的线程中创建 ICollectionView。更改 MenuItemBaseViewModel 中 ICollectionView 的 getter 解决了这个问题,因为之前的 ICollectionView 是在后台线程中创建的。返回新创建的一个解决了我的问题:
public ICollectionView MenuItemSource {
get { return new CollectionViewSource { Source = SubItems.Where(x => x.IsVisible) }.View; }
}