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