Woher bekomme ich eine thread-sichere CollectionView?

68

Beim Aktualisieren einer Sammlung von Geschäftsobjekten in einem Hintergrundthread wird folgende Fehlermeldung angezeigt:

Diese Art von CollectionView unterstützt keine Änderungen an der SourceCollection von einem Thread, der sich vom Dispatcher-Thread unterscheidet.

Ok, das macht Sinn. Es stellt sich aber auch die Frage, welche Version von CollectionView mehrere Threads unterstützt und wie ich meine Objekte dazu bringe, sie zu verwenden.

Jonathan Allen
quelle
1
Versuchen Sie den folgenden Link, der eine thread-sichere Lösung bietet, die von jedem Thread aus funktioniert und über mehrere UI-Threads gebunden werden kann: codeproject.com/Articles/64936/…
Anthony

Antworten:

64

Das Folgende ist eine Verbesserung gegenüber der von Jonathan gefundenen Implementierung. Zunächst wird jeder Ereignishandler auf dem ihm zugeordneten Dispatcher ausgeführt, anstatt davon auszugehen, dass sich alle auf demselben Dispatcher (UI) befinden. Zweitens wird BeginInvoke verwendet, damit die Verarbeitung fortgesetzt werden kann, während wir darauf warten, dass der Dispatcher verfügbar wird. Dies macht die Lösung in Situationen, in denen der Hintergrund-Thread viele Aktualisierungen mit der Verarbeitung zwischen den einzelnen Threads durchführt, viel schneller. Vielleicht noch wichtiger ist, dass es Probleme überwindet, die durch Blockieren während des Wartens auf den Aufruf verursacht werden (Deadlocks können beispielsweise auftreten, wenn WCF mit ConcurrencyMode.Single verwendet wird).

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);
            }
    }
}

Da wir BeginInvoke verwenden, ist es möglich, dass die zu benachrichtigende Änderung rückgängig gemacht wird, bevor der Handler aufgerufen wird. Dies würde normalerweise zu einem "Index außerhalb des Bereichs" führen. Ausnahme wird ausgelöst, wenn die Ereignisargumente mit dem neuen (geänderten) Status der Liste verglichen werden. Um dies zu vermeiden, werden alle verzögerten Ereignisse durch Reset-Ereignisse ersetzt. Dies kann in einigen Fällen zu übermäßigem Neuzeichnen führen.

