在后台线程上更新业务对象的集合时,我收到以下错误消息:
这种类型的CollectionView不支持从与Dispatcher线程不同的线程更改其SourceCollection。
好的,这是有道理的。但它也引出了一个问题,什么版本的CollectionView支持多个线程,如何让我的对象使用它?
答案 0 :(得分:87)
使用:
System.Windows.Application.Current.Dispatcher.Invoke(
System.Windows.Threading.DispatcherPriority.Normal,
(Action)delegate()
{
// Your Action Code
});
答案 1 :(得分:64)
以下是对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,所以在调用处理程序之前,通知的更改可能会被撤消。这通常会导致“指数超出范围”。根据列表的新(已更改)状态检查事件参数时抛出异常。为了避免这种情况,所有延迟事件都将被重置事件替换。在某些情况下,这可能会导致过度重绘。
答案 2 :(得分:17)
This帖子解释了 错误信息及其原因。
编辑:来自Bea的博客
不幸的是,这段代码导致异常:“NotSupportedException - 这种类型的CollectionView不支持从与Dispatcher线程不同的线程更改其SourceCollection。”我理解这条错误消息会让人们认为,如果是CollectionView他们使用的不支持跨线程更改,然后他们必须找到那个。好吧,这个错误消息有点误导:我们提供的开箱即用的CollectionViews都不支持跨线程集合更改。不,不幸的是,我们无法在此时修复错误消息,我们已经被锁定了。
答案 3 :(得分:7)
找到一个。
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
答案 4 :(得分:3)
您还可以查看:BindingOperations.EnableCollectionSynchronization
。
请参阅Upgrading to .NET 4.5: An ItemsControl is inconsistent with its items source
答案 5 :(得分:2)
抱歉,无法添加评论,但这一切都是错误的。
ObservableCollection不是线程安全的。不仅因为这个调度程序问题,而且它根本不是线程安全的(来自msdn):
此类型的任何公共静态(在Visual Basic中为Shared)成员都是线程安全的。不保证任何实例成员都是线程安全的。
看这里http://msdn.microsoft.com/en-us/library/ms668604(v=vs.110).aspx
使用“重置”操作调用BeginInvoke时也存在问题。 “重置”是处理程序应该查看集合本身的唯一操作。如果您开始启动“重置”,然后立即开始启动几个“添加”操作,那么处理程序将接受已经更新的集合的“重置”,接下来“添加”会产生混乱。
这是我的实施工作。实际上我正在考虑删除BeginInvoke:
答案 6 :(得分:2)
通过启用集合同步,您可以通过wpf来管理对集合的跨线程更改:
BindingOperations.EnableCollectionSynchronization(collection, syncLock);
listBox.ItemsSource = collection;
这告诉WPF,可以从UI线程修改集合,以便知道必须将任何UI更改整理回适当的线程。
如果没有锁对象,则还有一个过载可提供同步回调。
答案 7 :(得分:1)
如果您想定期更新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;
}
}
}
答案 8 :(得分:0)
他们都没有,只需使用Dispatcher.BeginInvoke
答案 9 :(得分:0)
试试这个:
this.Dispatcher.Invoke(DispatcherPriority.Background, new Action(
() =>
{
//Code
}));
答案 10 :(得分:0)
这是我在一些谷歌搜索和轻微修改后制作的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
答案 11 :(得分:0)
VB版本中的小错误。只需替换:
Dim obj As DispatcherObject = invocation.Target
通过
Dim obj As DispatcherObject = TryCast(invocation.Target, DispatcherObject)