我在哪里可以获得线程安全的CollectionView?

时间:2010-01-26 06:12:57

标签: .net wpf multithreading thread-safety collectionview

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

  

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

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

12 个答案:

答案 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)

Bea Stollnitz发表的

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:

Fast performing and thread safe observable collection

答案 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)