Nathan Phillips
quelle
1
Etwas spät und ein altes Thema, aber dieser Code hat mir viele Kopfschmerzen erspart, danke! :)
KingTravisG
Caliburn hat auch eine wirklich schöne Implementierung in seiner BindableCollection <T>. Werfen
Stephanvs
Ich erhalte eine Ausnahme mit dieser Version, aber nicht mit der von Jonathan bereitgestellten Version. Hat jemand Ideen, warum dies geschieht? Hier ist meine InnerException: Diese Ausnahme wurde ausgelöst, weil der Generator für die Steuerung 'System.Windows.Controls.DataGrid Items.Count: 3' mit dem Namen 'OrdersGrid' eine Folge von CollectionChanged-Ereignissen empfangen hat, die nicht mit dem aktuellen Status der Items übereinstimmen Sammlung. Die folgenden Unterschiede wurden festgestellt: Die akkumulierte Anzahl 2 unterscheidet sich von der tatsächlichen Anzahl 3. [Die akkumulierte Anzahl ist (Anzahl beim letzten Zurücksetzen + #Adds - #Entfernt seit dem letzten Zurücksetzen).
SoftwareFactor
@ Nathan Phillips Ich weiß, dass ich ein Jahr zu spät zu diesem Thread komme, aber ich verwende Ihre MTObservableCollection-Implementierung und sie funktioniert ziemlich gut. In seltenen Fällen werde ich diesen Index jedoch zeitweise außerhalb der Bereichsausnahme erhalten. Haben Sie eine Idee, warum dies zeitweise passieren würde?
Das funktioniert super und erspart mir viele Probleme. Ich benutze es seit Monaten und wollte meine Erfahrungen damit teilen. Das einzige kleine Problem, mit dem ich Probleme habe, ist, dass der Dispatcher so ziemlich immer läuft, wenn er möchte. Wenn ich die Sammlung abfrage, kurz nachdem sie gelegentlich leer ist oder alle Artikel noch nicht in der Sammlung sind. Immer noch ziemlich selten. Ich brauchte eine 100% fehlerfreie Version, also habe ich eine Klasse erstellt, die die Sammlung abruft, und diese Klasse hat einen Thread-Ruhezustand von einer Zehntelsekunde, und seitdem ist kein Fehler aufgetreten.
Franck
87

Verwenden:

System.Windows.Application.Current.Dispatcher.Invoke(
    System.Windows.Threading.DispatcherPriority.Normal,
    (Action)delegate() 
    {
         // Your Action Code
    });
Luke
quelle
Einfach, elegant, direkt auf den Punkt, liebe es ... Ich habe es für immer gelernt. Vielen Dank.
Patrice Calvé
8
Verwenden von InvokeErgebnissen beim Einfrieren der Benutzeroberfläche. Verwenden Sie BeginInvokestattdessen.
Xaqron
1
@MonsterMMORPG Diese Lösung mit .BeginInvoke statt .Invoke ist eine gute Antwort.
Amaranth
17

Dieser Beitrag von Bea Stollnitz erklärt diese Fehlermeldung und warum sie so formuliert ist, wie sie ist.

EDIT: Aus Bea's Blog

Leider führt dieser Code zu einer Ausnahme: "NotSupportedException - Diese Art von CollectionView unterstützt keine Änderungen an der SourceCollection von einem Thread, der sich vom Dispatcher-Thread unterscheidet." Ich verstehe, dass diese Fehlermeldung die Leute zu der Annahme veranlasst, dass sie diejenige finden müssen, die dies tut, wenn die von ihnen verwendete CollectionView keine threadübergreifenden Änderungen unterstützt. Nun, diese Fehlermeldung ist ein wenig irreführend: Keine der von uns bereitgestellten CollectionViews unterstützt Änderungen der Thread-übergreifenden Sammlung. Und nein, leider können wir die Fehlermeldung an dieser Stelle nicht beheben, wir sind sehr gesperrt.

Cameron MacFarland
quelle
Ich mag die Implementierung von Mark besser, aber ich muss Ihnen die Ehre erweisen, die beste Erklärung zu finden.
Jonathan Allen
7

Ich habe einen gefunden.

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

Jonathan Allen
quelle
3
Beachten Sie, dass dies zu einem Threadwechsel für jede Sammlungsänderung führt und dass alle Änderungen serialisiert werden (was den Zweck von Hintergrundthreads zunichte macht :-)). Bei einigen Elementen spielt es keine Rolle, aber wenn Sie vorhaben, viele Elemente hinzuzufügen, wird die Leistung erheblich beeinträchtigt. Normalerweise füge ich die Elemente einer anderen Sammlung im Hintergrund-Thread hinzu und verschiebe sie dann auf einem Timer in die GUI-Sammlung.
Adrianm
1
Ich kann damit leben. Die Kosten, die ich vermeiden möchte, sind das Abrufen der Elemente, da dadurch die Benutzeroberfläche gesperrt wird. Das Hinzufügen zur Sammlung ist im Vergleich dazu billig.
Jonathan Allen
@adrianm Ich interessiere mich für Ihre Bemerkung: Was meinen Sie in diesem Fall mit "Serialisierung"? Und haben Sie ein Beispiel für "Mit einem Timer in die GUI-Sammlung wechseln"?
Gerard
Alle Änderungen an der Sammlung verursachen a dispatcher.Invoke, dh etwas im GUI-Thread. Dies bedeutet zwei Dinge: 1. Der Worker-Thread muss jedes Mal anhalten und auf den GUI-Thread warten, wenn er der Sammlung etwas hinzufügt. Das Wechseln von Aufgaben ist teuer und verringert die Leistung. 2. Der GUI-Thread drosselt möglicherweise den Arbeitsaufwand, der zu einer nicht reagierenden GUI führt. Eine zeitgesteuerte Lösung für ein ähnliches Problem finden Sie hier stackoverflow.com/a/4530900/157224 .
Adrianm
2

Entschuldigung, ich kann keinen Kommentar hinzufügen, aber das ist alles falsch.

ObservableCollection ist nicht threadsicher. Nicht nur wegen dieser Dispatcher-Probleme, sondern es ist überhaupt nicht threadsicher (von msdn):

Alle öffentlichen statischen (in Visual Basic freigegebenen) Mitglieder dieses Typs sind threadsicher. Es wird nicht garantiert, dass Instanzmitglieder threadsicher sind.

Schauen Sie hier http://msdn.microsoft.com/en-us/library/ms668604(v=vs.110).aspx

Es gibt auch ein Problem beim Aufrufen von BeginInvoke mit einer Aktion "Zurücksetzen". "Zurücksetzen" ist die einzige Aktion, bei der der Handler die Sammlung selbst anzeigen sollte. Wenn Sie ein "Zurücksetzen" beginnen und dann sofort ein paar "Hinzufügen" -Aktionen starten, akzeptiert der Handler ein "Zurücksetzen" mit bereits aktualisierter Sammlung, und das nächste "Hinzufügen" führt zu einem Durcheinander.

Hier ist meine Implementierung, die funktioniert. Eigentlich denke ich daran, BeginInvoke überhaupt zu entfernen:

Schnelle und threadsichere beobachtbare Sammlung

Norekhov
quelle
2

Sie können wpf veranlassen, threadübergreifende Änderungen an einer Sammlung zu verwalten, indem Sie die Synchronisierung der Sammlung wie folgt aktivieren:

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

Dies teilt WPF mit, dass die Sammlung möglicherweise außerhalb des UI-Threads geändert wird, sodass bekannt ist, dass alle UI-Änderungen wieder in den entsprechenden Thread zurückgeführt werden müssen.

Es gibt auch eine Überlastung, um einen Synchronisationsrückruf bereitzustellen, wenn Sie kein Sperrobjekt haben.

Hamish
quelle
1

Wenn Sie die WPF-Benutzeroberflächensteuerung regelmäßig aktualisieren und gleichzeitig die Benutzeroberfläche verwenden möchten, können Sie DispatcherTimer verwenden .

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;
            }
        }   

    }
