在后台线程上更新业务对象集合时,我收到以下错误消息:
这种类型的 CollectionView 不支持从不同于 Dispatcher 线程的线程更改其 SourceCollection。
好的,这是有道理的。但它也引出了一个问题,CollectionView 的哪个版本支持多线程以及如何让我的对象使用它?
在后台线程上更新业务对象集合时,我收到以下错误消息:
这种类型的 CollectionView 不支持从不同于 Dispatcher 线程的线程更改其 SourceCollection。
好的,这是有道理的。但它也引出了一个问题,CollectionView 的哪个版本支持多线程以及如何让我的对象使用它?
采用:
System.Windows.Application.Current.Dispatcher.Invoke(
System.Windows.Threading.DispatcherPriority.Normal,
(Action)delegate()
{
// Your Action Code
});
以下是对 Jonathan 发现的实现的改进。首先,它在与其关联的调度程序上运行每个事件处理程序,而不是假设它们都在同一个(UI)调度程序上。其次,它使用 BeginInvoke 允许在我们等待调度程序可用时继续处理。这使得解决方案在后台线程进行大量更新并在每个更新之间进行处理的情况下更快。也许更重要的是,它克服了在等待 Invoke 时因阻塞而导致的问题(例如,在使用带有 ConcurrencyMode.Single 的 WCF 时可能发生死锁)。
public class MTObservableCollection<T> : ObservableCollection<T>
{
public override event NotifyCollectionChangedEventHandler CollectionChanged;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
NotifyCollectionChangedEventHandler CollectionChanged = this.CollectionChanged;
if (CollectionChanged != null)
foreach (NotifyCollectionChangedEventHandler nh in CollectionChanged.GetInvocationList())
{
DispatcherObject dispObj = nh.Target as DispatcherObject;
if (dispObj != null)
{
Dispatcher dispatcher = dispObj.Dispatcher;
if (dispatcher != null && !dispatcher.CheckAccess())
{
dispatcher.BeginInvoke(
(Action)(() => nh.Invoke(this,
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset))),
DispatcherPriority.DataBind);
continue;
}
}
nh.Invoke(this, e);
}
}
}
因为我们使用的是 BeginInvoke,所以通知的更改可能在调用处理程序之前被撤消。这通常会导致“索引超出范围”。根据列表的新(更改的)状态检查事件参数时引发异常。为了避免这种情况,所有延迟事件都替换为重置事件。在某些情况下,这可能会导致过度重绘。
Bea Stollnitz 的这篇文章解释了该错误消息以及为什么它的措辞如此。
编辑:来自 Bea 的博客
不幸的是,此代码导致异常:“NotSupportedException - 这种类型的 CollectionView 不支持从不同于 Dispatcher 线程的线程对其 SourceCollection 的更改。” 我知道这个错误信息让人们认为,如果他们使用的 CollectionView 不支持跨线程更改,那么他们必须找到支持的那个。好吧,这个错误消息有点误导:我们提供的开箱即用的 CollectionView 都不支持跨线程集合更改。不,很遗憾,我们目前无法修复错误消息,我们非常锁定。
找到了一个。
public class MTObservableCollection<T> : ObservableCollection<T>
{
public override event NotifyCollectionChangedEventHandler CollectionChanged;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
var eh = CollectionChanged;
if (eh != null)
{
Dispatcher dispatcher = (from NotifyCollectionChangedEventHandler nh in eh.GetInvocationList()
let dpo = nh.Target as DispatcherObject
where dpo != null
select dpo.Dispatcher).FirstOrDefault();
if (dispatcher != null && dispatcher.CheckAccess() == false)
{
dispatcher.Invoke(DispatcherPriority.DataBind, (Action)(() => OnCollectionChanged(e)));
}
else
{
foreach (NotifyCollectionChangedEventHandler nh in eh.GetInvocationList())
nh.Invoke(this, e);
}
}
}
}
http://www.julmar.com/blog/mark/2009/04/01/AddingToAnObservableCollectionFromABackgroundThread.aspx
你也可以看看:BindingOperations.EnableCollectionSynchronization
。
抱歉,无法添加评论,但这一切都是错误的。
ObservableCollection 不是线程安全的。不仅因为这个调度程序问题,而且它根本不是线程安全的(来自 msdn):
此类型的任何公共静态(在 Visual Basic 中为 Shared)成员都是线程安全的。不保证任何实例成员都是线程安全的。
看这里 http://msdn.microsoft.com/en-us/library/ms668604(v=vs.110).aspx
使用“重置”操作调用 BeginInvoke 时也会出现问题。“重置”是处理程序应该查看集合本身的唯一操作。如果您 BeginInvoke 一个“Reset”然后立即 BeginInvoke 几个“Add”操作,那么处理程序将接受一个具有已更新集合的“Reset”,并且下一个“Add”将造成混乱。
这是我的实现。实际上,我正在考虑完全删除 BeginInvoke:
您可以通过启用集合同步来让 wpf 管理对集合的跨线程更改,如下所示:
BindingOperations.EnableCollectionSynchronization(collection, syncLock);
listBox.ItemsSource = collection;
这告诉 WPF 集合可能会在 UI 线程之外进行修改,因此它知道它必须将任何 UI 更改编组回适当的线程。
如果您没有锁定对象,还有一个重载来提供同步回调。
如果您想定期更新 WPF UI Control 并同时使用 UI,您可以使用DispatcherTimer。
XAML
<Grid>
<DataGrid AutoGenerateColumns="True" Height="200" HorizontalAlignment="Left" Name="dgDownloads" VerticalAlignment="Top" Width="548" />
<Label Content="" Height="28" HorizontalAlignment="Left" Margin="0,221,0,0" Name="lblFileCouner" VerticalAlignment="Top" Width="173" />
</Grid>
C#
public partial class DownloadStats : Window
{
private MainWindow _parent;
DispatcherTimer timer = new DispatcherTimer();
ObservableCollection<FileView> fileViewList = new ObservableCollection<FileView>();
public DownloadStats(MainWindow parent)
{
InitializeComponent();
_parent = parent;
Owner = parent;
timer.Interval = new TimeSpan(0, 0, 1);
timer.Tick += new EventHandler(timer_Tick);
timer.Start();
}
void timer_Tick(object sender, EventArgs e)
{
dgDownloads.ItemsSource = null;
fileViewList.Clear();
if (_parent.contentManagerWorkArea.Count > 0)
{
foreach (var item in _parent.contentManagerWorkArea)
{
FileView nf = item.Value.FileView;
fileViewList.Add(nf);
}
}
if (fileViewList.Count > 0)
{
lblFileCouner.Content = fileViewList.Count;
dgDownloads.ItemsSource = fileViewList;
}
}
}
试试这个:
this.Dispatcher.Invoke(DispatcherPriority.Background, new Action(
() =>
{
//Code
}));
它们都没有,只需使用 Dispatcher.BeginInvoke
这是我在谷歌搜索和轻微修改后制作的 VB 版本。为我工作。
Imports System.Collections.ObjectModel
Imports System.Collections.Specialized
Imports System.ComponentModel
Imports System.Reflection
Imports System.Windows.Threading
'from: http://stackoverflow.com/questions/2137769/where-do-i-get-a-thread-safe-collectionview
Public Class ThreadSafeObservableCollection(Of T)
Inherits ObservableCollection(Of T)
'from: http://geekswithblogs.net/NewThingsILearned/archive/2008/01/16/listcollectionviewcollectionview-doesnt-support-notifycollectionchanged-with-multiple-items.aspx
Protected Overrides Sub OnCollectionChanged(ByVal e As System.Collections.Specialized.NotifyCollectionChangedEventArgs)
Dim doit As Boolean = False
doit = (e.NewItems IsNot Nothing) AndAlso (e.NewItems.Count > 0)
doit = doit OrElse ((e.OldItems IsNot Nothing) AndAlso (e.OldItems.Count > 0))
If (doit) Then
Dim handler As NotifyCollectionChangedEventHandler = GetType(ObservableCollection(Of T)).GetField("CollectionChanged", BindingFlags.Instance Or BindingFlags.NonPublic).GetValue(Me)
If (handler Is Nothing) Then
Return
End If
For Each invocation As NotifyCollectionChangedEventHandler In handler.GetInvocationList
Dim obj As DispatcherObject = invocation.Target
If (obj IsNot Nothing) Then
Dim disp As Dispatcher = obj.Dispatcher
If (disp IsNot Nothing AndAlso Not (disp.CheckAccess())) Then
disp.BeginInvoke(
Sub()
invocation.Invoke(Me, New NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset))
End Sub, DispatcherPriority.DataBind)
Continue For
End If
End If
invocation.Invoke(Me, e)
Next
End If
End Sub
End Class
VB版的小错误。只需更换:
Dim obj As DispatcherObject = invocation.Target
经过
Dim obj As DispatcherObject = TryCast(invocation.Target, DispatcherObject)