Aktualizacja interfejsu z drugiego wątku(windows forms i WPF)

Na różnych forach często użytkownicy mają problem z aktualizacją kontrolek z innego wątku. Załóżmy, że odpaliliśmy sobie BackgroundWorker lub po prostu Thread. Wykonujemy jakieś operację, np. łączymy się ze zdalnymi zasobami. Stworzenie osobnego wątku w takim scenariuszu jest bardzo pożądane ponieważ nie blokujemy wtedy aktualizacji interfejsu. W trakcie pobierania informacji z Internetu chcemy aktualizować interfejs aby informować użytkownika o postępach np.

progressBar.Value = progessValue;

Jeśli powyższy kod jest wywołany z obcego wątku użytkownik dostanie następujący komunikat o błędzie:

Cross-thread operation not valid:
Control accessed from a thread other than the thread it was created on.

Wyjątek jest wyrzucany ponieważ zarówno w WindowsForms jak i w WPF nie można aktualizować interfejsu z innego wątku niż z tego w którym została stworzona kontrolka. Musimy więc w jakiś sposób dostać się do wątku macierzystego dla kontrolki i tam wykonać aktualizacje tej kontrolki. Służy do tego metoda Control.Invoke(windows forms) lub ControlDispatcher.Invoke(Wpf). Chcąc więc zaktualizować ProgressBar w WinForms musimy napisać:

Action<int> updateAction = new Action<int>((value) => progressBar.Value = value);
progressBar.Invoke(updateAction,32);

Powyższy kod będzie działał w każdym przypadku ale ma jedną wadę związaną z wydajnością kodu. Co w przypadku gdy mamy osobną klasę do aktualizacji interfejsu i jest ona wywoływana zarówno z wątku kontrolki jak i z obcego wątku? Dla drugiego przypadku(z obcego wątku) kod jest maksymalnie optymalny. Z kolei w sytuacji gdy wywołujemy ją z wątku macierzystego dla kontrolki nie potrzebnie będzie wykonywana masa operacji związanych z wpompowaniem operacji w kolejkę dla wątku interfejsu. Na szczęście istnieje gotowa metoda, która stwierdza czy dla aktualnego przypadku użycia jest wymagane wywołanie Invoke:

Action<int> updateAction = new Action<int>((value) => progressBar.Value = value);
if (progressBar.InvokeRequired)
    progressBar.Invoke(updateAction,5);
else
    updateAction(4);

Posiadamy już optymalną wersje kodu. Jednak pisanie powyższego kodu za każdym razem gdy chcemy zaktualizować interfejs(nigdy nie wiemy czy kod nie będzie wywoływany w przyszłości z innego wątku) jest co najmniej niewygodne. Z ratunkiem przychodzą nam tzw. rozszerzenia(extensions,c# 3.0). Możemy przecież stworzyć własną metodę nazwaną powiedzmy InvokeIfRequired:

public static class ControlExtensions
{
   public static void InvokeIfRequired(this Control control, Action action)
   {
       if (control.InvokeRequired)
           control.Invoke(action);
       else
           action();
   }
   public static void InvokeIfRequired<T>(this Control control, Action<T> action, T parameter)
   {
       if (control.InvokeRequired)
           control.Invoke(action, parameter);
       else
           action(parameter);
   }
}

Za pomocą takiego rozwiązania aktualizacja kontrolki sprowadzi się wyłącznie do poniższego kodu:

this.InvokeIfRequired((value) => progressBar.Value = value, 10);

W przypadku Wpf rozwiązanie jest bardzo podobne:

public static class ControlExtensions
{
   public static void InvokeIfRequired(this Control control, Action action)
   {
       if (System.Threading.Thread.CurrentThread!=control.Dispatcher.Thread)
           control.Dispatcher.Invoke(action);
       else
           action();
   }
   public static void InvokeIfRequired<T>(this Control control, Action<T> action, T parameter)
   {
       if (System.Threading.Thread.CurrentThread!=control.Dispatcher.Thread)
           control.Dispatcher.Invoke(action, parameter);
       else
           action(parameter);
   }
}

Z kolei użycie metody jest identyczne jak w przypadku WinForms:

this.InvokeIfRequired((value) => bar.Value = value, 10);

13 thoughts on “Aktualizacja interfejsu z drugiego wątku(windows forms i WPF)”

  1. Background worker nic nie załatwi. Też wyskoczy błąd Cross-Thread exception bez Invoke 🙂

  2. True, pospieszyłem się. BW będzie się nadawał jeśli wynik wyswietlimy po zakonczeniu akcji 😉 Dawno to pisalem i zapomnialem o subtelnej roznicy 🙂

  3. Bez Invoke wyskoczy Ci błąd „Cross-Thread exception…”. Zawsze przy aktualizacji interfejsu z innego wątku musisz używać Invoke…

  4. Nie prawda nie wyskoczy, napisałem nie jeden program wielowątkowy zawsze korzystając z event i delegate i nigdy nie było tego problemu, a metoda z invoke wprowadza tylko zamieszanie, w ramach sprostowania piszę o tym jak jest w winforms, wpf to nie moja działka.

  5. Witam,
    Przetestuj na innych komputerach. Z pewnością to tylko przypadek, że pogram działa Ci prawidłowo. Inna możliwość to taka, że wywołujesz kod aktualizacji w tym samym wątku(np. w przypadku Backgroundworker może to być zdarzenie Completed).

    Na potwierdzenie wklejam link do MSDN:
    http://msdn.microsoft.com/en-us/library/system.windows.forms.control.invokerequired.aspx

    Na dole w community content masz wyjaśnioną przyczynę wyjątku Cross-thread operation. Chodzi mi o post “How to Use InvokeRequired and Invoke on Windows Forms with Multithreading”

    Pozdrawiam

  6. Co do backgroundWorkera to posiada on metode ReportProgess i zdarzenie o podobnej nazwie. Wystarczy mu ustawic Wlasciwosc (Zdaje sie ze WorkerReportsProgress jakby sie nie dalo tego z zawartosci eventa wywnioskowac) podpiac sie pod zdarzenie i w metodzie podpietej pod DoAction wywolywac ReportProgress. Metoda moze przyjmuje parametr ktorym moze byc dowolny obiekt (np. Intem mowiacym o wartosci progressbara) dobrze jest aby ten obiekt nie zmienial stanu po utworzeniu.

  7. Mam tylko małą sugestię zamiast extension method dla Control warto by zrobić dla intercejsu ISynchronizeInvoke, zysk taki ze w kodzie nie referensujemy do Windows.Forms i bedzie działać dla innych szerszych implemenacji

  8. @andrew zdecydowanie artykuł jest na rzeczy, a Ty niestety zawsze pisales to źle, problem jest znany dlatego wpier sugeruje zapoznać sie z tym co piszą na MSDN.

  9. @tryliop,@Wojciech(szogun1987:
    Dzięki za info.
    Post stary ale postanowiłem wszystkie posty podpiąć pod dotnetomaniaka ponieważ bardzo dużo ludzi czyta blogi poprzez ten serwis właśnie, a nie bezpośrednio przez blog.

  10. A co zrobić z sytuacją gdy uchwyt okna jeszcze lub już nie istnieje? Np. gdy wątek roboczy ruszył razem z wątkiem głównym programu. Msdn porusza problem w Control.InvokeRequred (IsHandleCreated), ale z moich prób wynika iż to nie zawsze działa. Tak samo jeśli zamkniemy aplikację gdy wątek jest w trakcie Invoke… Dostajemy wówczas ObjectDisposedException.

Leave a Reply

Your email address will not be published.