W ramach mojego przygotowania do egzaminu 70-511 (Windows Applications Development with Microsoft .NET Framework 4) oraz uczestnictwa w „StudyGroup” organizowanym przez Łódzką Grupę Profesionalistów IT & .NET opracowałem zagadnienia związane z tematem określonym w training kicie jako „Enhancing Usability”. Z moimi czytelnikami chciałbym podzielić się moimi opracowaniami. W tym wpisie będzie o implementacji przetwarzania asynchronicznego, ze szczególnym naciskiem na to, jak jest to rozwiązane w WPF.
Pisząc aplikację w WPF lub Windows Forms musimy pamiętać, że kod obsługi zdarzenia (np. kliknięcia przycisku) powinien być krótki, gdyż na czas jego wykonania interfejs aplikacji ulega „zamrożeniu”. Często potrzeba jednak wykonać jakieś bardziej czasochłonne operacje, jednocześnie pozwalając użytkownikowi na korzystanie z interfejsu aplikacji. Dodatkowo zalecane jest również, by informować użytkownika o postępie wykonania operacji, jej zakończeniu, jak również pozwolenie na anulowanie długotrwałej operacji. Oznacza to, że potrzebujemy skorzystać z wielowątkowości i przetwarzania asynchronicznego. Trzeba jednak pamiętać, że kontrolki Windows Forms, czy WPF można tylko aktualizować z
poziomu wątku, który je wytworzył!
Ponieważ kontrolki Windows Forms, czy WPF można tylko aktualizować
z poziomu wątku, który je wytworzył, dlatego w .NET framework
dostępny jest wygodny komponent: BackgroundWorker.
System.ComponentModel.BackgroundWorker
jest komponentem wspierającym programistę w wykonywaniu
długotrwałych operacji (jak np. ładowanie pliku, komunikacja z
bazą danych itp…). Realizuje on zadaną pracę w osobnym wątku,
jednocześnie udostępniając mechanizmy informujące o jego stanie,
postępie, zakończeniu. Pozwala również na anulowanie operacji.
Jego mechanizmy informacyjne korzystają z głównego wątku okna
aplikacji, dzięki czemu mogą łatwo aktualizować interfejs
użytkownika. Można korzystać z niego w aplikacjach WPF i Windows
Forms. W przypadku Windows Forms, można go łatwo dodać do formatki
(okienka) korzystając z zasobnika narzędzi (Toolbox) edytora
wizualnego dostępnego w Visual Studio. W przypadku aplikacji WPF
należy obiekt BackgroundWorker
ręcznie dodać i zainicjować w kodzie programu.
Wśród podstawowych elementów BackgroudWorker'a wyróżnić
można:
- RunWorkerAsync – podstawowa metoda, która wywołana
powoduje aktywację zdarzenia DoWork. Można ją wywołać
bez parametrów lub z parametrem (Object) jako parametr
inicjalizacyjny przekazywany do operacji, która ma być wykonana.
- DoWork – metoda z obsługą zdarzenia (private
void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)),
które wykonane jest w osobnym wątku, więc interfejs użytkownika
pozwala na dalszą obsługę poleceń ze strony użytkownika.
DoWorkEventArgs ma m.in. właściwość Argument,
przez którą można pobrać przekazany przez RunWorkerAsync
argument.
Mocną cechą BackgroundWorker'a są możliwości
informacyjne. BackgroundWorker może informować o
zakończeniu operacji. Służy do tego zdarzenie: RunWorkerCompleted,
które ma miejsce, kiedy praca do wykonania w tle skończyła się
lub miał miejsce wyjątek. Szczegółowe informacje (w tym czy
operacja była anulowana, jaki rezultat, itp…) dostępne przez
przekazany obiekt klasy RunWorkerCompletedEventArgs.
BackgroundWorker może informować o postępie:
- Właściwość: WorkerReportsProgress.
Wskazuje, czy BackgroundWorker może informować o postępie.
- Metoda: ReportProgress. Wyzwala zdarzenie
ProgressChanged. Można do niej przekazać int
(postęp w %) i object (stan).
- Zdarzenie: ProgressChanged. Ma miejsce, gdy jest
wywołana metoda ReportProgress. Udostępnia informacje
poprzez obiekt klasy ProgressChangedEventArgs.
BackgroundWorker może informować o stanie:
- Właściwość: IsBusy. Wskazuje, czy BackgroundWorker
aktualnie wykonuje jakąś pracę.
- Właściwość: CancellationPending. Wskazuje, czy
aplikacja żąda anulowania pracy.
BackgroundWorker ułatwia również implementację
scenariusza, którym użytkownik chce anulować wykonywaną operację:
- Właściwość: WorkerSupportsCancellation. Wskazuje,
czy BackgroundWorker wspiera asynchroniczne anulowanie pracy.
- Metoda: CancelAsync. Wysyła żądanie anulowania
pracy w tle.
- Wykonywana operacja sprawdza, czy nie pojawiło się żądanie
anulowania poprzez właściwość CancellationPending (klasy
BackgroundWorker) i jeżeli została anulowana, to powinna
ustawić właściwość Cancel klasy DoWorkEventArgs.
Gdy BackgroundWorker nie wystarcza ...
Oprócz klasy BackgroundWorker można oczywiście
wykorzystywać:
- Delegacje (Delegates) – synchronicznie i asynchronicznie
- Wątki (Threads)
- Zadania (Tasks) (tylko od .NET 4.0)
- I wszystkie możliwości synchronizacyjne dostępne w .NET
(Monitor, Mutex, Semaphore, …)
Powtórzę jednak jeszcze raz, że trzeba jednak pamiętać, że
kontrolki Windows Forms, czy WPF można tylko aktualizować z
poziomu wątku, który je wytworzył!
Nie przestrzeganie powyższego zalecenie może spowodować otrzymanie
wyjątku podobnego do: „System.InvalidOperationException was
unhandled by user code: The calling thread cannot access this object
because a different thread owns it”.
W przypadku Windows Forms wyjątek może wyglądać trochę inaczej,
pisałem o tym wcześniej we wpisie: „WinForms:
Cross-thread operation not valid”. A rozwiązanie ogólnie
sprowadza się do wykorzystania faktu, że każda kontrolka ma
właściwość InvokeRequired, informującą o konieczności
aktualizacji poprzez metodę BeginInvoke, która wykonuje przekazaną
delegację w wątku obsługującym kontrolkę.
Rozwiązaniem tego problemu dla WPF sprowadza się do wykorzystania
Dispatcher'a. Każda kontrolka w WPF ma dostęp do swojego
Dispatcher'a we właściwości Dispatcher. Precyzyjniej mówiąc
każda kontrolka (czyli System.Windows.DependencyObject,
System.Windows.Media.Visual
lub System.Windows.UIElement)
dziedziczy po klasie System.Windows.Threading.DispatcherObject.
Do aktualizacji wykorzystujemy metody Invoke
lub BeginInvoke,
do których przekazujemy delegację, która ma być wykonana w wątku
obsługującym kontrolki. Oprócz delegacji, ustalamy również
priorytet w jakim Dispatcher ma wykonać aktualizację kontrolki
(jest to enumeracja DispatcherPriority, dla której wyjaśnienia
priorytetów zawiera tabelka dostępna
http://msdn.microsoft.com/en-us/library/system.windows.threading.dispatcherpriority.aspx).
Przed wykorzystaniem Dispatcher'a warto sprawdzić czy musimy to
robić (być może aktualizacja kontrolki odbywa się we właściwym
wątku, w tym celu można wykorzystać następujące metody klasy
Dispatcher:
- CheckAccess, która zwraca wartość true/false (true jeżeli
możemy aktualizować kontrolkę bezpośrednio i bez wykorzystania
Dispatcher'a),
- VerifyAccess, która kończy się wyjątkiem, jeżeli nie kod
nie wykonuje się w wątku obsługującym kontrolki.
Powyższe rozwiązanie można zawrzeć w kodzie zbliżonym do
poniższego (przykład z MSDN):
// Uses the Dispatcher.CheckAccess method to determine if // the calling thread has access to the thread the UI object is on. private void TryToUpdateButtonCheckAccess(object uiObject) { Button theButton = uiObject as Button; if (theButton != null) { // Checking if this thread has access to the object. if (theButton.Dispatcher.CheckAccess()) { // This thread has access so it can update the UI thread. UpdateButtonUI(theButton); } else { // This thread does not have access to the UI thread. // Place the update method on the Dispatcher of the UI thread. theButton.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new UpdateUIDelegate(UpdateButtonUI), theButton); } } }
Brak komentarzy:
Prześlij komentarz