Где я могу получить потокобезопасный CollectionView? - PullRequest
64 голосов
/ 26 января 2010

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

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

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

Ответы [ 11 ]

87 голосов
/ 16 декабря 2010

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

System.Windows.Application.Current.Dispatcher.Invoke(
    System.Windows.Threading.DispatcherPriority.Normal,
    (Action)delegate() 
    {
         // Your Action Code
    });
64 голосов
/ 04 сентября 2012

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

17 голосов
/ 26 января 2010

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

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

К сожалению, этот код приводит к исключению: «NotSupportedException - этот тип CollectionView не поддерживает изменения его SourceCollection из потока, отличного от потока Dispatcher». Я понимаю, что это сообщение об ошибке заставляет людей думать, что если CollectionView они используют не поддерживает изменения между потоками, тогда они должны найти тот, который делает. Что ж, это сообщение об ошибке немного вводит в заблуждение: ни один из представлений CollectionView, которые мы предоставляем из коробки, не поддерживает изменения коллекции между потоками. И нет, к сожалению, мы не можем исправить сообщение об ошибке на этом этапе, мы очень сильно заблокированы.

7 голосов
/ 26 января 2010

Найден один.

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

3 голосов
/ 24 июня 2013

Вы также можете посмотреть на: BindingOperations.EnableCollectionSynchronization.

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

2 голосов
/ 03 апреля 2014

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

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

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

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

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

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

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

1 голос
/ 26 января 2012

Если вы хотите периодически обновлять элемент управления WPF UI Control и одновременно использовать пользовательский интерфейс, вы можете использовать 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;
            }
        }   

    }
0 голосов
/ 18 августа 2016

Небольшая ошибка в версии VB. Просто замените:

Dim obj As DispatcherObject = invocation.Target

По

Dim obj As DispatcherObject = TryCast(invocation.Target, DispatcherObject)
0 голосов
/ 17 декабря 2013

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

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

  'from: /1746578/gde-ya-mogu-poluchit-potokobezopasnyi-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
0 голосов
/ 14 марта 2013

Попробуйте:

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

 //Code

}));
...