VB.NET: Metodo Invoke per aggiornare controlli WinForms da thread secondari (it-IT)
Introduzione
Come molti sviluppatori sanno per esperienza pregressa, non è possibile - sfruttando il Framework .NET - far interagire i processi lanciati su thread diversi da quello principale con i controlli che costituiscono la UI dei propri applicativi. Ci si trova cioè nella situazione in cui si può sfruttare il vantaggio del multithreading per effettuare diverse operazioni in parallelo o comunque su processi separati, ma se tali processi devono restituire un risultato grafico immediato, si presenta la difficoltà di non poter far accedere tali processi ai controlli video.
In questo breve articolo vedremo come sia possibile, mediante il metodo Invoke, disponibile per tutti i controlli tramite il namespace System.Windows.Form, realizzare tale funzionalità di refresh grafico attraverso l'uso di delegati.
Delegati
Secondo la documentazione MSDN, i delegati sono costrutti simili ai puntatori a funzione di linguaggi come C o C++. I delegati incapsulano un metodo all'interno di un oggetto. L'oggetto delegato potrà quindi essere passato a codice che esegua il metodo referenziato, metodo che pu? essere sconosciuto durante la compilazione del programma stesso. I delegati possono essere istanze di EventHandler, oggetti di tipo MethodInvoker, o qualsiasi altra forma richieda una lista vuota di parametri.
Qui a seguire osserviamo un esempio banale ma efficace del loro utilizzo.
Esempio introduttivo
Consideriamo un WinForm su cui sia presente una Label, Label2. Essa deve essere utilizzata per visualizzare un contatore numerico crescente. Dal momento che vogliamo eseguire il conteggio di tali numeri su un thread secondario, si presenta il problema di cui in oggetto. Vediamo perchè. Anzitutto, scriviamo il codice che esegua l'incremento del valore su un Task separato rispetto a quello principale, tentando di aggiornare la Label2, ed osserviamone il risultato.
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim n As Integer = 0
Dim t As New Task(New Action(Sub()
n += 1
Label2.Text = n.ToString
End Sub))
t.Start()
End Sub
In fase di esecuzione, l'eccezione sollevata riguarderà quando detto sopra: non è possibile modificare le proprietà di un oggetto (nel nostro caso, Label2) che si trova ad essere gestito da un thread diverso da quello chiamante.
Il controllo Label dispone però, come ogni altro controllo, del metodo Invoke, attraverso cui chiamare delegati verso il thread principale. Possiamo allora riscrivere il nostro metodo (questa volta, per completezza, inserendo un loop di incremento numerico, per mostrare come Invoke possa essere utilizzato anche internamente a cicli).
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim n As Integer = 0
Dim t As New Task(New Action(Sub()
For n = 1 To 60000
Label2.Invoke(Sub()
Label2.Text = n.ToString
End Sub)
Next
End Sub))
t.Start()
End Sub
Eseguendo il programma, si noterà ora che l'aggiornamento dei dati a video verrà correttamente eseguito, contestualmente alla valorizzazione della variabile numerica.
Si tratta, come anticipato, di un esempio molto ristretto e banale, ma nel contesto di un delegato è possibile eseguire un numero di istruzioni arbitrarie e di diversa complessità, rendendo possibile realizzare una qualsivoglia funzionalità rispetto ad operazioni di cross-threading.
Aggiornamento UI da task diversi
Per mostrare in modo più massivo quanto visto finora, vediamo un esempio decisamente più massiccio ed impegnativo, in termini di risorse. In questo caso, redigeremo un programma che processerà in tempi pressoché simultanei diversi file di testo, aggiornando la UI per mostrare cosa ciascun thread sta facendo in un dato momento.
Definizione dell'esempio
Disponiamo di 26 file di testo differenti, e ciascuno di essi contiene un numero variabile di parole, principalmente italiane ma non solo, le quali iniziano con una particolare lettera. Per esempio, il file dizionario_a.txt conterrà solo parole che iniziano per "a", dizionario_b.txt solo quelle che iniziano per "b", e così via. Vogliamo creare un'applicazione che abbia 26 Label, e che - lanciando un task per ciascuna lettera - proceda ad inserire ogni parola in un costrutto di tipo List(Of String). Ogni task dovrà mostrare quale parola sta processando, di conseguenza ogni thread provvederà, tramite Invoke(), all'update del contenuto della Label su cui opera.
Codice sorgente
Qui a seguire vediamo il codice sorgente nella sua interezza, per discutere successivamente dei punti salienti
Imports System.IO
Public Class Form1
Dim wordList As New List(Of String)
Public Sub AddWords(letter As Char, lbl As Label)
Using sR As New StreamReader(Application.StartupPath & "\text\dizionario_" & letter & ".txt")
While Not sR.EndOfStream
Dim word As String = sR.ReadLine
wordList.Add(word)
lbl.Invoke(Sub()
lbl.Text = word
counter.Text = wordList.Count.ToString
Me.Refresh()
End Sub)
End While
End Using
lbl.ForeColor = Color.Green
End Sub
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Me.DoubleBuffered = True
Dim _x As Integer = 8
Dim _y As Integer = 40
For ii As Integer = Asc("a") To Asc("z")
Dim c As New Label
c.Name = "Label" & ii.ToString("000")
c.Text = "---"
c.Top = _y
c.Left = _x
Me.Controls.Add(c)
_y += 20
If _y > 180 Then
_y = 40
_x += 120
End If
Dim j As Integer = ii
Dim t As New Task(Sub()
AddWords(Chr(j), CType(Me.Controls("Label" & j.ToString("000")), Label))
End Sub)
t.Start()
Next
End Sub
End Class
Il progetto è un semplice applicativo WinForms. Durante l'evento Load() del Form, vengono create le Label necessarie, e lanciati i task che utilizzeranno, ciascuno su una lettera differente, la sub AddWords() per processare il proprio dizionario (tutti i files sono presenti nel codice scaricabile di cui a seguire). Si noterà, con il loop nell'evento Load(), che ciascun task creato viene immediatamente lanciato, lasciando al sistema il compito di accordare eventuali processi non immediatamente gestibili.
Ciascuno dei task, chiamando la sub AddWords(), provvederà a: 1) aprire il file avente una data lettera, 2) leggerne ogni riga, 3) salvarne il valore nella lista wordList. Si noterà, inoltre, il richiamo del metodo Invoke() sulla Label che viene passata alla sub stessa. Particolare interessante, è che all'interno di un solo metodo Invoke() si possono gestire più aggiornamenti relativi alla UI. Nel caso specifico, si noti come viene aggiornata la Label legata alla singola routine, ma anche la Label generica denominata "counter", la quale riporta il numero di parole lette globalmente fino ad un dato momento. Inoltre, per favorire l'esposizione a video dei controlli aggiornati, viene chiamato il metodo Refresh() del Form stesso, anche se - così facendo - le performance globali risulteranno inferiori, in quanto verranno dedicati dei cicli all'aggiornamento grafico del Form e dei controlli figli.
Come è possibile vedere eseguendo il codice, o mediante il video seguente, l'aggiornamento del contenuto dei controlli avviene in modo concertato, permettendo ai singoli task di modificare i valori dei controlli che costituiscono l'interfaccia utente del nostro programma.
Video dimostrativo
Download
Il codice sorgente relativo all'esempio di cui sopra, comprensivo di files di appoggio, è liberamente scaricabile all'indirizzo:
https://code.msdn.microsoft.com/Invoke-Method-to-update-UI-5970a859
Altre lingue
Il presente articolo è disponibile nelle seguenti localizzazioni: