Video of the week

This is a must-watch video about one of us trying to reach the stars :-)

Well done #HRejterzy

WPF ( Threads, ControlContainer, System.Windows.Application.Current.Shutdown(); )

Dziś chciałbym przybliżyć pojęcie chyba najlepszej metody radzenia sobie z wątkami w WPF oraz dodać kilka słów o tym jak nie tworzyć miliarda okien przy pracy nad aplikacją typu instalator oraz słówko na temat

System.Windows.Application.Current.Shutdown();

1. Notatka będzie w języku polskim, staram się w miarę jasno przedstawić swój punkt widzenia na wątki w WPF i po wielu testach uważam, że rozwiązanie, które opisałem poniżej można śmiało propagować. Temat jest dosyć popularny na zagranicznych forach, na codeproject itp - analizując własną literaturę, zebrałem co się dało i zdecydowałem, że we własnych gratach będę propagować wątki w ten właśnie sposób - obiektowy ;-)

Wątki to istotna kwestia - chociaż kiedyś spotkałem się z szeregiem aplikacji w przedsiębiorstwie i nawet Google nie znalazło w kodzie tego oprogramowania chociażby jednego osobnego wątku (prócz głównego). Do rzeczy - jak najlepiej komunikować się z pobocznego wątku z kontrolkami z innych wątków - tutaj przyjmijmy - wątek poboczny chce komunikować się z kontrolkami z wątku głównego. Czyli przykład -

mainwindow

Po uruchomieniu przycisku osobny wątek rozpocznie wypełniać pasek postępu a na zakończenie wątku w kontrolce TexBox na górze zostanie wyświetlony komunikat.

progressbar

textboxmessage

Jak sobie z tym najlepiej poradzić? - Krok po kroku - zakładam, że utworzyłeś projekt i masz w nim dowolną definicję głównego okna a zatem i klasy, okey - dodaj nową klasę - KLASĘ WĄTKU! (poniżej podaje trochę szerszy komentarz do kodu)

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Windows.Controls;
  6. using System.Threading;
  7.  
  8. namespace ThreadExample
  9. {
  10. public class KlasaWatku
  11. {
  12. public TextBox textBoxWKlasieWatku;
  13. public ProgressBar progressBarWKlasieWatku;
  14.  
  15. public KlasaWatku()
  16. {
  17. // ..
  18. }
  19.  
  20. public delegate void OnMetodaWatkuComplete();
  21.  
  22. public event OnMetodaWatkuComplete OnMetodaComplete;
  23.  
  24. public void MetodaWatku()
  25. {
  26. for (int i = 1; i <= 100; ++i)
  27. {
  28. Thread.Sleep(200);
  29. this.progressBarWKlasieWatku.Dispatcher.Invoke(
  30. System.Windows.Threading.DispatcherPriority.Normal,
  31. new Action(
  32. delegate()
  33. {
  34. this.progressBarWKlasieWatku.Value = i;
  35. }));
  36. }
  37.  
  38. OnMetodaComplete();
  39. }
  40.  
  41. public void KlasaWatkuOnMetodaComplete()
  42. {
  43. this.textBoxWKlasieWatku.Dispatcher.Invoke(
  44. System.Windows.Threading.DispatcherPriority.Normal,
  45. new Action(
  46. delegate()
  47. {
  48. this.textBoxWKlasieWatku.Text = "Done ;-)";
  49. }));
  50. }
  51. }
  52. }

 

threadsolution

 

Uwaga - jeśli chcesz, aby wątek miał dostęp do kontrolek z innego wątku (np. do kontrolek z wątku głównego jak tutaj) - koniecznie zadeklaruj wewnątrz tej klasy pola o takim samym typie jak kontrolka, z którą chcesz się komunikować z wątku pobocznego (lub inaczej - wątku innego niż główny). W moim przykładzie wątek wypełnia pasek postępu (zwiększa jego właściwość Value) oraz komunikuje sie z kontrolką TextBox - obie te kontrolki znajdują się w oknie głównym, czyli tworzone są w wątku głównym. Zauważ, że w mojej klasie KlasaWatku przygotowałem sobie wcześniej takie dwa pola - to bardzo ważne i to jest już schemat, który za chwilę rozbudujemy.

 

