• 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

+1 głos
127 wizyt
pytanie zadane 18 sierpnia 2016 w C# i .NET przez użytkownika jankustosz1 Gaduła (4,400 punkty)

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ź

+3 głosów
odpowiedź 18 sierpnia 2016 przez użytkownika Sebastian Fojcik Nałogowiec (39,010 punkty)
wybrane 19 sierpnia 2016 przez użytkownika jankustosz1
 
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 użytkownika jankustosz1 Gaduła (4,400 punkty)
Wiielkie dzięki!

Napracowałeś się.

Podobne pytania

0 głosów
0 odpowiedzi 24 wizyt
pytanie zadane 4 października 2016 w C# i .NET przez użytkownika Max78 Nowicjusz (140 punkty)
0 głosów
1 odpowiedź 31 wizyt
pytanie zadane 3 października 2016 w C# i .NET przez użytkownika Max78 Nowicjusz (140 punkty)
+1 głos
1 odpowiedź 40 wizyt
pytanie zadane 25 września 2016 w C# i .NET przez użytkownika jankustosz1 Gaduła (4,400 punkty)
...