BackgroundWorker
In dit tweede deel gaan we het hebben over het inschakelen van een BackgroundWorker die sinds .Net 2.0 aanwezig is in de .Net api's. Het eerste deel kan u hier nalezen:
linkje
We bespraken daarin waardoor het komt dat je GUI blokkeert wanneer je een langdurige berekening doet, of waarom je niets meer kan doen wanneer je programma blokkeert omdat het wacht op invoer of een verbinding via een socket. De oplossing daar aangeboden was gebaseerd op het gebruik van delegates. Op het einde had ik al aangegeven dat er nog een andere manier was: de BackgroundWorker. Aangezien er interesse was om hierover meer te weten te komen gaan we het daarover nu hebben in dit vervolg. Veel plezier ermee!
Inleiding
Een BackgroundWorker is net wat de naam zegt: een werker die op de achtergrond werkt. Je gooit er een stapel werk naartoe en hij geeft je informatie over hoe ver het werk staat en wanneer en met welk resultaat het werk afgerond werd. Dit alles gebeurd met het event-systeem. We zullen stap voor stap bekijken hoe we deze BackgroundWorker kunnen inschakelen in de code.
Hiervoor kies ik voor de grafische component die de BackgroundWorker voorstelt. Je vindt hiervoor het volgende icoontje terug in de toolbar:
Dat kun je op het form slepen zodat je daar een BackgroundWorker kunt zien staan (niet op het form maar eronder, omdat het eigenlijk geen grafisch component is) Je zou ook alles zelf kunnen programmeren (BackgroundWorker instantiëren e.d. ) en de functies toewijzen, maar het voordeel van deze 'grafische' manier is dat je de methodes automatisch kan laten aanmaken in Visual Studio.
De functionaliteit
De BackgroundWorker kan een aantal zaken doen, zoals in de inleiding aangegeven: werk uitvoeren, status-informatie geven en laten weten wanneer en hoe de taak afgerond werd. We kunnen ons herinneren uit het eerste deel dat code die door een andere thread wordt uitgevoerd geen toegang heeft tot GUI-componenten die in een andere thread werden aangemaakt. Dat is uiteraard niet anders voor de code die uitgevoerd wordt door de BackgroundWorker, maar hier zien we iets bijzonders.
De BackgroundWorker heeft 2 speciale events, één om de status-informatie door te geven en één om te laten weten wanneer en hoe de taak afgerond werd. Deze worden intern door de BackgroundWorker doorgegeven naar bv de thread waarop de BackgroundWorker werd gestart (via de Invoke) en op die manier kan men vanuit die 2 bijhorende codesegmenten wél de GUI updaten! Meer moeten we dus niet hebben om onze applicatie nog eens na te bouwen, maar nu met een BackgroundWorker.
Instellen
De BackgroundWorker die je op het form gesleept hebt, kan je nu ook gaan instellen in Visual Studio. Dat doe je met behulp van het event- en property-venster. Zo kan je de eigenschappen WorkerReportsProgress instellen op true zodat de BackgroundWorker de statusinformatie door zal geven. Zo is er ook de WorkerSupportsCancellation, die toelaat dat het werk van de BackgroundWorker kan onderbroken worden. Zet deze beide op true voor ons voorbeeld.
Als je die eigenschappen hebt ingesteld kijk je naar de events die horen bij de BackgroundWorker. Door zie je de 3 events. Door te dubbelklikken in het vak na een event wordt automatisch een afhandelingsmethode gekoppeld aan de BackgroundWorker en zie je de header van die methode verschijnen in de editor. Doe dit voor alle 3 de events en bekijk alvast even de aangemaakte methoden.
Voor de rest bevat ons form 2 knoppen en een progressbar, allen met standaard namen (ook voor de BackgroundWorker), waarbij button1 de start-knop is en button2 de stop-knop. In dit voorbeeld gebruiken we niet meer die richtextbox uit het eerste deel omdat het maar illustratief was.
De taak toekennen
Als we de gegenereerde code bekijken zien we de DoWork methode. Dat is de methode die het werk zal uitvoeren op de achtergrond. (Herinner u dat die code geen toegang heeft tot de GUI-componenten.)
Voeg hieraan volgende code toe:
Code:
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
for (int i = 0; i < 200000; i++) {
if (backgroundWorker1.CancellationPending) {
e.Cancel = true;
return;
}
backgroundWorker1.ReportProgress(0, i);
Thread.Sleep(10); //We vertragen het werk om een mooier resultaat te kunnen tonen
}
e.Result = 12345; //Enkel ter illustratie
}
Deze doet enigszins ander werk dan in het eerste deel, maar het principe blijft: we doorlopen een lus waardoor de thread de hele tijd bezig zal blijven.
Wat er regelmatig moet gebeuren is kijken of er nergens een cancelling gebeurd is van de BackgroundWorker. Doorom moet je regelmatig kijken naar de boolean CancellationPending. Staat deze op true dan werd het werk geannuleerd. Vandaar:
Code:
if (backgroundWorker1.CancellationPending) {
e.Cancel = true;
return;
}
Als het werk werd geannuleerd zetten we eerst nog eens de Cancel-boolean op true om later aan te geven dat we het werk hebben gestaakt en doen we return om de DoWork te beëindigen.
Als het (nog) niet geannuleerd werd zeggen we een update van de status te melden:
Code:
backgroundWorker1.ReportProgress(0, i);
Het eerste argument wordt verondersteld een percentage te zijn an 0 tot 100 dat aangeeft hoe ver het werk zit. Aangezien we hier verder niets doen laten we het gewoon 0. Optioneel kan je nog een Object meegeven wat eigen informatie is. In ons geval geven we mee hoever we staan in de for lus. Dit zullen we later kunnen opvragen.
Indien de BackgroundWorker niet geannuleerd werd zal het werk beëindigen na de for lus in ons geval. Dan kunnen we nog een waarde opgeven die het eindresultaat van het werk voorstelt. Dat speelt in ons geval niet veel rol, dus geven we een arbitraire waarde mee:
Code:
e.Result = 12345; //Enkel ter illustratie
Nu weet de BackgroundWorker al welke code hij moet uitvoeren, maar welke code wordt uitgevoerd bij het beëindigen en bij het melden van de status? Dat leg ik hieronder uit:
De statusmelding
Als we in DoWork de code:
Code:
backgroundWorker1.ReportProgress(0, i);
zal de BackgroundWorker de ProgressChanged methode aanroepen. Vul die als volgt aan:
Code:
private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
progressBar1.Value = ((int)e.UserState)%100;
}
Via e kunnen we opvragen wat het tweede argument was (de i die we meegaven). Dat was een int, dus we casten het terug naar een int en dan kunnen we die waarde gebruiken om de progressbar in te stellen. Merk hierbij op dat we hieruit wel de GUI-componenten kunnen aanspreken! Dit komt omdat de BackgroundWorker ervoor zorgt dat ProgressChanged op de thread van de GUI-componenten wordt uitgevoerd.
De eindafhandeling
Als het werk wordt beëindigt, ofwel door cancellen ofwel omdat het werk gedaan is, dan wordt een andere methode aangeroepen: RunWorkerCompleted. Hierin kunnen we nog wat zaken instellen nadat het werk gestopt is. Vul dit aan als volgt:
Code:
private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Cancelled) MessageBox.Show("Cancelled!");
else if (e.Error != null) MessageBox.Show("Error!");
else MessageBox.Show("Het resultaat is: " + e.Result);
}
Ter illustratie laten we zien wat er bijvoorbeeld mogelijk is: je kan opvragen of de taak gecancelled werd, of er een error was, ofwel , indien het werk gewoon gedaan was, het eindresultaat opvragen. Ook vanuit deze methode kunnen we de GUI-componenten aanspreken en eventueel het resultaat in een tekstveld ofzo laten verschijnen.
Samenvatting
Om het nog eens samen te vatten: voer het werk dat je wil doen uit in DoWork. Als je je GUI wil aanpassen of instellen doe je dit in ProgressChanged en roep je in DoWork de methode ReportProgress aan (die dan zorgt voor het aanroepen van ProgressChanged). Na het beëindigen kan je nog wat zaken laten afhandelen in RunWorkerCompleted.
De GUI-componenten kunnen worden aangepast in:
ProgressChanged
RunWorkerCompleted
Maar niet in:
DoWork
Alle aanmerkingen, aanvullingen, fouten, ... zijn uiteraard welkom!
Ik heb ook de tutorial in pdf formaat hieraan toegevoegd:
BackgroundWorker.pdf (Grootte: 103,35 KB / Aantal keer gedownload: 43)
En de volledige code 
Code:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Threading;
namespace WindowsApplication1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
for (int i = 0; i < 200000; i++) {
if (backgroundWorker1.CancellationPending) {
e.Cancel = true;
return;
}
backgroundWorker1.ReportProgress(0, i);
Thread.Sleep(10);
}
e.Result = 12345; //Enkel ter illustratie
}
private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
progressBar1.Value = ((int)e.UserState)%100;
}
private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Cancelled) MessageBox.Show("Cancelled!");
else if (e.Error != null) MessageBox.Show("Error!");
else MessageBox.Show("" + e.Result);
}
private void button1_Click(object sender, EventArgs e)
{
//Start de backgroundworker
backgroundWorker1.RunWorkerAsync();
}
private void button2_Click(object sender, EventArgs e)
{
//Stop de backgroundworker
backgroundWorker1.CancelAsync();
}
}
}