środa, 1 lipca 2009

WinForms: Cross-thread operation not valid


Chyba każdy, kto pisał kiedykolwiek aplikacje wielowątkowe, które swoje wyniki prezentowały na kontrolkach WinForms spotkał się z następującym wyjątkiem:
System.InvalidOperationException occurred, Message="Cross-thread operation not valid: Control 'MainForm' accessed from a thread other than the thread it was created on.", Source="System.Windows.Forms"
Powyższy wyjątek pojawia się, gdy chcemy zmodyfikować zawartość kontrolki z innego wątku, niż ona została wytworzona. W prostszych słowach można powiedzieć, że innego wątku niż ten, który obsługuje okno. Rozwiązanie nie jest może skomplikowane, ale ponieważ musiałem je stosować już wiele razy postanowiłem sobie w tym post'cie przygotować pewnego rodzaju ściągę, a może przyda się to jeszcez komuś.
Otóż w funkcji, która modyfikuje zawartość kontrolki, należy sprawdzić, właściwość o nazwie InvokeRequired i jeżeli jest ona ustawiona na true, to mamy do czynienia z przypadkiem, że funkcję wywołał inny wątek, niż ten co obsługuje kontrolkę. Jeżeli tak jest, to musimy wykorzystać funkcję kontrolki: BeginInvoke, która spowoduje, że to co chcemy wykonać odbędzie się asynchronicznie w stosunku do wątku wywołującego oraz że zostanie, to wykonane w wątku obsługującym kontrolkę.
Poniższy przykład wykorzystuje opisywaną tu metodę i zamykanie okna odbywa się poprzez delegację, jeżeli zachodzi taka potrzeba (InvokeRequired).
    void Application_ShutdownRequest( object sender, EventArgs e )
    {
      if ( InvokeRequired )
      {
        BeginInvoke(new MethodInvoker(Close));
      }
      else
        this.Close();
    }

5 komentarzy:

  1. Tez kiedys o tym pisalem: http://tomaszwisniewski.com/watki-c-i-strona-microsoft/
    :)

    OdpowiedzUsuń
  2. no to razem napisaliśmy ;)

    OdpowiedzUsuń
  3. Można sobie ułatwić życie, pisząc tak:

    this.BeginInvoke(o => o.Close());

    Korzystając z rozszerzeń dla klasy Control:

    public static class ControlEx
    {
      public static void Invoke<T>(this T control, Action<T> action) where T : Control
      {
        if (control.InvokeRequired)
        {
          control.Invoke(action, new object[] { control });
        }
        else action(control);
      }

      public static TResult Invoke<T, TResult>(this T control, Func<T, TResult> function) where T : Control
      {
        if (control.InvokeRequired)
        {
          return (TResult)control.Invoke(function, new object[] { control });
        }
        else return function(control);
      }

      public static void BeginInvoke<T>(this T control, Action<T> action) where T : Control
      {
        if (control.InvokeRequired)
        {
          control.BeginInvoke(action, new object[] { control });
        }
        else action(control);
      }
    }

    OdpowiedzUsuń
  4. Do wątku głównego możemy się też odwołać za pomocą delegata

    Application.Current.Dispatcher.Invoke(DispatcherPriority.Normal, new Action(delegate
    {
    //Tutaj kod który ma się wykonać w kontekście głównego wątku
    }));

    OdpowiedzUsuń
  5. Dodam jeszcze, że istnieje już delegat stworzony dokładnie do tego celu: MethodInvoker (http://msdn.microsoft.com/en-us/library/system.windows.forms.methodinvoker.aspx).

    OdpowiedzUsuń

Posty powiązane / Related posts