Condividi tramite


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

View


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: