piątek, 10 czerwca 2011

70-511: Enhancing Usability: Implementacja przetwarzania asynchronicznego (teoria) [PL]

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);
        }
    }
}

Promuj

Brak komentarzy:

Prześlij komentarz

Posty powiązane / Related posts