wake-up-neo.net

Woher bekomme ich eine thread-sichere CollectionView?

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

Dieser CollectionView-Typ unterstützt keine Änderungen an seiner SourceCollection von einem anderen Thread als dem Dispatcher-Thread.

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

64
Jonathan Allen

Das Folgende ist eine Verbesserung der von Jonathan gefundenen Implementierung. Erstens führt es jeden Event-Handler auf dem zugeordneten Dispatcher aus, anstatt davon auszugehen, dass sich alle auf demselben Dispatcher (UI) befinden. Zweitens verwendet es BeginInvoke, um die Verarbeitung fortzusetzen, während wir warten, bis der Dispatcher verfügbar wird. Dies macht die Lösung in Situationen, in denen der Hintergrund-Thread viele Aktualisierungen durchführt, viel schneller. Vielleicht noch wichtiger ist es, Probleme zu beheben, die durch das Blockieren beim Warten auf das Invoke verursacht werden (Deadlocks können beispielsweise bei der Verwendung von WCF mit ConcurrencyMode.Single auftreten).

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 benachrichtigte Änderung rückgängig gemacht wird, bevor der Handler aufgerufen wird. Dies würde normalerweise dazu führen, dass "der Index außerhalb des Bereichs lag". Eine Ausnahme wird ausgelöst, wenn die Ereignisargumente gegen den neuen (geänderten) Status der Liste geprüft 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.

64
Nathan Phillips

Benutzen:

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

Dieser Post von Bea Stollnitz erklärt die Fehlermeldung that und warum es so formuliert ist.

EDIT: Aus Bea's Blog

Leider führt dieser Code zu einer Ausnahme: "NotSupportedException - Dieser Typ von CollectionView unterstützt keine Änderungen an seiner SourceCollection von einem anderen Thread als dem Dispatcher-Thread." Ich verstehe, dass diese Fehlermeldung den Leuten denkt, wenn das CollectionView ist Wenn keine Cross-Thread-Änderungen verwendet werden, müssen sie diejenige finden, die dies tut. Nun, diese Fehlermeldung ist etwas irreführend: Keine der von uns mitgelieferten CollectionViews unterstützt Änderungen der Cross-Thread-Erfassung. Und nein, leider können wir die Fehlermeldung an dieser Stelle nicht beheben, wir sind sehr gesperrt.

17

Fand einen.

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

7
Jonathan Allen

Sie können auch Folgendes anzeigen: BindingOperations.EnableCollectionSynchronization.

Siehe Upgrade auf .NET 4.5: Ein ItemsControl ist inkonsistent mit seiner Elementquelle

3
Richard

Sie können mit wpf Thread-übergreifende Änderungen an einer Sammlung verwalten, indem Sie die Sammlungssynchronisierung wie folgt aktivieren:

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

Dies teilt WPF mit, dass die Auflistung außerhalb des UI-Threads geändert werden kann, sodass bekannt ist, dass Änderungen an der Benutzeroberfläche wieder in dem entsprechenden Thread gespeichert werden müssen.

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

2
Hamish

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

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

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

Schauen Sie hier http://msdn.Microsoft.com/de-de/library/ms668604(v=vs.110).aspx

Es gibt auch ein Problem, wenn BeginInvoke mit der Aktion "Zurücksetzen" aufgerufen wird. "Zurücksetzen" ist die einzige Aktion, bei der der Handler die Sammlung selbst betrachten soll. Wenn Sie mit BeginInvoke ein "Reset" durchführen und sofort BeginInvoke ein paar "Add" - Aktionen ausführen, akzeptiert der Handler ein "Reset" mit bereits aktualisierter Sammlung und das nächste "Add" erstellt ein Durcheinander.

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

Schnelle und Thread-sichere beobachtbare Sammlung

2
norekhov

Wenn Sie die WPF-UI-Steuerung 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;
            }
        }   

    }

Hier ist eine VB -Version, die ich nach ein paar guten 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
0
Peter pete

Kleiner Fehler in der VB Version. Ersetzen Sie einfach:

Dim obj As DispatcherObject = invocation.Target

Durch 

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

Keiner von ihnen, verwenden Sie einfach Dispatcher.BeginInvoke

0
Paul Betts

Versuche dies:

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

 //Code

}));
0