68

在后台线程上更新业务对象集合时,我收到以下错误消息:

这种类型的 CollectionView 不支持从不同于 Dispatcher 线程的线程更改其 SourceCollection。

好的,这是有道理的。但它也引出了一个问题,CollectionView 的哪个版本支持多线程以及如何让我的对象使用它?

4

12 回答 12

88

采用:

System.Windows.Application.Current.Dispatcher.Invoke(
    System.Windows.Threading.DispatcherPriority.Normal,
    (Action)delegate() 
    {
         // Your Action Code
    });
于 2010-12-16T14:18:52.333 回答
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,所以通知的更改可能在调用处理程序之前被撤消。这通常会导致“索引超出范围”。根据列表的新(更改的)状态检查事件参数时引发异常。为了避免这种情况,所有延迟事件都替换为重置事件。在某些情况下,这可能会导致过度重绘。

于 2012-09-04T00:32:27.913 回答
17

Bea Stollnitz 的这篇文章解释了错误消息以及为什么它的措辞如此。

编辑:来自 Bea 的博客

不幸的是,此代码导致异常:“NotSupportedException - 这种类型的 CollectionView 不支持从不同于 Dispatcher 线程的线程对其 SourceCollection 的更改。” 我知道这个错误信息让人们认为,如果他们使用的 CollectionView 不支持跨线程更改,那么他们必须找到支持的那个。好吧,这个错误消息有点误导:我们提供的开箱即用的 CollectionView 都不支持跨线程集合更改。不,很遗憾,我们目前无法修复错误消息,我们非常锁定。

于 2010-01-26T07:04:10.380 回答
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

于 2010-01-26T06:48:38.363 回答
3

你也可以看看:BindingOperations.EnableCollectionSynchronization

请参阅升级到 .NET 4.5:ItemsControl 与其项目源不一致

于 2013-06-24T12:23:24.510 回答
2

抱歉,无法添加评论,但这一切都是错误的。

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:

快速执行和线程安全的可观察集合

于 2014-04-03T14:00:17.943 回答
2

您可以通过启用集合同步来让 wpf 管理对集合的跨线程更改,如下所示:

BindingOperations.EnableCollectionSynchronization(collection, syncLock);
listBox.ItemsSource = collection;

这告诉 WPF 集合可能会在 UI 线程之外进行修改,因此它知道它必须将任何 UI 更改编组回适当的线程。

如果您没有锁定对象,还有一个重载来提供同步回调。

于 2019-08-12T05:32:52.040 回答
1

如果您想定期更新 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;
            }
        }   

    }
于 2012-01-26T16:29:57.447 回答
1

试试这个:

this.Dispatcher.Invoke(DispatcherPriority.Background, new Action(
() =>
{

 //Code

}));
于 2013-03-14T11:57:21.070 回答
0

它们都没有,只需使用 Dispatcher.BeginInvoke

于 2010-01-26T06:15:34.033 回答
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
于 2013-12-17T10:04:51.870 回答
0

VB版的小错误。只需更换:

Dim obj As DispatcherObject = invocation.Target

经过

Dim obj As DispatcherObject = TryCast(invocation.Target, DispatcherObject)
于 2016-08-18T09:03:20.663 回答