英文:
Why does adding items to an ICollectionView from a UI thread in WPF result in a error, while it works fine with ObservableCollection?
问题
I've translated the provided code for you:
我正在开发一个WPF应用程序,遇到了使用ICollectionView将项目添加到ObservableCollection<VMDataPorts>的问题。我有一个名为IOPortsViewModel的类,其中包含一个名为DataPorts的ObservableCollection<VMDataPorts>。在这个类的构造函数中,我使用CollectionViewSource.GetDefaultView(DataPorts)创建了一个名为CollectionDataPorts的ICollectionView实例。
在我的MainWindowViewModel类中,我有一个名为ReloadMainTreeViewSeparateThread()的方法,它在一个单独的工作线程中创建了一些新的MenuItem对象,并将它们添加到一个名为MenuItemsLeft的ObservableCollection<MenuItem>中,该集合在UI线程上定义,通过Dispatcher进行操作。这个方法工作得很好。
然而,当我尝试从UI线程添加项目到CollectionDataPorts时,我会收到以下错误消息:
System.NotSupportedException: '此类型的CollectionView不支持在与Dispatcher线程不同的线程中更改其SourceCollection。'
如果我直接绑定到IOPortsView中的DataPorts而不是CollectionDataPorts,一切都正常工作。
这是我的代码供参考:
```csharp
internal class IOPortsViewModel : ObservableObject
{
public IOPortsViewModel(object obj)
{
//处理obj
CollectionDataPorts = CollectionViewSource.GetDefaultView(DataPorts);
}
private ICollectionView m_collectionDataPorts;
public ICollectionView CollectionDataPorts
{
get => m_collectionDataPorts;
set => SetProperty(ref m_collectionDataPorts, value);
}
private ObservableCollection<VMDataPorts> m_dataPorts;
public ObservableCollection<VMDataPorts> DataPorts
{
get => m_dataPorts;
set => SetProperty(ref m_dataPorts, value);
}
}
IOPortsView:
<DataGrid Name="dataGrid" ItemsSource="{Binding CollectionDataPorts}">
internal class MainWindowViewModel : CommunityToolkit.Mvvm.ComponentModel.ObservableObject
{
ObservableCollection<MenuItem> MenuItemsLeft = new();
public void ReloadMainTreeViewSeparateThread()
{
Thread t = new Thread(() =>
{
App.Current.Dispatcher.Invoke(() => { IsDialogLoadingOpen = true; });
try
{
// 在这里进行一些耗时的工作,需要创建ViewModels和MenuItems
object objFromBackend = Backend.GetSomeExpensiveObject();
ObservableCollection<MenuItem> NewProcessedLeftMenuTree = new()
{
new MenuItem ( new IOPortsViewModel(objFromBackend), "Title"),
};
// 将在此工作线程(Thread t)上创建的菜单项添加到UI线程中
App.Current.Dispatcher.Invoke(()=>
{
foreach (MenuItem menuItem in NewProcessedLeftMenuTree)
{
MenuItemsLeft.Add(menuItem);
}
});
}
catch (AggregateException aggrEx)
{
App.Current.Dispatcher.BeginInvoke(() =>
{
IsDialogLogOpen = true;
});
}
finally
{
App.Current.Dispatcher.Invoke(() =>
{
IsDialogLoadingOpen = false;
});
}
});
t.SetApartmentState(ApartmentState.STA);
t.Start();
}
private bool m_isDialogLoadingOpen;
public bool IsDialogLoadingOpen
{
get => m_isDialogLoadingOpen;
set => SetProperty(ref m_isDialogLoadingOpen, value);
}
private bool m_isDialogLogOpen;
public bool IsDialogLogOpen
{
get => m_isDialogLogOpen;
set => SetProperty(ref m_isDialogLogOpen, value);
}
}
为什么在IOPortsView中直接绑定到ObservableCollection DataPorts会正常工作:
<DataGrid Name="dataGrid" ItemsSource="{Binding DataPorts}">
而绑定到ICollectionView CollectionDataPort时不起作用?所以我只能在提供的代码中进行更改,然后它正常工作。只有当我绑定到ICollectionView CollectionDataPort时才无法工作。
为什么会发生这种情况?这种方式在WPF应用程序中处理线程是否有缺陷?非常感谢任何帮助。
我已经翻译了您提供的代码。如果您需要进一步的帮助或有其他问题,请随时告诉我。
<details>
<summary>英文:</summary>
I'm working on a WPF application and have encountered an issue with using ICollectionView to add items to a ObservableCollection<VMDataPorts>. I have a IOPortsViewModel class that contains an ObservableCollection<VMDataPorts> called DataPorts. In the constructor of this class, I create an instance of ICollectionView called CollectionDataPorts using CollectionViewSource.GetDefaultView(DataPorts).
In my MainWindowViewModel class, I have a method called ReloadMainTreeViewSeparateThread() that creates some new MenuItem objects in a separate worker thread and adds them to an ObservableCollection<MenuItem> called MenuItemsLeft which is defined on UI thread, trough Dispatcher. This works fine.
However, when I try to add items to CollectionDataPorts from the UI thread, I get the following error:
System.NotSupportedException: 'This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread.'
If I bind directly to DataPorts instead of CollectionDataPorts in my IOPortsView, everything works fine.
Here's my code for reference:
internal class IOPortsViewModel : ObservableObject
{
public IOPortsViewModel(object obj)
{
//Do something with obj
CollectionDataPorts = CollectionViewSource.GetDefaultView(DataPorts);
}
private ICollectionView m_collectionDataPorts;
public ICollectionView CollectionDataPorts
{
get => m_collectionDataPorts;
set => SetProperty(ref m_collectionDataPorts, value);
}
private ObservableCollection<VMDataPorts> m_dataPorts;
public ObservableCollection<VMDataPorts> DataPorts
{
get => m_dataPorts;
set => SetProperty(ref m_dataPorts, value);
}
}
IOPortsView:
<DataGrid Name="dataGrid" ItemsSource="{Binding CollectionDataPorts}">
internal class MainWindowViewModel : CommunityToolkit.Mvvm.ComponentModel.ObservableObject
{
ObservableCollection<MenuItem> MenuItemsLeft = new();
public void ReloadMainTreeViewSeparateThread()
{
Thread t = new Thread(() =>
{
App.Current.Dispatcher.Invoke(() => { IsDialogLoadingOpen = true; });
try
{
//some heavy workload which is required to create ViewModels and MenuItems below
object objFromBackend = Backend.GetSomeExpensiveObject();
ObservableCollection<MenuItem> NewProcessedLeftMenuTree = new()
{
new MenuItem ( new IOPortsViewModel(objFromBackend), "Title"),
};
//Adding menuItems into UI thread which were created on this worker thread (Thread t)
App.Current.Dispatcher.Invoke(()=>
{
foreach (MenuItem menuItem in NewProcessedLeftMenuTree)
{
MenuItemsLeft.Add(menuItem);
}
});
}
catch (AggregateException aggrEx)
{
App.Current.Dispatcher.BeginInvoke(() =>
{
IsDialogLogOpen = true;
});
}
finally
{
App.Current.Dispatcher.Invoke(() =>
{
IsDialogLoadingOpen = false;
});
}
});
t.SetApartmentState(ApartmentState.STA);
t.Start();
}
private bool m_isDialogLoadingOpen;
public bool IsDialogLoadingOpen
{
get => m_isDialogLoadingOpen;
set => SetProperty(ref m_isDialogLoadingOpen, value);
}
private bool m_isDialogLogOpen;
public bool IsDialogLogOpen
{
get => m_isDialogLogOpen;
set => SetProperty(ref m_isDialogLogOpen, value);
}
}
Why it works when I bind directly to ObservableCollection DataPorts in IOPortsView :
`<DataGrid Name="dataGrid" ItemsSource="{Binding DataPorts}">` ? So I can only change this in provided code and then it works. It only doesn't work when I bind to ICollectionView CollectionDataPort.
Why does this happen? Is this way of working with threads in a WPF application flawed? Any help would be greatly appreciated.
EDIT:
internal class MenuItem : CommunityToolkit.Mvvm.ComponentModel.ObservableObject, IEnumerable<MenuItem>
{
public PackIconKind? Icon { get; set; } = PackIconKind.None;
public PackIconKind? IconStatus { get; } = PackIconKind.None;
public ObservableCollection<MenuItem> Children { get; set; } = new ObservableCollection<MenuItem>();
public MenuItem? Parent { get; set; }
public ObservableObject ViewModel { get; }
private bool m_isExpanded;
public bool IsExpanded
{
get => m_isExpanded;
set => SetProperty(ref m_isExpanded, value);
}
private bool m_isSelected;
public bool IsSelected
{
get => m_isSelected;
set => SetProperty(ref m_isSelected, value);
}
private bool m_visibility = true;
public bool Visibility
{
get => m_visibility;
set => SetProperty(ref m_visibility, value);
}
public MenuItem(ObservableObject viewModel, string title, MenuItem? parent, PackIconKind icon)
: this(viewModel, title, parent)
{
Icon = icon;
}
public MenuItem(ObservableObject? viewModel, string title, MenuItem? parent, ObservableCollection<MenuItem> children, PackIconKind icon)
: this(viewModel, title, parent)
{
Children = children;
Icon = icon;
}
public MenuItem(ObservableObject viewModel, string title, MenuItem? parent)
{
ViewModel = viewModel;
Parent = parent;
//Title = title;
if (viewModel is not null)
{
viewModel.Title = title;
}
}
public void Add(MenuItem child)
{
Children.Add(child);
}
public ICommand SaveButtonCommand => new RelayCommand<object>(SaveButtonCommandFx, (object? x) => { return true; });
private void SaveButtonCommandFx(object? obj)
{
if (ViewModel is RuleViewModel rule)
{
rule.ActiveCheck = !rule.ActiveCheck;
}
}
public void SelectAndFocus()
{
var ancestors = this.GetAncestors();
ancestors.ToList().ForEach(an => an.IsExpanded = true);
IsSelected = true;
IsExpanded = true;
}
public IEnumerator<MenuItem> GetEnumerator()
{
return Children.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return Children.GetEnumerator();
}
public IEnumerable<MenuItem> Descendants()
{
var nodes = new Stack<MenuItem>(new[] { this });
while (nodes.Any())
{
MenuItem node = nodes.Pop();
yield return node;
foreach (var n in node.Children) nodes.Push(n);
}
}
public IEnumerable<MenuItem> GetAncestors()
{
MenuItem parent = this.Parent;
while (parent != null)
{
yield return parent;
parent = parent.Parent;
}
}
}
</details>
# 答案1
**得分**: 1
您遇到的错误是因为 ICollectionView 实例 CollectionDataPorts 与 UI 绑定,因此必须在 UI 线程上进行修改。在您的代码中,您尝试从单独的工作线程修改 CollectionDataPorts,这是不允许的,并导致了您提到的 NotSupportedException 错误。
要解决此问题,您应确保对 CollectionDataPorts 的修改在 UI 线程上执行。您可以使用 Dispatcher 来在正确的线程上调用这些修改。
我将 DataPorts 集合的初始化移到了 IOPortsViewModel 的构造函数中。然后,在创建 newProcessedLeftMenuTree 时,我访问了 IOPortsViewModel 实例,并在 UI 线程上更新了它的 CollectionDataPorts 属性。
通过在 UI 线程上执行修改,您可以避免 NotSupportedException,并确保 ICollectionView 得到正确更新。
编辑:
当您直接绑定到 ObservableCollection DataPorts 时,DataGrid 在集合中添加、删除或修改项时会收到通知。WPF 的数据绑定系统会自动处理这些通知并相应地更新 UI。
另一方面,当您绑定到 ICollectionView CollectionDataPorts 时,DataGrid 绑定到集合的视图而不是集合本身。ICollectionView 提供排序、过滤和分组功能,充当底层数据集合(DataPorts)和 UI 之间的中介。
在您的原始代码中,当您尝试从单独的工作线程修改 CollectionDataPorts 时,由于 ICollectionView 不是为在与创建它的线程不同的线程上处理修改而设计的(在这种情况下是 UI 线程),因此会遇到 NotSupportedException。
要使它与 ICollectionView 配合工作,您需要确保对底层 ObservableCollection(DataPorts)的任何修改都在 UI 线程上执行。在更新的代码中,我们使用 Dispatcher.Invoke 方法在 UI 线程上执行 DataPorts 的修改并更新 CollectionDataPorts 属性。
或者,如果您只需要基本功能而不需要排序、过滤或分组,您可以继续直接将 ObservableCollection DataPorts 绑定到 DataGrid,并避免使用 ICollectionView 的需求。
<details>
<summary>英文:</summary>
The error you're encountering occurs because the ICollectionView instance, CollectionDataPorts, is bound to the UI and therefore must be modified on the UI thread. In your code, you're trying to modify CollectionDataPorts from a separate worker thread, which is not allowed and results in the NotSupportedException you mentioned.
To resolve this issue, you should ensure that modifications to the CollectionDataPorts are performed on the UI thread. You can use the Dispatcher to invoke the modifications on the correct thread.
internal class IOPortsViewModel : ObservableObject
{
public IOPortsViewModel()
{
DataPorts = new ObservableCollection<VMDataPorts>();
CollectionDataPorts = CollectionViewSource.GetDefaultView(DataPorts);
}
private ICollectionView m_collectionDataPorts;
public ICollectionView CollectionDataPorts
{
get => m_collectionDataPorts;
set => SetProperty(ref m_collectionDataPorts, value);
}
private ObservableCollection<VMDataPorts> m_dataPorts;
public ObservableCollection<VMDataPorts> DataPorts
{
get => m_dataPorts;
set => SetProperty(ref m_dataPorts, value);
}
}
internal class MainWindowViewModel :
CommunityToolkit.Mvvm.ComponentModel.ObservableObject
{
private ObservableCollection<MenuItem> menuItemsLeft = new();
public ObservableCollection<MenuItem> MenuItemsLeft
{
get => menuItemsLeft;
set => SetProperty(ref menuItemsLeft, value);
}
public void ReloadMainTreeViewSeparateThread()
{
Thread t = new Thread(() =>
{
App.Current.Dispatcher.Invoke(() => { IsDialogLoadingOpen = true; });
try
{
ObservableCollection<MenuItem> newProcessedLeftMenuTree = new()
{
new MenuItem(new IOPortsViewModel(), "Title"),
};
App.Current.Dispatcher.Invoke(() =>
{
foreach (MenuItem menuItem in newProcessedLeftMenuTree)
{
MenuItemsLeft.Add(menuItem);
}
var ioPortsViewModel = newProcessedLeftMenuTree[0].ViewModel as IOPortsViewModel;
if (ioPortsViewModel != null)
{
ioPortsViewModel.CollectionDataPorts = CollectionViewSource.GetDefaultView(ioPortsViewModel.DataPorts);
}
});
}
catch (AggregateException aggrEx)
{
App.Current.Dispatcher.BeginInvoke(() =>
{
IsDialogLogOpen = true;
});
}
finally
{
App.Current.Dispatcher.Invoke(() =>
{
IsDialogLoadingOpen = false;
});
}
});
t.SetApartmentState(ApartmentState.STA);
t.Start();
}
}
I moved the initialization of the DataPorts collection to the constructor of IOPortsViewModel. Then, when creating the newProcessedLeftMenuTree, I access the IOPortsViewModel instance and update its CollectionDataPorts property on the UI thread.
By performing the modification on the UI thread, you avoid the NotSupportedException and ensure that the ICollectionView is updated correctly.
EDIT:
When you bind directly to the ObservableCollection DataPorts, the DataGrid receives notifications whenever items are added, removed, or modified in the collection. WPF's data binding system automatically handles these notifications and updates the UI accordingly.
On the other hand, when you bind to the ICollectionView CollectionDataPorts, the DataGrid is bound to a view of the collection rather than the collection itself. The ICollectionView provides sorting, filtering, and grouping capabilities, and it acts as an intermediary between the underlying data collection (DataPorts) and the UI.
In your original code, when you attempted to modify the CollectionDataPorts from a separate worker thread, you encountered the NotSupportedException because the ICollectionView was not designed to handle modifications from a different thread than the one it was created on (in this case, the UI thread).
To make it work with the ICollectionView, you need to ensure that any modifications to the underlying ObservableCollection (DataPorts) are performed on the UI thread. In the updated code, we used the Dispatcher.Invoke method to execute the modification of DataPorts and updating the CollectionDataPorts property on the UI thread.
Alternatively, if you only need basic functionality without sorting, filtering, or grouping, you can continue binding directly to the ObservableCollection DataPorts in your DataGrid and avoid the need for the ICollectionView.
</details>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论