W klasie wątku tworzysz METODĘ WĄTKU - czyli serce nowego wątku - to po co za chwilę zostanie stworzony by funkcjonować w naszym RAMie. U mnie w metodzie wątku wypełniam pasek postępu z okna głównego (wątku głównego). Zwróć uwagę na specyfikę wywołania metody Invoke poprzez Dispatcher (trochę inaczej niż w WinForms)

  1. for (int i = 1; i <= 100; ++i)
  2. {
  3. Thread.Sleep(200);
  4. this.progressBarWKlasieWatku.Dispatcher.Invoke(
  5. System.Windows.Threading.DispatcherPriority.Normal,
  6. new Action(
  7. delegate()
  8. {
  9. this.progressBarWKlasieWatku.Value = i;
  10. }));
  11. }

W klasie wątku deklaruję również dodatkowo 3 elementy (można bez nich żyć i nie są one tutaj kwestią pierwszoplanową): delegat (delegate aka middle-man) , zdarzenie (event) oraz metodę, która zostanie wywołana kiedy zajdzie potrzeba obsłużenia zdarzenia (event) - sam zadecydujesz, kiedy wystrzelisz taki event a system sam zwróci się do metody, która ten event obsłuży (przypominam - Twojej metody). Po co nam delegat i event oraz ta metoda? - dobrą praktyką jest dać innym czy sobie znać, że wątek zakończył swoje działanie ("Done ;-)"). Zapewne nie raz dodawałeś metodę do obsługi zdarzenia Click dla Button'a - tutaj jest analogicznie, nasza klasa KlasaWatku to 'taka sama klasa' jak System.Windows.Controls.Button, która może posiadać zdarzenia jak Button i któremu to zdarzeniu można przypisać metodę, która zostanie wywołana, kiedy wywołany zostanie event (tutaj OnMetodaComplete() powoduje wystrzał eventu a co za tym idzie uruchomienie metody obsługującej to zdarzenie - patrz wyżej KlasaWatku, w metodzie wątku na koniec wywoływane jest to zdarzenie.

  1. public delegate void OnMetodaWatkuComplete();
  2.  
  3. public event OnMetodaWatkuComplete OnMetodaComplete;
  4. public void KlasaWatkuOnMetodaComplete()
  5. {
  6. this.textBoxWKlasieWatku.Dispatcher.Invoke(
  7. System.Windows.Threading.DispatcherPriority.Normal,
  8. new Action(
  9. delegate()
  10. {
  11. this.textBoxWKlasieWatku.Text = "Done ;-)";
  12. }));
  13. }

Ponownie mamy tutaj okazję zobaczyć jak metoda z wątku pobocznego komunikuję się za pośrednictwem Dispatchera z kontrolką z innego wątku.

Wróćmy do okna głównego i definicji tej klasy, z której za chwilę wywołamy wątek, wcześniej tworząc obiekt klasy wątku i ODPOWIEDNIO GO INICJALIZUJĄC:

  1. public partial class MainWindow : Window
  2. {
  3. public MainWindow()
  4. {
  5. InitializeComponent();
  6. }
  7.  
  8. public void MetodaWKtorejWywolaszWatek()
  9. {
  10. KlasaWatku kw = new KlasaWatku();
  11. kw.textBoxWKlasieWatku = this.tb1;
  12. kw.progressBarWKlasieWatku = this.pb1;
  13. kw.OnMetodaComplete +=
  14. new KlasaWatku.OnMetodaWatkuComplete(kw.KlasaWatkuOnMetodaComplete);
  15.  
  16. ThreadStart tS = new ThreadStart(kw.MetodaWatku);
  17. Thread t = new Thread(tS);
  18. t.SetApartmentState(ApartmentState.STA);
  19. t.Start();
  20. }
  21.  
  22. private void bt1_Click(object sender, RoutedEventArgs e)
  23. {
  24. this.MetodaWKtorejWywolaszWatek();
  25. }
  26. }

