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);
A BackgroundWorker? Przeciez wlasnie do tego zostal stworzony 🙂
http://msdn.microsoft.com/en-us/library/system.componentmodel.backgroundworker.aspx
Background worker nic nie załatwi. Też wyskoczy błąd Cross-Thread exception bez Invoke 🙂
True, pospieszyłem się. BW będzie się nadawał jeśli wynik wyswietlimy po zakonczeniu akcji 😉 Dawno to pisalem i zapomnialem o subtelnej roznicy 🙂
No i zonk, jedyna prawidłowa metoda to użycie delegate i eventu bez mieszania z invoke.
Bez Invoke wyskoczy Ci błąd „Cross-Thread exception…”. Zawsze przy aktualizacji interfejsu z innego wątku musisz używać Invoke…
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.
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
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.
Andrev a moze miales globalnie wylaczone sprawdzanie czy wywolanie pochodzi z watku UI czy z innego??
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
@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.
@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.
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.