• Najnowsze pytania
  • Bez odpowiedzi
  • Zadaj pytanie
  • Kategorie
  • Tagi
  • Zdobyte punkty
  • Ekipa ninja
  • IRC
  • FAQ
  • Regulamin
  • Książki warte uwagi

wpf problem z korzystaniu z kontrolek w innym wątku

VPS Starter Arubacloud
+1 głos
5,930 wizyt
pytanie zadane 18 sierpnia 2016 w C# przez jankustosz1 Nałogowiec (35,880 p.)

Mam problem taki jak w temacie.

Znalazłem takie strony:

http://patryknet.blogspot.co.uk/2010/04/programowanie-wielowatkowe-w-net-cz-2.html

http://www.pzielinski.com/?p=77

niestety te artykuły chyba są jakieś przestarzałe bo vs nie może znaleźć IDispather ani Invoke

 

 

1 odpowiedź

+11 głosów
odpowiedź 18 sierpnia 2016 przez Sebastian Fojcik Nałogowiec (43,020 p.)
edycja 1 października 2017 przez Sebastian Fojcik
 
Najlepsza

To bardzo ważna kwestia, o której tutaj wspominasz i z którą masz problem. Jest niby wiele artykułów na ten temat, pięknie zatytułowanych, a w ostateczności żaden z nich nie podaje rozwiązania dla WPF (bo różni się ono trochę od np. Windows Forms).

Postaram się w podstawowym zakresie wyjaśnić jak obsługiwane są kontrolki z innego wątku w ogóle, a później pokazać ostateczny przykład rozwiązujący problem w WPF.

No więc wszystkie kontrolki, które tworzysz w swojej aplikacji, są tworzone w wątku głównym. Podstawowa zasada działania wątków jest taka, że dany wątek ma dostęp wyłącznie do swoich własnych zmiennych / obiektów. To znaczy, że z zewnętrznego wątku nie możesz zmienić zmiennej należącej do innego wątku. Takie działanie ma oczywiście sens, ponieważ (w teorii) 2 wątki działają jednocześnie. A co gdyby jednocześnie mogły uzyskać dostęp do jakiejś zmiennej? Jeden czyta, drugi zapisuje, no i robi się bałagan.

Czy to oznacza, że z jednego wątku nie możemy w ogóle ingerować w obiekty / kontrolki innego wątku? Oczywiście, że możemy. Tylko nie bezpośrednio. Rozwiązaniem tego problemu w WPF jest obiekt Dispatcher. Potrafi on niejako zlecić jakieś zadanie wątkowi głównemu. Czy nam się to w ogóle przyda? Oczywiście, że tak! Jak wspominałem, kontrolki w WPF są tworzone w wątku głównym programu. A co gdyby tak z innego wątku zlecić wątkowi głównemu zmienić coś w kontrolce? To jest dokładnie to, o co nam chodziło. Obiekt Dispatcher powinien być elementem każdej standardowej kontrolki WPF. Posiada on kilka ciekawych funkcji. Ja omówię tylko te najbardziej Ci potrzebne.

Dispatcher.CheckAccess()

Zwraca prawdę, jeśli obecnie znajdujesz się w wątku głównym. W przeciwnym przypadku — fałsz. Zastosowanie pokażę na końcu "wykładu" :-D

Dispatcher.Invoke()

Zleca wykonanie danej funkcji wątkowi głównemu i czeka na jej zakończenie. Dopiero potem wątek pracuje dalej.

Dispatcher.BeginInvoke()

Zleca wykonanie danej funkcji wątkowi głównemu i nie czeka na jej zakończenie. Wątek po zleceniu zadania, pracuje dalej.

Teraz jeszcze jakie parametry przyjmują funkcje Invoke i BeginInvoke. Funkcja jest przeładowana wielokrotnie. Może przyjmować obiekt typu Action, Func, delegate i wiele innych. Te trzy, które wymieniłem są najważniejsze. Powiedzmy, że stworzyłeś TextBox o nazwie "textBox". Teraz zmienimy jego zawartość na różne sposoby funkcją Invoke, aby pokazać wywołania na przykładach:

Invoke( new Action( () => {textBox.Text = "Napis"; } ) )

Tworzymy nowy obiekt typu Action. Polecam Ci poczytać o obiektach Action oraz Func. Bardzo ułatwisz sobie sprawę, a i mi oszczędzisz dalszych wyjaśnień :-)

Invoke( () => { textBox.Text = "Napis"; } )

Tutaj korzystamy z wyrażenia lambda. Delegat to taki jakby wskaźnik na funkcję, a lambda, to właśnie funkcja, więc owe wyrażenie zostanie przypisane do delegata.

Invoke( delegate { textBox.Text = "Napis"; } )

Tym razem wyraźnie zaznaczamy, że tworzymy delegata, który ma zostać wywołany przez wątek główny.

Invoke(System.Windows.Threading.DispatcherPriority.Normal, new Action(() => { textBox.Text = "Napis"; }))

Możemy też ustalić z jakim priorytetem zlecamy wątkowi głównemu wykonanie naszej funkcji. Pamiętajmy, że wątek główny ma też swoje własne sprawy na głowie ;-)

Jest wiele wiele więcej wywołań tej funkcji, rzecz jasna nie jestem w stanie pokazać wszystkich.
Wszędzie powyżej tworzyłem ciało funkcji w klamrach, w momencie wywołania Invoke. Oczywiście nie trzeba tak robić, można posłać normalną, wcześniej utworzoną i nazwaną funkcję do wykonania, ale po co. My tylko zmieniamy zawartość pola Text na inną. Taki sposób nie ma sensu:

private void nowyText()
{
     textBox.Text = "Napis";
}

// ...

Invoke( nowyText );

Ta wiedza wystarczy aby używać kontrolek z innych wątków. Teraz przyda Ci się jakiś praktyczny przykład, który zademonstruje działanie dwóch wątków w akcji.

Spróbuj stworzyć projekt aplikacji według tego co napiszę. Dodaj do okienka Button oraz ProgressBar. Nazwij button i progressBar. Cel do wykonania: wypełnianie się naszego paska bez zablokowania interakcji z programem (wypełnianie paska ma być z osobnego wątku). Na początek dodaj:

using System.Threading;

Teraz zróbmy funkcję wypełniającą cały pasek:

void uzupelnij()
{
     for( int i = 1; i < =100; i++ )
     {
          progressBar.Value = i;
          Thread.Sleep(100);
     }
}

Następnie kliknij dwukrotnie na Button, który dodałeś i w funkcji Click przycisku dopisz wywołanie powyższej funkcji:

private void button_Click(object sender, RoutedEventArgs e)
{
         uzupelnij();
}

Uruchom program i wciśnij przycisk. Efekt? Zawieszenie interfejsu i po chwili nagle pojawia się w pełni uzupełniony pasek. To było do przewidzenia. Teraz obsłużmy tę funkcję w innym wątku.

Do Click przycisku wpisz takie coś:

private void button_Click(object sender, RoutedEventArgs e)
{
     Thread thread = new Thread(uzupelnij);
     thread.start();
}

Uruchom program i wciśnij przycisk. Znany błąd, prawda? Próba uzyskania dostępu do kontrolki z innego wątku.
I teraz sedno całego mojego wywodu. Rozwiązanie Twojego problemu. Po tym wszystkim co napisałem wiesz już jak to działa i co trzeba zrobić. Musimy zlecić zmianę wartości progressBar wątkowi głównemu, bo tylko on ma prawo dostępu do tej kontrolki. Teraz tylko jak to zrobić:

W funkcji void uzupelnij() zamień linijkę:

progressBar.Value = i;

na takie coś:

progressBar.Dispatcher.Invoke( () => {progressBar.Value = i;} );

Zlecamy zmianę wartości do wątku głównego, a dokładniej zlecamy wątkowi głównemu wywołanie funkcji, którą przekazujemy (w tym przypadku wyrażenie lambda). 
Teraz uruchom program i wciśnij przycisk.
Tadaaaam, ładuje się, a interfejs dalej działa. (Zamknięcie programu w czasie ładowania może rzucić wyjątek).
Mam nadzieję, ze po moich opisach rozumiesz różnicę pomiędzy Invoke oraz BeginInvoke. Kilka słów jeszcze o CheckAccess.

Czasami jakaś funkcja może być wywoływana zarówno przez wątek główny oraz inny. Co się stanie jeśli wywołamy funkcję uzupelnij() będąc w wątku głównym? Wątek główny zleci samemu sobie wykonanie przesłanej funkcji? Otóż... trochę jakby tak. Efekt tego będzie taki, że działanie głównego wątku się spowolni. Dlatego właśnie stworzono możliwość sprawdzenia wątku, w którym się aktualnie znajdujemy. Jeżeli jesteśmy w wątku głównym to nie należy używać Dispatcher

Pokażę to na ostatnim już przykładzie. TextBox o nazwie log oraz funkcja wypisz().
Raz wywołujemy tę funkcje z wątku głównego (dostęp do kontrolek normalny), a raz z innego wątku (dostęp do kontrolek poprzez Dispatcher).

public MainWindow()
{
     InitializeComponent();
     wypisz(); // główny wątek
     Thread thread = new Thread( wypisz );
     thread.Start(); // inny wątek
}
		
private void wypisz()
{
	if( log.Dispatcher.CheckAccess())
	{
		log.Text += "Wątek główny";
	}
	else
	{
		log.Dispatcher.Invoke( delegate
		{
		     log.Text += "Inny wątek";
		})
	}
}

Przy okazji pokazałem ciekawy sposób wywołania i zapisania Invoke z delegatem :-)

Mam nadzieję, że nie tylko wiesz jak obsłużyć kontrolkę z innego wątku, ale jednocześnie rozumiesz jak ta "obsługa" działa. Taki był cel całego mojego wywodu. A nuż, widelec trafią tu inni i zechcą się czegoś nowego dowiedzieć :-)

Pozdrawiam.

komentarz 19 sierpnia 2016 przez jankustosz1 Nałogowiec (35,880 p.)
Wiielkie dzięki!

Napracowałeś się.
komentarz 1 października 2017 przez obl Maniak (51,280 p.)
Dobry wykład, możesz wykładać częściej :) Też kiedyś miałem z tym problem, ale to obsłużyłem w C# przy okazji robienia generatora albumu HTML.
1
komentarz 5 października 2017 przez MedykMatyk Nowicjusz (100 p.)
Wow, nie spodziewałem się tak wspaniałego postu w odpowiedzi na nurtujące także mnie pytanie. Oby więcej takich świetnych użytkowników! :)

Btw. Specjalnie założyłem konto, żeby móc podziękować :P

Podobne pytania

0 głosów
0 odpowiedzi 100 wizyt
pytanie zadane 4 października 2016 w C# przez Max78 Nowicjusz (140 p.)
0 głosów
1 odpowiedź 146 wizyt
pytanie zadane 3 października 2016 w C# przez Max78 Nowicjusz (140 p.)
+1 głos
1 odpowiedź 204 wizyt
pytanie zadane 25 września 2016 w C# przez jankustosz1 Nałogowiec (35,880 p.)

92,454 zapytań

141,262 odpowiedzi

319,098 komentarzy

61,854 pasjonatów

Motyw:

Akcja Pajacyk

Pajacyk od wielu lat dożywia dzieci. Pomóż klikając w zielony brzuszek na stronie. Dziękujemy! ♡

Oto polecana książka warta uwagi.
Pełną listę książek znajdziesz tutaj.

Akademia Sekuraka

Akademia Sekuraka 2024 zapewnia dostęp do minimum 15 szkoleń online z bezpieczeństwa IT oraz dostęp także do materiałów z edycji Sekurak Academy z roku 2023!

Przy zakupie możecie skorzystać z kodu: pasja-akademia - użyjcie go w koszyku, a uzyskacie rabat -30% na bilety w wersji "Standard"! Więcej informacji na temat akademii 2024 znajdziecie tutaj. Dziękujemy ekipie Sekuraka za taką fajną zniżkę dla wszystkich Pasjonatów!

Akademia Sekuraka

Niedawno wystartował dodruk tej świetnej, rozchwytywanej książki (około 940 stron). Mamy dla Was kod: pasja (wpiszcie go w koszyku), dzięki któremu otrzymujemy 10% zniżki - dziękujemy zaprzyjaźnionej ekipie Sekuraka za taki bonus dla Pasjonatów! Książka to pierwszy tom z serii o ITsec, który łagodnie wprowadzi w świat bezpieczeństwa IT każdą osobę - warto, polecamy!

...