Technology Experience

Contenuti gestiti da Igor Damiani
posts - 949, comments - 2741, trackbacks - 15120

My Links

News

  • Questo blog si propone di raccogliere riflessioni, teoriche e pratiche, su tutto quello che riguarda il world-computing che mi sta attorno: programmazione in .NET, software attuale e futuro, notizie provenienti dal web, tecnologia in generale, open-source.

    L'idea è quella di lasciare una sorta di patrimonio personale, una raccolta di idee che un giorno potrebbe farmi sorridere, al pensiero di dov'ero e cosa stavo facendo.

    10/05/2005,
    Milano

Archives

Post Categories

Generale

[70-536, #20] Qualche dettaglio in più sul BackgroundWorker

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.

powered by IMHO 1.2

Print | posted on martedì 28 febbraio 2006 15:16 | Filed Under [ Esame 70-536 ]

Feedback

Gravatar

# [70-526, #6] Carrellata: ToolTip, ProgressBar ed HelpProvider

15/01/2007 13:14 | Technology Experience
Comments have been closed on this topic.

Powered by:
Powered By Subtext Powered By ASP.NET