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

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

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

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

Ответ 1

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

Ответ 2

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

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

Ответ 3

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

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

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

Ответ 4

Найдено один.

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

Ответ 6

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

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

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

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

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

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

Быстрая и надежная наблюдаемая коллекция

Ответ 7

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

С#

 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

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

Ответ 9

Попробуйте следующее:

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

 //Code

}));

Ответ 10

Здесь версия 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

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

Dim obj As DispatcherObject = invocation.Target

Через

Dim obj As DispatcherObject = TryCast(invocation.Target, DispatcherObject)