在后台线程上更新业务对象的集合时,出现以下错误消息:
这种类型的CollectionView不支持从与Dispatcher线程不同的线程对其SourceCollection进行更改。
好的,那很有道理。但这也引出了一个问题,哪个版本的CollectionView支持多线程,如何使我的对象使用它?
Answers:
以下是乔纳森发现的实现的改进。首先,它在与其关联的调度程序上运行每个事件处理程序,而不是假设它们都在同一(UI)调度程序上。其次,它使用BeginInvoke允许处理在我们等待调度程序可用之前继续进行。如果后台线程在每个线程之间进行大量更新和处理的情况下,这将使解决方案更快。也许更重要的是,它克服了等待调用时因阻塞而导致的问题(例如,将WCF与ConcurrencyMode.Single一起使用时,可能会发生死锁)。
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,所以有可能在调用处理程序之前撤消通知的更改。这通常会导致“索引超出范围”。根据列表的新(更改)状态检查事件参数时引发的异常。为了避免这种情况,将所有延迟事件替换为Reset事件。在某些情况下,这可能导致过多的重画。
采用:
System.Windows.Application.Current.Dispatcher.Invoke(
System.Windows.Threading.DispatcherPriority.Normal,
(Action)delegate()
{
// Your Action Code
});
Invoke
在UI冻结中使用结果。使用BeginInvoke
代替。
Bea Stollnitz的这篇文章解释了该错误消息以及其字样的原由。
编辑:从Bea的博客
不幸的是,此代码导致异常:“ NotSupportedException –这种类型的CollectionView不支持从与Dispatcher线程不同的线程对其SourceCollection进行更改。” 我了解到此错误消息使人们认为,如果他们使用的CollectionView不支持跨线程更改,那么他们必须找到能做到这一点的代码。嗯,此错误消息有点误导性:我们提供的现成的CollectionViews都不支持跨线程集合更改。而且,不幸的是,目前我们无法修复错误消息,我们非常受限制。
找到一个。
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
dispatcher.Invoke
,即在GUI线程上执行某些操作。这意味着两件事:1.工作者线程每次向集合添加内容时都必须停止并等待GUI线程。任务切换非常昂贵,并且会降低性能。2. GUI线程可能会阻塞导致GUI不响应的工作量。可以在此处stackoverflow.com/a/4530900/157224找到针对类似问题的基于计时器的解决方案。
您还可以查看:BindingOperations.EnableCollectionSynchronization
。
抱歉,无法添加评论,但所有这些都是错误的。
ObservableCollection不是线程安全的。不仅因为此调度程序问题,而且(从msdn)它根本不是线程安全的:
此类型的任何公共静态(在Visual Basic中为Shared)成员都是线程安全的。不保证任何实例成员都是线程安全的。
在这里看 http://msdn.microsoft.com/zh-cn/library/ms668604(v=vs.110).aspx
使用“重置”操作调用BeginInvoke时也存在问题。“重置”是处理程序应查看集合本身的唯一操作。如果您开始BeginInvoke一个“重置”,然后立即开始BeginInvoke几个“添加”动作,则处理程序将接受具有已更新集合的“重置”,而下一个“添加”将造成混乱。
这是我可行的实现。实际上,我正在考虑完全删除BeginInvoke:
如果您想定期更新WPF UI控件并同时使用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
}));
这是我在进行一些谷歌搜索和少量修改后制作的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