Где я могу получить потокобезопасный CollectionView?

При обновлении коллекции бизнес-объектов в фоновом потоке я получаю следующее сообщение об ошибке:

Этот тип CollectionView не поддерживает изменения в его SourceCollection из потока, отличного от потока Dispatcher.

Хорошо, это имеет смысл. Но также возникает вопрос: какая версия CollectionView поддерживает несколько потоков и как мне сделать так, чтобы мои объекты ее использовали?

11 ответов

Решение

Следующее является улучшением реализации, найденной Джонатаном. Во-первых, он запускает каждый обработчик событий в связанном с ним диспетчере, а не предполагает, что все они находятся в одном диспетчере (UI). Во-вторых, он использует BeginInvoke, чтобы продолжить обработку, пока мы ожидаем, что диспетчер станет доступным. Это значительно ускоряет решение в ситуациях, когда фоновый поток выполняет много обновлений с обработкой между ними. Возможно, более важно, что он преодолевает проблемы, вызванные блокировкой во время ожидания Invoke (взаимные блокировки могут возникать, например, при использовании 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, возможно, что уведомление об изменении отменяется до вызова обработчика. Это обычно приводит к тому, что "индекс выходит за пределы диапазона". генерируется исключение, когда аргументы события проверяются относительно нового (измененного) состояния списка. Чтобы избежать этого, все отложенные события заменяются событиями сброса. Это может вызвать чрезмерное перерисовывание в некоторых случаях.

Использование:

System.Windows.Application.Current.Dispatcher.Invoke(
    System.Windows.Threading.DispatcherPriority.Normal,
    (Action)delegate() 
    {
         // Your Action Code
    });

Это сообщение от Bea Stollnitz объясняет это сообщение об ошибке и почему оно так и есть.

РЕДАКТИРОВАТЬ: Из блога Беа

К сожалению, этот код приводит к исключению: "NotSupportedException - этот тип CollectionView не поддерживает изменения в его SourceCollection из потока, отличного от потока Dispatcher". Я понимаю, что это сообщение об ошибке заставляет людей думать, что если 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,

См. Обновление до.NET 4.5: ItemsControl несовместим с источником своих элементов.

Извините, не могу добавить комментарий, но все это неправильно.

ObservableCollection не является потокобезопасным. Не только из-за проблем с диспетчером, но и вовсе не является потокобезопасным (из msdn):

Любые открытые статические (Shared в Visual Basic) члены этого типа являются потокобезопасными. Любые члены экземпляра не гарантированно являются потокобезопасными.

Посмотрите здесь http://msdn.microsoft.com/en-us/library/ms668604(v=vs.110).aspx

Существует также проблема при вызове BeginInvoke с действием "Сброс". "Сброс" - единственное действие, когда обработчик должен смотреть на саму коллекцию. Если вы начинаете вызывать "Сброс", а затем сразу начинаете инициировать пару действий "Добавить", то обработчик примет "Сброс" с уже обновленной коллекцией, а последующие "Добавить" создадут беспорядок.

Вот моя реализация, которая работает. На самом деле я думаю об удалении BeginInvoke:

Быстродействующая и потокобезопасная наблюдаемая коллекция

Вы можете настроить wpf для управления межпотоковыми изменениями в коллекции, включив синхронизацию коллекции следующим образом:

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

Это сообщает WPF, что коллекция может быть изменена вне потока пользовательского интерфейса, поэтому он знает, что он должен маршалировать любые изменения пользовательского интерфейса обратно в соответствующий поток.

Также существует перегрузка для обеспечения обратного вызова синхронизации, если у вас нет объекта блокировки.

Попробуй это:

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

 //Code

}));

Если вы хотите периодически обновлять 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;
            }
        }   

    }

Вот версия для VB, которую я сделал после некоторых поисков в Google и небольших модов. Работает для меня.

  Imports System.Collections.ObjectModel
  Imports System.Collections.Specialized
  Imports System.ComponentModel
  Imports System.Reflection
  Imports System.Windows.Threading

  'from: http://stackru.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)

Ни один из них, просто используйте Dispatcher.BeginInvoke

Другие вопросы по тегам