Il BackgroundWorker (d'ora in poi BW) è
un componente nuovo disponibile nel FX2.0 di .NET, per cui vale la pena
approfondire il suo funzionamento con qualche dettaglio in più. Mi piacerebbe
dimostrare veramente che il task gestito dal BW gira davvero in un thread
separato rispetto a quello della UI. Ed ancora, vedremo come implementare
l'annullamento di un'operazione lunga, tramite il semplice click di un pulsante.
Per far questo, ho creato un progetto Windows Forms completamente nuovo: a
questo progetto ho aggiunto una semplice WF Form1, con una
ListBox ancorata su tutti e quattro i lati, in modo tale da poter
ridimensionare/ingrandire la form e vedere la ListBox meglio.
Abbiamo visto ieri che il BW si basa fondamentalmente su 3 eventi, a cui
devono essere associati 3 handler differenti, che hanno scopi ovviamente
differenti. Vediamo uno per uno.
BackgroundWorker.DoWork
Questo evento
si scatena alla chiamata del metodo RunWorkerAsync, scatenato
dal click di un Button sul nostro WF. L'handler per questo evento deve contenere
il task che vogliamo venga eseguito in modo asincrono: diversi esempi su MSDN
mostrano come fare il download di un file, o come caricare un'immagine da disco
fisso. Io in questo esempio farò un semplice contatore da 1 a 50.000.000, ma sul
campo ho provato a fare il download di un'immagine dal mio sito Web e a
mostrarla in una PictureBox. In questo metodo possiamo notificare alla UI
qualche aggiornamento, oppure annullare l'operazione.
BackgroundWorker.ProgressChanged
Questo
evento viene scatenato alla chiamata del metodo
ReportsProgress, solitamente all'interno del DoWork visto
prima. Da qui, possiamo aggiornare la UI, prelevando il parametro
ProgressChangedEventArgs che può contenere quello che vogliamo noi.
BackgroundWorker.RunWorkerCompleted
Questo
evento si scatena alla fine dell'esecuzione del task asincrono. Solitamente,
avviene quando termina l'esecuzione dell'handler dell'evento DoWork. In questo
evento possiamo concludere l'operazione, magari chiamando la Dispose() di
eventuali oggetti coinvolti nel task asincrono. Oppure, notificare all'utente
che l'operazione è terminata mostrando il risultato (immagine scaricata, per
esempio).
Un po' di codice...
Come ho accennato prima, in questo
esempio ho creato un banale contatore che va avanti per molto, molto tempo. L'handler dell'evento Click del
Button btnStart è il seguente:
private void btnStart_Click(object sender, EventArgs e)
{
if (worker.IsBusy) {
MessageBox.Show("E' già in corso un'altra operazione asincrona!");
return; }
// Proprietà
worker.WorkerReportsProgress = true;
worker.WorkerSupportsCancellation = true;
// Eventi
worker.DoWork += new DoWorkEventHandler(worker_DoWork);
worker.ProgressChanged += new ProgressChangedEventHandler(updateUI);
// Comincio il caricamento
worker.RunWorkerAsync();
}
Prima osservazione
importante: se clicchiamo la prima volta, il BW comincia a fare
il suo lavoro e vediamo la ListBox che si popola con il valore del contatore. Se
clicchiamo una seconda volta, viene sollevata una
InvalidOperationException. Per evitarlo, testiamo il valore
della proprietà IsBusy, che ci ritorna true o
false in base allo stato corrente del BW: se ritorna true, avvisiamo l'utente con una bella MessageBox, che ho
volutamente messo per evidenziare che il task gira in un thread secondario.
Difatti, mentre la MessageBox (che è modale) è sullo schermo, la mia WF sullo
sfondo continua ad essere aggiornata, proprio perchè il task continua a girare
in background.
L'handler dell'evento DoWork comprende il seguente
codice:
public static void worker_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker wk = ((BackgroundWorker)sender);
for (int Counter = 0; Counter < 50000000; Counter++)
{
System.Threading.Thread.Sleep(150);
if(wk.WorkerReportsProgress)
wk.ReportProgress(0, Counter);
if (wk.CancellationPending)
break;
}
}
Qui vediamo qualcosa di interessante: il primo parametro object è il BW che stiamo gestendo, quindi possiamo farne il
casting. Poi, ecco il famigerato ciclo for. Al suo interno, faccio alcune cose:
innanzitutto metto in pausa il thread corrente (150 millisecondi) e tento di
notificare alla UI un aggiornamento. Questa operazione la posso fare solo se la
proprietà WorkerReportsProgress me lo
permette, ovvero vale true. Questa pausa mi è servita
per testare veramente - ancora una volta - che in quel punto siamo in un thread
diverso: possiamo spostare la WF, scorrere le ScrollBars, etc. etc. La pausa non
inficia assolutamente l'usabilità della WF, e questa è davvero una cosa
utile.
Inoltre, controllo il valore di CancellationPending. Se è
true, significa che è stato chiamato il metodo
CancelAsync(), quindi l'utente ha richiesto l'annullamento
dell'operazione. In questo caso, esco dal ciclo for con
un bel break. Da notare che anche in questo caso viene
comunque sollevato l'evento RunWorkerCompleted che ci permette
quindi in ogni caso di concludere il task asincrono gestendolo come necessario.
Ho avuto una difficoltà, per magari vi
riporterò in seguito: gli esempi su MSDN
(come questo) se vogliono
interrompere il task impostano semplicemente e.Cancel =
true, mentre nel mio caso non è così. Perciò, prendete alla leggera questo paragrafo, perchè
può essere che sia stato un po' impreciso.
Ad ogni evento ProgressChanged, viene ovviamente eseguito il
suo handler, che nel mio caso è un metodo privato che ho chiamato
updateUI. Il codice, banalmente, è il seguente:
void updateUI(object sender, ProgressChangedEventArgs e)
{
lstCounter.Items.Add(e.UserState.ToString());
}
Non faccio altro che aggiungere un elemento alla ListBox, prelevando il
valore del parametro di tipo ProgressChangedEventArgs e, che
viene valorizzato nel DoWork visto prima. Io lo valorizzo con la variabile
Counter che uso per il ciclo, quindi non faccio altro che prelevarla da
UserState e mostrarla. Resta un'ultima cosa da vedere, ovvero
come annullare il task in esecuzione: per questo, ho messo un Button che ho
stupidamente chiamato btnSuspend. In realtà, non sospenso un
bel nulla, interrompo l'esecuzione punto e basta. Il codice associato al
pulsante è il seguente:
private void btnSuspend_Click(object sender, EventArgs e)
{
if (worker.WorkerSupportsCancellation)
worker.CancelAsync();
}
Ancora, prima di chiamare CancelAsync, verifico che
il mio BW supporti tale operazione, tramite la sua proprietà
WorkerSupportsCancellation. La chiamata al metodo
CancelASync non fa nient'altro che impostare a true la proprietà CancellationPending, che utilizzo poi per
fare un break nel ciclo di cui sopra.
Nel mio caso ho utilizzato il BW per caricare un'immagine in modo asincrono:
ho catturato uno screenshot del mio desktop 1024x768, l'ho ingrandito di 5
volte, e l'ho salvato in BMP. Ho ottenuto un file wallpaper.bmp di 55Mb. E l'esperimento è riuscito, nel senso
che l'evento Load della WF ha cominciato a caricare l'immagine, che è apparsa
dopo un paio di secondi nella PictureBox. In questo scenario, per esempio, non
ho gestito alcun evento ProgressChanged, ma soltanto il
RunWorkerCompleted, per mostrare la bitmap sullo
schermo.