czwartek, 25 kwietnia 2024

C# - Aktualizacja kontrolek z innego wątku

W tym poście chciałbym opisać sposób aktualizacji kontrolek w aplikacji napisanej w języku C#. 


Sposób będzie przedstawiony na przykładzie aplikacji wykonującej komunikacje z płytą STM32 po SSL. Przesyłane są rozkazy pomiędzy płytą a aplikacją z danymi do odczytania z układu, lub do wprowadzenia parametrów do płytki. 

Na samym początku tworzymy klasę do której będę przekazywał dane:

  1.     public class Strefa
  2.     {
  3.         public byte HourOn { get; set; }
  4.         public byte MinuteOn { get; set; }
  5.         public byte SecondOn { get; set; }
  6.         public byte HourOff { get; set; }
  7.         public byte MinuteOff { get; set; }
  8.         public byte SecondOff { get; set; }
  9.         public bool ActiveState { get; set; }
  10.         public byte WeekDays { get; set; }
  11.         public string Password { get; set; }
  12.         public bool BlockEntrance { get; set; }
  13.     }
  14.  
  15.     public static class GetStrefa_Data
  16.     {
  17.         public static bool strefa_ReceiveResponse { get; set; }
  18.         static Int16 msgError = 0;
  19.         static Strefa[] s_Data = new Strefa[8];
  20.  
  21.         public static bool MsgError { get; }
  22.  
  23.         public static bool PutDataIntoBuffer(string recData)
  24.         {
  25.             /*
  26.              * Uzupełnienie danych w klasie, z otrzymanej ramki danych
  27.              */
  28.  
  29.             /* Ustawienie flagi, */
  30.             strefa_ReceiveResponse = true;
  31.             return true;
  32.         }
  33.  
  34.         public static Strefa GetData(byte s_Number)
  35.         {
  36.             if (s_Number == 0 || s_Number > 8)
  37.             {
  38.                 return null;
  39.             }
  40.             return s_Data[s_Number - 1];
  41.         }
  42.     }

Teraz w głównym oknie musimy stworzyć asynchroniczne zadanie oczekujące ustawienie flagi. Można je wywołać z okna form load. 

  1. private async void FormStrefa_Load(object sender, EventArgs e)
  2. {
  3.     await UpdateControlBasedOnVariableAsync();
  4. }

Niestety w formie jakiej mam to zadanie przygotowane, zadziało ono w takim przypadku tylko raz. Kolejne próby odczytu danych będą wymagały ponownego otwarcia okna. Wobec tego najrozsądniej jest wywołać to zadanie z metody Button Click, która wysyła do urządzenia informację o konieczności odczytania danych. 

  1. CancellationTokenSource cts = null;
  2.        
  3. public FormStrefa()
  4. {
  5.     InitializeComponent();
  6.     cts = new CancellationTokenSource();
  7. }
  8.  
  9. private void readDataNumberBtn_Click(object sender, EventArgs e)
  10. {
  11.     Program form = new Program();
  12.     if (cts != null)
  13.     {
  14.         cts.Cancel();
  15.         cts = null;
  16.         UpdateControlBasedOnVariableAsync();
  17.     }
  18.     SendReadDataToDevice();
  19. }

Ewentualnie można zmodyfikować funkcję asynchroniczną, tak aby po odczycie i wprowadzeniu danych do kontrolek zaczynał on działać ponownie np. czyścimy flagę odbioru danych i wracamy do oczekiwania na jej ponowne ustawienie. 

Teraz samo zadanie asynchroniczne:

  1. private async Task UpdateControlBasedOnVariableAsync()
  2. {
  3.     if (cts == null)
  4.     {
  5.         await Task.Run(() =>
  6.         {
  7.             while (!GetStrefa_Data.strefa_ReceiveResponse)
  8.             {
  9.                 if (cts == null)
  10.                 {
  11.                     Thread.Sleep(500);
  12.                 }
  13.                 else
  14.                 {
  15.                     break;                                     
  16.                 }
  17.             }
  18.         });
  19.  
  20.         if (GetStrefa_Data.strefa_ReceiveResponse)
  21.         {
  22.             for(byte i = 0; i<8; i++)
  23.             {
  24.                 Strefa strefa = GetStrefa_Data.GetData((byte)(i + 1));
  25.  
  26.                 if (GetStrefa_Data.MsgError == 0)
  27.                 {
  28.                     strefa1Active_CheckBox.Invoke((MethodInvoker)delegate
  29.                     {
  30.                         strefa1Active_CheckBox.Checked = false;
  31.                         MessageBox.Show("Blad podczas odczytu danych", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
  32.                     });
  33. /* Uzupełnienie reszty danych */
  34.                 }
  35.                 else
  36.                 {
  37.                     strefa1Active_CheckBox.Invoke((MethodInvoker)delegate
  38.                     {
  39.                         strefa1Active_CheckBox.Checked = strefa.ActiveState;
  40.                     });
  41.                 }
  42.                 GetStrefa_Data.strefa_ReceiveResponse = false;
  43.                 cts = new CancellationTokenSource();
  44.             }
  45.         }
  46.     }
  47. }

W zadaniu oczekujemy na ustawienie flagi, jak jest ustawiona to sprawdzam czy nie ma błędów i odpowiednio do otrzymanej odpowiedzi wprowadzam dane do komórek. 

Należy także pamiętać o zakończeniu zadania przy zamykaniu okna. Do tego celu wykorzystałem CancellationToken.

  1. private void FormStrefa_FormClosing(object sender, FormClosingEventArgs e)
  2. {
  3.     cts = new CancellationTokenSource();
  4. }

Jest to o tyle istotne, że w przypadku gdy zadanie oczekuje na odebranie i ustawienia danych, a one nie zostaną odebrane to nie nastąpi jego zakończenie. Można do funkcji oczekujące na odebranie danych dołożyć licznik. Tak aby po zadanym maksymalnym czasie samo się zakończyło.