Można rzec, że przypisując wartości dla pól kw.textBoxWKlasieWatku oraz kw.progressBarWKlasieWatku rzucamy bezpieczny pomost dla komunikacji pochodzącej z różnych wątków. Dodajemy obsługę dla zdarzenia OnMetodaComplete (to jest event) - metoda, która obsłuży to zdarzenie wyświetla komunikat "Done ;-)" w kontrolce TextBox (a sama metoda działa w naszym osobnym wątku!)

Dalsza część to już standard - definicja obiektu ThreadStart (lub ParameterizedThreadStart jeśli mamy parametr dla funkcji wątku - tutaj nie mamy), definicja Thread oraz ustawienie STA no i ruszamy z wątkiem poprzez wywołanie t.Start();

STA - Single Thread Apartment - o co chodzi? To tak zwany przedział jednowątkowy w .NET, oprócz tego przedziału istnieją jeszcze MTA - przedział wielowątkowy oraz Przedział neutralny. To różne podejścia do programowania z użyciem wątków. STA oznacza, że w programie możemy utworzyć sobie wątek a w nim jakiś obiekt - jak dotąd brzmi zupełnie normalnie - ale ten obiekt należy tylko do tego wątku i nie można od tak uzyskać dostępu do tego obiektu z obcego wątku. Aby to zrobić z innego wątku, należy przebić się do danego przedziału jednowątkowego i dołączyć do kolejki pod tytułem "Ja w sprawie dostępu do obiektu". Tak mniej więcej działa pod spodem powyższy kod. W przypadku MTA, kiedy utworzysz obiekt w jednym z wielu wątków, to każdy wątek może bezczelnie domagać się dostępu do obiektu i tutaj uwaga - należy samodzielnie zaimplementować synchronizację dostępu do takiego gorącego obiektu.

2. ControlContainer czyli sposób na upakowanie kilku okien w jedno

Kiedy uruchamiany jest instalator aplikacji (dedykowany lub typu Install Creator) zauważyć można, że w tych oknach dzieje się w sumie niewiele prócz komunikatów i pasków postępu. Można tworzyć osobne okna dla każdego etapu - tak właśnie zrobiłem ostatnim razem, jednak stwierdziłem, że można to zrobić inaczej - bez wykorzystywania metod ShowDialog() i Hide() dla kolejnych okien:

  1. public class ControlContainer
  2. {
  3. List<object> container;
  4.  
  5. public ControlContainer()
  6. {
  7. this.container = new List<object>();
  8. }
  9.  
  10. public void AddControl(object control)
  11. {
  12. this.container.Add(control);
  13. }
  14.  
  15. public void ShowControls()
  16. {
  17. for (int i = 0; i < this.container.Count; ++i)
  18. {
  19. ((UIElement)container[i]).Visibility = Visibility.Visible;
  20. }
  21. }
  22.  
  23. public void HideControls()
  24. {
  25. for (int i = 0; i < this.container.Count; ++i)
  26. {
  27. ((UIElement)container[i]).Visibility = Visibility.Hidden;
  28. }
  29. }
  30. }

Tworzę List(ę) object(ów) czyli swoistą przechowalnie dla każdego obiektu w .NET - taka lista oznacza, że mogę zapakować do niej i Button i TextBlock i TextBox i Label i wszystko co na początku swojego łańcucha ewolucji posiada object. W tym obiekcie - kontenerze na kontrolki przetrzymuję kontrolki z pierwotnego okna nr 1, w drugim kontenerze przechowuję kontrolki z pierwotnego okna nr 2 itd. Ciekawą właściwością tej mini klasy jest to, że jednym uruchomieniem metody można wszystkie wyświetlić i zabunkrować w oknie ;-) - każda kontrolka pochodzi od UIElement a więc posiada właściwość Visibility - operacja rzutowania na elementach listy w magiczny sposób w jednej pętli pokazuje i ukrywa cały kontener kontrolek.

controlcontainer

Powyżej widać wersję roboczą okna w Visual Studio. Taki kontener może posiadać dodatkowe pola definiujące położenie kontrolek, ponieważ w przypadku wielu okien należałoby kontrolki rozmieścić wszystkie odrazu co mogłoby spowodować bałagan na płótnie - gridzie - panelu - innym. Ustawienie w oknie Properties kontrolki właściwości Visibility na Hidden nie powoduje jej ukrycia w fazie projektowej, zatem zdefiniuj położenie - zapisz i 'przesuń' kontrolki robiąc miejsce następnym. A może obiekt Page + Frame w WPF jest lepszy? ;> - podoba mi się Perlowe motto - "There's More Than One Way To Do It" (warto znać zatem więcej niż jeden sposób)

gotopage2

backtopage1

3. System.Windows.Application.Current.Shutdown();

Co się dzieje, kiedy uruchamiamy this.Close() w metodzie klasy naszego jedynego okna? - aplikacja kończy działanie oczywiście, ale dlaczego aplikacja skoro zamykam tylko okno? A jeśli mam dwa i więcej okien - co wtedy? Czy jeśli wyświetlę sobię 3 nowe okna (utworzone dynamicznie) to czy zamknięcie pierwszego spowoduje zamknięcie pozostałych? A może zamknięcie jednego z 3 nowych spowoduje zamknięcie wszystkich? Hmmm.

Wywołanie this.Close() w metodzie naszego jedynego okna powoduje zamknięcie aplikacji ponieważ domyślnie właściwość System.Windows.Application.Current.ShutdownMode jest ustawiona na ShutdownMode.OnMainWindowClose (co ciekawe ta właściwość jest GET i SET co oznacza, że możesz uczynić głównym oknem własnej aplikacji każde okno) - zamknięcie aplikacji następuje wraz z zamknięciem okna głównego - takie jest przesłanie.

  1. public partial class Window1 : Window
  2. {
  3. public Window1()
  4. {
  5. InitializeComponent();
  6. System.Windows.Application.Current.ShutdownMode = ShutdownMode.OnMainWindowClose;
  7. this.Title = DateTime.UtcNow.ToString();
  8. label1.Content = "Okno główne: " + System.Windows.Application.Current.MainWindow.Title;
  9. }
  10.  
  11. private void button1_Click(object sender, RoutedEventArgs e)
  12. {
  13. Window1 w = new Window1();
  14. w.Show();
  15. }
  16. }

W przykładzie powyżej tworzę prosty kod, który polega na tym, że na formie mam Button, który dynamicznie tworzy nowe okno. W konstruktorze okna ustawiam dodatkowo tytuł okna - żeby móc je chronologicznie odróżnić oraz na środku każdego okna wyświetlam tytuł okna głównego (podwójne ułatwienie rozpoznania)

shutdownmode

Teraz spróbuję zamknąć nowe okno i aplikacja nie zakończy działania. Ok zmieńmy właściwość System.Windows.Application.Current.ShutdownMode na ShutdownMode.OnLastWindowClose - uruchamiam teraz aplikację i zamykam okno główne aplikacji:

oknoglowne

Okey, teraz zamknąłem główne okno i co? I nic - aplikacja zakończy działanie dopiero po zamknięciu ostatniego okna w programie. Jest jeszcze trzecia wartość dla właściwości System.Windows.Application.Current.ShutdownMode - w skrócie chodzi o to, że aplikacja zakończy działanie dopiero po jawnym wywołaniu metody System.Windows.Application.Current.Shutdown();