Jak zaktualizować ObservableCollection za pośrednictwem wątku roboczego?

83

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_subcollectionwybrany 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_collectionwątek roboczy "wykonywał pracę", a następnie aktualizował odpowiednie b_subcollectionsi 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ść?

Maciek
źródło
Wypróbuj poniższy link, który zapewnia bezpieczne dla wątków rozwiązanie, które działa z dowolnego wątku i może być powiązane za pośrednictwem wielu wątków interfejsu użytkownika: codeproject.com/Articles/64936/ ...
Anthony

Odpowiedzi:

74

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.

Josh
źródło
co z runWorkerAsyncCompleted BackgroundWorkera? czy jest to również powiązane z wątkiem interfejsu użytkownika?
Maciek
1
Tak, sposób, w jaki BackgroundWorker został zaprojektowany, polega na użyciu SynchronizationContext.Current w celu podniesienia jego zdarzeń zakończenia i postępu. Zdarzenie DoWork zostanie uruchomione w wątku w tle. Oto dobry artykuł na temat wątków w WPF, w którym omówiono również BackgroundWorker msdn.microsoft.com/en-us/magazine/cc163328.aspx#S4
Josh
5
Ta odpowiedź jest piękna w swojej prostocie. Dzięki za udostępnienie tego!
Beaker
@Michael W większości przypadków wątek działający w tle nie powinien blokować i czekać na aktualizację interfejsu użytkownika. Korzystanie z Dispatcher.Invoke grozi martwym blokowaniem, jeśli dwa wątki czekają na siebie, co w najlepszym przypadku znacznie obniży wydajność twojego kodu. W twoim konkretnym przypadku może być konieczne zrobienie tego w ten sposób, ale w większości sytuacji ostatnie zdanie jest po prostu nieprawidłowe.
Josh
@Josh Usunąłem odpowiedź, ponieważ moja sprawa wydaje się wyjątkowa. Przyjrzę się dalszemu projektowi i pomyślę jeszcze raz, co można zrobić lepiej.
Michael
125

Nowa opcja dla .NET 4.5

Począwszy od .NET 4.5 istnieje wbudowany mechanizm automatycznej synchronizacji dostępu do kolekcji i wysyłania CollectionChangedzdarzeń 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:

  1. Zapamiętuje wątek, z którego jest wywoływany, i powoduje, że potok powiązania danych CollectionChangedorganizuje zdarzenia w tym wątku.
  2. Uzyskuje blokadę kolekcji do momentu obsłużenia zdarzenia zorganizowanego, aby programy obsługi zdarzeń z wątkiem interfejsu użytkownika nie próbowały odczytać kolekcji, gdy jest ona modyfikowana z wątku w tle.

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 EnableCollectionSynchronizationnależy użyć. W większości przypadków lockwystarczy 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 lockinstrukcji, musisz podać obiekt blokady jako argument. Jeśli używasz synchronizacji niestandardowej, musisz podać CollectionSynchronizationCallbackdelegata i obiekt kontekstu (którym może być null). Po wywołaniu ten delegat musi uzyskać niestandardową blokadę, wywołać Actionprzekazaną 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 do EnableCollectionSynchronizationw prostym scenariuszu lub z tym samym niestandardowym mechanizmem synchronizacji w scenariuszu niestandardowym.

Jon
źródło
2
Czy to powoduje, że aktualizacje kolekcji są blokowane, dopóki wątek interfejsu użytkownika nie zajmie się nimi? W scenariuszach obejmujących jednokierunkowe, powiązane z danymi kolekcje niezmiennych obiektów (relatywnie powszechny scenariusz), wydaje się, że możliwe byłoby posiadanie klasy kolekcji, która utrzymywałaby „ostatnio wyświetlaną wersję” każdego obiektu, a także kolejkę zmian i użyj BeginInvokedo uruchomienia metody, która wykonałaby wszystkie odpowiednie zmiany w wątku interfejsu użytkownika [co najwyżej jedna BeginInvokebyłaby oczekująca w danym momencie.
supercat
16
Mały przykład uczyniłby tę odpowiedź znacznie bardziej użyteczną. Myślę, że to prawdopodobnie właściwe rozwiązanie, ale nie mam pojęcia, jak je wdrożyć.
RubberDuck
3
@Kohanz Wywołanie do dyspozytora wątków interfejsu użytkownika ma wiele wad. Największą z nich jest to, że Twoja kolekcja nie zostanie zaktualizowana, dopóki wątek interfejsu użytkownika faktycznie nie przetworzy wysyłki, a następnie będziesz działać w wątku interfejsu użytkownika, co może powodować problemy z responsywnością. Z drugiej strony, korzystając z metody blokowania, natychmiast aktualizujesz kolekcję i możesz kontynuować przetwarzanie w wątku w tle bez konieczności wykonywania jakichkolwiek czynności przez wątek interfejsu użytkownika. Wątek interfejsu użytkownika w razie potrzeby nadrobi zmiany w następnym cyklu renderowania.
Mike Marynowski
2
Patrzę na synchronizację kolekcji w 4.5 już od około miesiąca i nie sądzę, aby część tej odpowiedzi była poprawna. Odpowiedź stwierdza, że ​​wywołanie enable musi nastąpić w wątku interfejsu użytkownika i że wywołanie zwrotne występuje w wątku interfejsu użytkownika. Żaden z nich nie wydaje się mieć miejsca. Jestem w stanie włączyć synchronizację kolekcji w wątku w tle i nadal korzystać z tego mechanizmu. Co więcej, głębokie wywołania we frameworku nie wykonują żadnego uporządkowania (por. ViewManager.AccessCollection. Referenceource.microsoft.com/#PresentationFramework/src/… )
Reginald Blue
2
Więcej informacji na temat EnableCollectionSynchronization można znaleźć w odpowiedzi na ten wątek: stackoverflow.com/a/16511740/2887274
Matthew S,
22

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)));
WhileTrueSleep
źródło
11

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);
        }
    }
}
LadderLogic
źródło