FAQ: Hoe kan ik m'n GUI deblokkeren?
Deze vraag komt de laatste tijd geregeld terug op het forum: waarom blokkeert mijn programma en komt er niets meer in m'n controls? Tijd dus voor een FAQ-tutorial die het probleem en de oplossing uitlegt
Het probleem
In deze tutorial gaan we een vaak voorkomend probleem behandelen: het freezen van de UI bij het uitvoeren van een taak. Zo'n taak kan bestaan uit een hele reeks intensieve berekeningen, maar ook bijvoorbeeld het wachten op een binnenkomende verbinding (blocking IO) in een client-server applicatie, waardoor je niets meer kan doen.
Om het probleem duidelijker te illustreren maken we volgend programma: een programma bevat een form met 2 buttons (“Start� en “Stop�), een richttextbox en een progressbar (progressbar ingesteld met step op 1) Voor de eenvoud en om het eenvoudig zelf te doen heb ik gewoon de standaard naamwaarden gebruikt voor de componenten. Button1 is de start knop en Button2 is de stopknop.
De code in het formulier is heel eenvoudig en ziet er als volgt uit:
Code:
private Boolean doCalculation = true;
public Form1()
{
InitializeComponent();
}
//Start button
private void button1_Click(object sender, EventArgs e)
{
doCalculation = true;
doCalculations();
}
private void doCalculations() {
int number = 0;
int result = 0;
for (; doCalculation; number++) {
result = number * number;
richTextBox1.AppendText("Status: " + number % 100);
progressBar1.Value = number % 100;
}
}
//Stop button
private void button2_Click(object sender, EventArgs e)
{
doCalculation = false;
}
Bij het klikken op de startknop gaat het programma aan het rekenen. Dit is maar een arbitrair voorbeeld wat absoluut niets zinnigs doet, het is enkel om de processor bezig te houden

Dit doet het zolang de doCalculation-boolean true is. Deze kan false gemaakt worden door op de stop knop te drukken.
Als je dit programma echter uitvoert, en je wil op de stopknop drukken, dan merk je dat die stopknop niet reageert! Dit komt omdat alle code in dezelfde thread wordt uitgevoerd. Hierdoor kan er binnen de thread maar 1 stuk code tegelijkertijd worden uitgevoerd. Aangezien het programma volop bezig is in de for lus om de berekeningen te maken, krijgt het programma gewoon geen kans om de code van de stopknop uit te voeren... (Dit is analoog bij programma's die staan te wachten op in te lezen gegevens uit bestanden, sockets, ... ) Ook als je het programma wil afsluiten krijg je een hangend programma en wordt zelfs je programma niet meer hertekend:

Hier moeten we dus duidelijk een oplossing voor vinden, aangezien dit absoluut niet goed overkomt bij de gebruiker van je programma.
De oplossing
Wat we nu moeten doen is de code in de berekeningen in een aparte thread uitvoeren. Dat kan zeer eenvoudig door volgende code te gebruiken in de startknop:
Code:
private void button1_Click(object sender, EventArgs e)
{
doCalculation = true;
Thread myFirstThread = new Thread(new ThreadStart(doCalculations));
myFirstThread.Start();
}
Op deze manier wordt een nieuwe thread gestart die als beginpunt de methode doCalculations heeft.
MAAR: als we dit uitvoeren zien we een foutmelding:
Citaat:“Cross-thread operation not valid: Control 'richTextBox1' accessed from a thread other than the thread it was created on.�
Dit komt omdat je in C# geen toegang hebt tot user controls die in een andere thread werden aangemaakt... Aangezien de controls op het formulier gemaakt werden door de code die in de main-thread loopt kunnen we er van in onze eigen thread niet meer aan om de instelling te wijzigen. Maar ook hier hebben we een oplossing voor

We maken gebruik van een delegate.
We voorzien een methode in het formulier die voor ons de instellingen zal doen:
Code:
private void setControls(int number) {
richTextBox1.AppendText("Status: " + number % 100);
progressBar1.Value = number % 100;
}
We kunnen nu nog niet gewoon volgende code uitvoeren in doCalculations, want dan blijft het probleem: we benaderen nog steeds de UI vanuit onze eigen thread en niet vanuit de mainthread waarin de componenten gemaakt werden.
Wat er moet gebeuren is een delegate aanmaken in het formulier:
Code:
public delegate void SetControlsCallback(int number);
Die dezelfde argumenten neemt als onze setControls methode. Deze zal dienst doen om die setControls methode te laten uitvoeren.
Voor code in doCalculations krijgen we nu dit:
Code:
private void doCalculations() {
int number = 0;
int result = 0;
for (; doCalculation; number++) {
result = number * number;
Object[] arguments = new Object[1];
arguments[0] = number;
this.Invoke(new SetControlsCallback(this.setControls),arguments);
}
}
Waar het hier om draait is deze code:
Code:
Object[] arguments = new Object[1];
arguments[0] = number;
this.Invoke(new SetControlsCallback(this.setControls),arguments);
Zoals te zien is maken we hier een nieuwe callback-instantie aan. Deze verwijst naar de setControls-methode. De this wijst erop dat die verwijst naar de methode in het object dat op dat moment aan het uitvoeren is (dus ons formulier).
Elke UI-component heeft een methode invoke, die kan gebruikt worden om code uit te voeren in de thread waarin die specifieke component werd aangemaakt. Dat geldt dus ook voor een formulier. Door
Code:
this.Invoke(new SetControlsCallback(this.setControls),arguments);
uit te voeren, zeggen we dus eigenlijk: voer (invoke) in de thread waarin dit (this) formulier werd aangemaakt, de code “setControls� van dit object (this) uit, met de argumenten (arguments) zoals opgegeven.
Het eindresultaat
De volledige code ziet er
als volgt uit 
private Boolean doCalculation = true;
public delegate void SetControlsCallback(int number);
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
doCalculation = true;
Thread myFirstThread = new Thread(new ThreadStart(doCalculations));
myFirstThread.Start();
}
private void doCalculations() {
int number = 0;
int result = 0;
for (; doCalculation; number++) {
result = number * number;
Object[] arguments = new Object[1];
arguments[0] = number;
this.Invoke(new SetControlsCallback(this.setControls),arguments);
}
}
private void setControls(int number) {
richTextBox1.AppendText("Status: " + number % 100);
progressBar1.Value = number % 100;
}
private void button2_Click(object sender, EventArgs e)
{
doCalculation = false;
}
Als we nu ons programma uitvoeren merken we direct dat we de stopknop nu wel kunnen gebruiken!

Er is nog een andere manier mogelijk om dit te doen: met een BackgroundWorker-control, maar dat is voor een andere tutorial
Verdere vragen, opmerkingen, ... zijn uiteraard steeds welkom