Mam ObservableCollection<A> a_collection;
. Kolekcja zawiera „n” elementów. Każdy element A wygląda następująco:
public class A : INotifyPropertyChanged
{
public ObservableCollection<B> b_subcollection;
Thread m_worker;
}
Zasadniczo wszystko jest podłączone do widoku listy WPF + kontrolka widoku szczegółów, która pokazuje b_subcollection
wybrany element w oddzielnym widoku listy (powiązania dwukierunkowe, aktualizacje w ramach zmiany właściwości itp.).
Problem pojawił się, gdy zacząłem wdrażać gwintowanie. Cały pomysł polegał na tym, aby cały a_collection
wątek roboczy "wykonywał pracę", a następnie aktualizował odpowiednie b_subcollections
i wyświetlał wyniki w interfejsie GUI w czasie rzeczywistym.
Kiedy go wypróbowałem, wystąpił wyjątek, który mówi, że tylko wątek Dispatcher może modyfikować ObservableCollection i praca została zatrzymana.
Czy ktoś może wyjaśnić problem i jak go obejść?
Odpowiedzi:
Technicznie problem nie polega na tym, że aktualizujesz ObservableCollection z wątku w tle. Problem polega na tym, że gdy to zrobisz, kolekcja wywołuje swoje zdarzenie CollectionChanged w tym samym wątku, który spowodował zmianę - co oznacza, że kontrolki są aktualizowane z wątku w tle.
Aby wypełnić kolekcję z wątku w tle, gdy kontrolki są z nim powiązane, prawdopodobnie musiałbyś utworzyć od podstaw własny typ kolekcji, aby rozwiązać ten problem. Jest jednak prostsza opcja, która może Ci się przydać.
Opublikuj wywołania Add w wątku interfejsu użytkownika.
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());
Ta metoda powróci natychmiast (zanim element zostanie faktycznie dodany do kolekcji), a następnie w wątku interfejsu użytkownika element zostanie dodany do kolekcji i wszyscy powinni być zadowoleni.
Rzeczywistość jest jednak taka, że rozwiązanie to prawdopodobnie ugrzęźnie pod dużym obciążeniem z powodu całej aktywności krzyżowej. Bardziej wydajne rozwiązanie pozwoliłoby grupować kilka elementów i okresowo publikować je w wątku interfejsu użytkownika, aby nie wywoływać w wątkach dla każdego elementu.
W BackgroundWorker klasa implementuje wzorzec, który pozwala zgłosić postępu poprzez swoją ReportProgress metody podczas pracy w tle. Postęp jest raportowany w wątku interfejsu użytkownika za pośrednictwem zdarzenia ProgressChanged. To może być inna opcja dla Ciebie.
źródło
Nowa opcja dla .NET 4.5
Począwszy od .NET 4.5 istnieje wbudowany mechanizm automatycznej synchronizacji dostępu do kolekcji i wysyłania
CollectionChanged
zdarzeń do wątku interfejsu użytkownika. Aby włączyć tę funkcję, musisz zadzwonić z poziomu wątku interfejsu użytkownika .BindingOperations.EnableCollectionSynchronization
EnableCollectionSynchronization
robi dwie rzeczy:CollectionChanged
organizuje zdarzenia w tym wątku.Co bardzo ważne, nie zajmuje się to wszystkim : aby zapewnić bezpieczny dla wątków dostęp do kolekcji , która z natury nie jest bezpieczna dla wątków , musisz współpracować z frameworkiem, uzyskując tę samą blokadę z wątków w tle, gdy kolekcja ma zostać zmodyfikowana.
Dlatego kroki wymagane do prawidłowego działania to:
1. Zdecyduj, jakiego rodzaju blokady będziesz używać
To określi, którego przeciążenia
EnableCollectionSynchronization
należy użyć. W większości przypadkówlock
wystarczy prosta instrukcja, więc to przeciążenie jest standardowym wyborem, ale jeśli używasz jakiegoś wymyślnego mechanizmu synchronizacji, istnieje również obsługa niestandardowych blokad .2. Utwórz kolekcję i włącz synchronizację
W zależności od wybranego mechanizmu blokady wywołaj odpowiednie przeciążenie w wątku interfejsu użytkownika . Jeśli używasz standardowej
lock
instrukcji, musisz podać obiekt blokady jako argument. Jeśli używasz synchronizacji niestandardowej, musisz podaćCollectionSynchronizationCallback
delegata i obiekt kontekstu (którym może byćnull
). Po wywołaniu ten delegat musi uzyskać niestandardową blokadę, wywołaćAction
przekazaną do niej blokadę i zwolnić blokadę przed powrotem.3. Współpracuj, blokując kolekcję przed jej zmodyfikowaniem
Musisz także zablokować kolekcję przy użyciu tego samego mechanizmu, gdy zamierzasz samodzielnie ją zmodyfikować; zrób to z
lock()
tym samym obiektem blokady przekazanym doEnableCollectionSynchronization
w prostym scenariuszu lub z tym samym niestandardowym mechanizmem synchronizacji w scenariuszu niestandardowym.źródło
BeginInvoke
do uruchomienia metody, która wykonałaby wszystkie odpowiednie zmiany w wątku interfejsu użytkownika [co najwyżej jednaBeginInvoke
byłaby oczekująca w danym momencie.W .NET 4.0 możesz używać tych jednowierszowych:
.Add
Application.Current.Dispatcher.BeginInvoke(new Action(() => this.MyObservableCollection.Add(myItem)));
.Remove
Application.Current.Dispatcher.BeginInvoke(new Func<bool>(() => this.MyObservableCollection.Remove(myItem)));
źródło
Kod synchronizacji kolekcji dla potomności. Używa to prostego mechanizmu blokady, aby włączyć synchronizację kolekcji. Zwróć uwagę, że musisz włączyć synchronizację kolekcji w wątku interfejsu użytkownika.
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); } } }
źródło