Ich habe eine ObservableCollection<A> a_collection;
Die Sammlung enthält 'n' Gegenstände. Jeder Punkt A sieht folgendermaßen aus:
public class A : INotifyPropertyChanged
{
public ObservableCollection<B> b_subcollection;
Thread m_worker;
}
Grundsätzlich ist alles mit einer WPF-Listenansicht + einem Detailansicht-Steuerelement verbunden, das b_subcollection
das ausgewählte Element in einer separaten Listenansicht anzeigt (bidirektionale Bindungen, Aktualisierungen bei geänderten Eigenschaften usw.).
Das Problem trat für mich auf, als ich anfing, Threading zu implementieren. Die gesamte Idee war, dass der gesamte a_collection
Arbeitsthread verwendet wird, um "Arbeit zu erledigen" und dann die jeweiligen zu aktualisieren b_subcollections
und die GUI die Ergebnisse in Echtzeit anzeigen zu lassen.
Als ich es versuchte, bekam ich eine Ausnahme, die besagte, dass nur der Dispatcher-Thread eine ObservableCollection ändern kann und die Arbeit zum Stillstand kam.
Kann jemand das Problem erklären und wie man es umgeht?
Antworten:
Technisch gesehen besteht das Problem nicht darin, dass Sie die ObservableCollection über einen Hintergrundthread aktualisieren. Das Problem ist, dass die Sammlung in diesem Fall das CollectionChanged-Ereignis für denselben Thread auslöst, der die Änderung verursacht hat. Dies bedeutet, dass Steuerelemente von einem Hintergrundthread aktualisiert werden.
Um eine Sammlung aus einem Hintergrund-Thread zu füllen, während Steuerelemente daran gebunden sind, müssten Sie wahrscheinlich Ihren eigenen Sammlungstyp von Grund auf neu erstellen, um dies zu beheben. Es gibt jedoch eine einfachere Option, die für Sie möglicherweise funktioniert.
Veröffentlichen Sie die Add-Aufrufe im UI-Thread.
public static void AddOnUI<T>(this ICollection<T> collection, T item) { Action<T> addMethod = collection.Add; Application.Current.Dispatcher.BeginInvoke( addMethod, item ); } ... b_subcollection.AddOnUI(new B());
Diese Methode wird sofort zurückgegeben (bevor das Element tatsächlich zur Sammlung hinzugefügt wird). Anschließend wird das Element im UI-Thread zur Sammlung hinzugefügt, und alle sollten zufrieden sein.
Die Realität ist jedoch, dass diese Lösung aufgrund der gesamten Cross-Thread-Aktivität wahrscheinlich unter starker Last ins Stocken geraten wird. Eine effizientere Lösung würde eine Reihe von Elementen stapeln und sie regelmäßig an den UI-Thread senden, damit Sie nicht für jedes Element über mehrere Threads hinweg aufrufen.
Die BackgroundWorker- Klasse implementiert ein Muster, mit dem Sie den Fortschritt während einer Hintergrundoperation über die ReportProgress- Methode melden können . Der Fortschritt wird im UI-Thread über das ProgressChanged-Ereignis gemeldet. Dies kann eine weitere Option für Sie sein.
quelle
Neue Option für .NET 4.5
Ab .NET 4.5 gibt es einen integrierten Mechanismus, mit dem der Zugriff auf die Erfassungs- und Versandereignisse automatisch
CollectionChanged
an den UI-Thread synchronisiert werden kann . Um diese Funktion zu aktivieren, müssen Sie in Ihrem UI-Thread aufrufen .BindingOperations.EnableCollectionSynchronization
EnableCollectionSynchronization
macht zwei Dinge:CollectionChanged
Ereignisse in diesem Thread zu marshallen.Sehr wichtig ist, dass dies nicht alles erledigt : Um einen thread-sicheren Zugriff auf eine inhärent nicht thread-sichere Sammlung zu gewährleisten , müssen Sie mit dem Framework zusammenarbeiten, indem Sie dieselbe Sperre von Ihren Hintergrund-Threads erhalten, wenn die Sammlung geändert werden soll.
Daher sind die für den korrekten Betrieb erforderlichen Schritte:
1. Entscheiden Sie, welche Art von Verriegelung Sie verwenden möchten
Dies bestimmt, welche Überlast von
EnableCollectionSynchronization
verwendet werden muss. In den meisten Fällen reicht eine einfachelock
Anweisung aus, sodass diese Überlastung die Standardauswahl ist. Wenn Sie jedoch einen ausgefallenen Synchronisationsmechanismus verwenden, werden auch benutzerdefinierte Sperren unterstützt .2. Erstellen Sie die Sammlung und aktivieren Sie die Synchronisierung
Rufen Sie je nach gewähltem Sperrmechanismus die entsprechende Überlastung des UI-Threads auf . Wenn Sie eine Standardanweisung verwenden
lock
, müssen Sie das Sperrobjekt als Argument angeben. Wenn Sie eine benutzerdefinierte Synchronisierung verwenden, müssen Sie einenCollectionSynchronizationCallback
Delegaten und ein Kontextobjekt (das sein kannnull
) bereitstellen . Beim Aufrufen muss dieser Delegat Ihre benutzerdefinierte Sperre erwerben, dieAction
übergebene Sperre aufrufen und die Sperre aufheben, bevor er zurückkehrt.3. Sperren Sie die Sammlung, bevor Sie sie ändern
Sie müssen die Sammlung auch mit demselben Mechanismus sperren, wenn Sie sie selbst ändern möchten. Führen Sie dies mit
lock()
demselben Sperrobjekt aus, an dasEnableCollectionSynchronization
im einfachen Szenario übergeben wurde, oder mit demselben benutzerdefinierten Synchronisierungsmechanismus im benutzerdefinierten Szenario.quelle
BeginInvoke
diese Option, um eine Methode auszuführen, die alle entsprechenden Änderungen im UI-Thread ausführt [höchstens eineBeginInvoke
zu einem bestimmten Zeitpunkt ausstehende.Mit .NET 4.0 können Sie diese Einzeiler verwenden:
.Add
Application.Current.Dispatcher.BeginInvoke(new Action(() => this.MyObservableCollection.Add(myItem)));
.Remove
Application.Current.Dispatcher.BeginInvoke(new Func<bool>(() => this.MyObservableCollection.Remove(myItem)));
quelle
Sammlungssynchronisationscode für die Nachwelt. Dies verwendet einen einfachen Sperrmechanismus, um die Sammlungssynchronisierung zu aktivieren. Beachten Sie, dass Sie die Sammlungssynchronisierung im UI-Thread aktivieren müssen.
public class MainVm { private ObservableCollection<MiniVm> _collectionOfObjects; private readonly object _collectionOfObjectsSync = new object(); public MainVm() { _collectionOfObjects = new ObservableCollection<MiniVm>(); // Collection Sync should be enabled from the UI thread. Rest of the collection access can be done on any thread Application.Current.Dispatcher.BeginInvoke(new Action(() => { BindingOperations.EnableCollectionSynchronization(_collectionOfObjects, _collectionOfObjectsSync); })); } /// <summary> /// A different thread can access the collection through this method /// </summary> /// <param name="newMiniVm">The new mini vm to add to observable collection</param> private void AddMiniVm(MiniVm newMiniVm) { lock (_collectionOfObjectsSync) { _collectionOfObjects.Insert(0, newMiniVm); } } }
quelle