Zmień kontrolki WPF z wątku innego niż główny przy użyciu Dispatcher.Invoke

81

Niedawno zacząłem programować w WPF i napotkałem następujący problem. Nie rozumiem, jak używać tej Dispatcher.Invoke()metody. Mam doświadczenie w tworzeniu wątków i stworzyłem kilka prostych programów Windows Forms, w których właśnie użyłem

Control.CheckForIllegalCrossThreadCalls = false;

Tak, wiem, że to dość kiepskie, ale były to proste aplikacje monitorujące.

Faktem jest, że teraz tworzę aplikację WPF, która pobiera dane w tle, zaczynam nowy wątek, aby wykonać wywołanie w celu pobrania danych (z serwera internetowego), teraz chcę wyświetlić je w moim formularzu WPF. Rzecz w tym, że nie mogę ustawić żadnej kontroli z tego wątku. Ani etykiety, ani nic. Jak można to rozwiązać?

Komentarze do odpowiedzi:
@Jalfp:
Więc używam tej metody Dispatchera w „nowym bieżniku”, kiedy otrzymuję dane? A może powinienem zmusić pracownika działającego w tle do pobrania danych, umieszczenia ich w polu i rozpoczęcia nowego wątku, który czeka, aż to pole zostanie wypełnione, i zadzwonić do dyspozytora, aby pokazał pobrane dane w kontrolkach?

D. Veloper
źródło
To CheckForIllegalCrossThreadCalls jest niesamowite. Chciałbym wiedzieć, że zanim na szybkie „kogo to obchodzi” aplikacji
Gaspa79

Odpowiedzi:

177

Pierwszą rzeczą jest zrozumienie, że Dispatcher nie jest przeznaczony do wykonywania długich operacji blokowania (takich jak pobieranie danych z serwera internetowego ...). Możesz użyć Dispatchera, gdy chcesz uruchomić operację, która zostanie wykonana w wątku interfejsu użytkownika (na przykład zaktualizowanie wartości paska postępu).

Możesz pobrać dane w tle procesu roboczego i użyć metody ReportProgress do propagowania zmian w wątku interfejsu użytkownika.

Jeśli naprawdę potrzebujesz bezpośrednio skorzystać z Dispatchera, jest to całkiem proste:

Application.Current.Dispatcher.BeginInvoke(
  DispatcherPriority.Background,
  new Action(() => this.progressBar.Value = 50));
japf
źródło
22
Możesz pozbyć się części „new Action (”) i po prostu użyć wyrażenia lambda: DispatcherPriority.Background, () => this.progressBar.Value = 50
jrista
Tak, nie wiem, dlaczego umieściłem tutaj akcję: p
japf
1
@Carsten Ta odpowiedź dotyczy aplikacji WPF, które używają klasy System.Windows.Application.
joshuapoehls
10
@jrista: Naprawdę możesz? Otrzymuję CS1660 , próbując bez new Action(...).
LUB Mapper
6
@jrista: Ogólnie rzecz biorąc, prawda - chociaż ten artykuł wyjaśnia, dlaczego nie działa w przypadku metod bez parametrów, takich jak te przekazywane do, BeginInvokea zamiast tego pojawia się błąd kompilatora CS1660.
LUB Mapper
31

japf odpowiedział poprawnie. Na wszelki wypadek, jeśli patrzysz na akcje wieloliniowe, możesz napisać jak poniżej.

Application.Current.Dispatcher.BeginInvoke(
  DispatcherPriority.Background,
  new Action(() => { 
    this.progressBar.Value = 50;
  }));

Informacje dla innych użytkowników, którzy chcą wiedzieć o wydajności:

Jeśli kod MUSI zostać napisany w celu uzyskania wysokiej wydajności, możesz najpierw sprawdzić, czy wywołanie jest wymagane, używając flagi CheckAccess.

if(Application.Current.Dispatcher.CheckAccess())
{
    this.progressBar.Value = 50;
}
else
{
    Application.Current.Dispatcher.BeginInvoke(
      DispatcherPriority.Background,
      new Action(() => { 
        this.progressBar.Value = 50;
      }));
}

Zwróć uwagę, że metoda CheckAccess () jest ukryta w programie Visual Studio 2015, więc po prostu napisz ją, nie oczekując, że inteligencja ją pokaże. Zauważ, że CheckAccess ma narzut na wydajność (narzut w ciągu kilku nanosekund). Lepiej jest tylko wtedy, gdy chcesz zaoszczędzić tę mikrosekundę wymaganą do wykonania „wywołania” za wszelką cenę. Ponadto zawsze istnieje opcja utworzenia dwóch metod (włączonych z invoke i innych bez), gdy wywołanie metody jest pewne, czy jest w wątku interfejsu użytkownika, czy nie. To bardzo rzadki przypadek, kiedy powinieneś spojrzeć na ten aspekt dyspozytora.

Yogee
źródło
4

Kiedy wykonywany jest wątek i chcesz wykonać główny wątek interfejsu użytkownika, który jest blokowany przez bieżący wątek, użyj poniższego:

aktualny wątek:

Dispatcher.CurrentDispatcher.Invoke(MethodName,
    new object[] { parameter1, parameter2 }); // if passing 2 parameters to method.

Główny wątek interfejsu użytkownika:

Application.Current.Dispatcher.BeginInvoke(
    DispatcherPriority.Background, new Action(() => MethodName(parameter)));
Gopi Rao
źródło
MethodName nie istnieje w obecnym kontekście
AriesConnolly
0

Powyższa odpowiedź @japf działa dobrze, aw moim przypadku chciałem zmienić kursor myszy z obracającego się koła z powrotem na normalną strzałkę, gdy przeglądarka CEF zakończy ładowanie strony. W przypadku, gdy może to komuś pomóc, oto kod:

private void Browser_LoadingStateChanged(object sender, CefSharp.LoadingStateChangedEventArgs e) {
   if (!e.IsLoading) {
      // set the cursor back to arrow
      Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background,
         new Action(() => Mouse.OverrideCursor = Cursors.Arrow));
   }
}
nrod
źródło