DmitryBoyko
quelle
Dies ist eine sehr gute Lösung, aber es gibt einen Fehler, Clark. Wenn Sie die Instanz des Timers erstellen, müssen Sie den Application Dispatcher an ihn übergeben, damit er funktioniert. Sie können dies im Konstruktor tun, indem Sie das System.Windows.Application.Current.Dispatcher-Objekt neben der Priorität übergeben!
Andry
1

Versuche dies:

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

 //Code

}));
Nalan Madheswaran
quelle
0

Keiner von ihnen, verwenden Sie einfach Dispatcher.BeginInvoke

Ana Betts
quelle
Dies macht den Zweck zunichte, Hintergrund-Threads und eine unabhängige Datenschicht zu haben.
Jonathan Allen
3
Nein, tut es nicht - die ganze Arbeit besteht darin, die Daten abzurufen / zu verarbeiten; Sie tun dies im Hintergrund-Thread und verschieben es dann mit Dispatcher.BeginInvoke in die Sammlung (was hoffentlich sehr wenig Zeit in Anspruch nimmt).
Ana Betts
0

Hier ist eine VB-Version, die ich nach einigem googeln und leichten Mods gemacht habe. Funktioniert bei mir.

  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
Peter Pete
quelle
0

Kleiner Fehler in der VB-Version. Einfach ersetzen:

Dim obj As DispatcherObject = invocation.Target

Durch

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