C# Blockieren einer WinForm GUI vermeiden

Ein Thema mit dem ich mich schon längst einmal auseinander setzen wollte ist dieses.
Warum Blockiert eine GUI und was kann man dagegen tun?

Grob gesehen kann man sagen, dass eine GUI blockiert, wenn sie arbeitet.
Die GUI läuft in dem so genannten GUI-Threat – dies ist EIN thread in dem alles abgearbeitet wird, was in der GUI läuft.
Setzt man z.b. einen Sleep mit z.B. 1000ms in eine Form, reagiert diese eine ganze Sekunde lang nicht.
Hierbei wird der Sleep im GUI Thread ausgeführt, welcher auf rückmeldung des Sleeps wartet um die Form danach neu zeichnen zu können.

In einem etwas minimal Komplexeren Beispiel-Projekt möchte ich nun Zeigen wie man so etwas umgehen kann.

Beispiel Quellcode (SVN): http://frickelblog.googlecode.com/svn/trunk/samples/20101125_ThreadTest/

Als Grundlage nehmen wir uns einfach mal eine Form mit einem Button und einer Textbox drauf.
Wir wollen jetzt etwas in die Textbox schreiben, wenn wir auf den Button geklickt haben.

Dies Könnte man natürlich sehr einfach mit folgendem Code tun:

1
2
3
4
5
6
7
private void button1_Click(object sender, EventArgs e)
{
	for (int i = 0; i < 10; i++)
	{
		textBox1.Text = textBox1.Text + DateTime.Now.ToString() + Environment.NewLine;
	}
}

In der TextBox bekommt man dann folgendes angezeigt:

1
2
3
4
5
6
7
8
9
10
24.11.2010 19:41:54
24.11.2010 19:41:54
24.11.2010 19:41:54
24.11.2010 19:41:54
24.11.2010 19:41:54
24.11.2010 19:41:54
24.11.2010 19:41:54
24.11.2010 19:41:54
24.11.2010 19:41:54
24.11.2010 19:41:54

Nun gut – das .NET wird sich eben keine Stunden damit aufhalten ein paar DateTimes in eine TextBox zu schreiben 😉

Also setzen wir Exemplarisch einen Sleep dazwischen, als Fake-Prozess der eine Sekunde lang irgendetwas arbeiten würde.

Unser Button Click:

1
2
3
4
5
6
7
8
private void button1_Click(object sender, EventArgs e)
{
	for (int i = 0; i < 10; i++)
	{
		textBox1.Text = textBox1.Text + DateTime.Now.ToString() + Environment.NewLine;
		System.Threading.Thread.Sleep(1000);
	}
}

Führen wir dies nun aus, haben wir das Anfangs erwähnte Problem: Die GUI blockiert.
Sie tut dies genau 10 Sekunden lang (so lang wie die Schleife läuft).
Ist die schleife fertig durchlaufen, hat der GUI-Thread wieder zeit die GUI neu zu zeichnen, also regaiert sie wieder.

Als Ausgabe bekommen wir nun in der TextBox folgendes:

1
2
3
4
5
6
7
8
9
10
24.11.2010 19:44:45
24.11.2010 19:44:46
24.11.2010 19:44:47
24.11.2010 19:44:48
24.11.2010 19:44:49
24.11.2010 19:44:50
24.11.2010 19:44:51
24.11.2010 19:44:52
24.11.2010 19:44:53
24.11.2010 19:44:54

Tja – jetzt wollen wir aber, dass unser Fake-Prozess seine Dicken Ressourcenlastigen Fake-Berechnungen abarbeiten kann, wir aber nach jeder Sekunde einen Status von ihm bekommen.

Da bleibt uns wohl nichts anderes übrig als diese Berechnungen in einem extra Thread auszulagern und unseren GUI-Thread über einen Event über den Fotschritt zu informieren.

Dies soll nun unsere Fake-Thread Klasse sein um den Fake-Prozess nicht in der GUI laufen zu lassen:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace ThreadTest
{
	// Der Delegat für den Event
	public delegate void OnFakeThreadHandler();
 
	class FakeThread
	{
		public FakeThread()
		{
 
		}
 
		// Der Event an sich
		public OnFakeThreadHandler Event_OnFakeThread;
 
 
		public void test()
		{ 
			// In der methode will ich einen Timer laufen lassen, der jede Sekunde  die GUI Akualisiert
			for (int i = 0; i < 10; i++)
			{
				if (this.Event_OnFakeThread != null)
				{
					this.Event_OnFakeThread.Invoke();
				}
				System.Threading.Thread.Sleep(1000);			
			}
		}
	}
}

Und Der Code in unserer Form sieht nun mit ausgelagerten aufruf im Thread und Event so aus:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
 
namespace ThreadTest
{
	public partial class Form1 : Form
	{
 
		private FakeThread ft;
 
		public Form1()
		{
			InitializeComponent();
			ft = new FakeThread();
			ft.Event_OnFakeThread += new OnFakeThreadHandler(OnFakeThread);
		}
 
		private void button1_Click(object sender, EventArgs e)
		{
			System.Threading.Thread t = new System.Threading.Thread(ft.test);
			t.Start();
		}
 
		private void OnFakeThread()
		{
			textBox1.Text = textBox1.Text+DateTime.Now.ToString()+Environment.NewLine;
		}
	}
}

Und wieder versuchen wir dieses ganze Konstrukt laufen zu lassen.
Hm… nun bekommen wir einen Fehler:

Ungültiger threadübergreifender Vorgang: Der Zugriff auf das Steuerelement textBox1 erfolgte von einem anderen Thread als dem Thread, für den es erstellt wurde.

Erklärung: Der Fehler passiert jetzt aus dem Grund, weil wir versuchen aus einem NICHT-GUI Thread ein Steuerelement der GUI zu aktualisieren. Sowas sollte man vermeiden.

Umgehen kann man diesen Fehler in dem man im Konstruktor der Form mit folgender Zeile sagt, das nicht geprüft werden soll ob solche aufrufe von anderen Threads aus passieren (CheckForIllegalCrossThreadCalls = false):

1
2
3
4
5
6
7
public Form1()
{
	InitializeComponent();
	CheckForIllegalCrossThreadCalls = false;
	ft = new FakeThread();
	ft.Event_OnFakeThread += new OnFakeThreadHandler(OnFakeThread);
}

Lassen wir unser Programm nun noch ein mal laufen, funktioniert scheinbar alles, so wie gewünscht.
Die GUI Blockiert nicht und jede Sekunde wird das aktuelle DateTime aufgerufen.
Was wir aber nun im Konstruktor gemacht haben ist ziemlich böse und einfach nur unsauber!

Also muss eine andere Lösung her.
Der Grundgedanke ist hierbei noch mal folgender:
Wir können nicht direkt aus einem anderen Thread auf ein Steuerelement der GUI zugreifen, weil diese in einem eigenständigen Thread läuft.
Die Lösung ist also, dem GUI-Thread anzuweisen das GUI-Steuerelement für den anderen Thread zu aktualisieren.

Hierzu ist ein „Invoke“ erforderlich.
Mit der Eigenschaft „InvokeRequired“ wird abgefragt ob ein Invoke erforderlich ist.
Wenn einer erforderlich ist, dann rufen wir die Selbe methode noch mal auf, nur eben als Invoke, statt direkt.

Das Ganze sieht dann für die Event-Methode „OnFakeThread“ wie folgt aus:

1
2
3
4
5
6
7
8
9
10
11
12
private void OnFakeThread()
{
	if (this.InvokeRequired)
	{
		// Wenn Invoke nötig ist, ...
		// dann rufen wir die Methode selbst per Invoke auf
		this.Invoke(new OnFakeThreadHandler(OnFakeThread));
		return;
	}
 
	textBox1.Text = textBox1.Text + DateTime.Now.ToString() + Environment.NewLine;
}

Und – wenn wir jetz das CheckForIllegalCrossThreadCalls aus dem Konstruktor wieder entfernen und das Programm noch ein letztes mal laufen lassen, dann stellen wir fest, das nun alles so funktioniert, wie wir es uns von Anfang an vorgestellt haben 🙂

One Response to C# Blockieren einer WinForm GUI vermeiden

  1.  

    Super Beitrag – bin .net Anfänger – hat sehr geholfen: Frage – auch wenn das eine blöde Frage sein sollte: wie bekomme ich eine Fortschrittsmeldung aus dem Thread in die GUI?

leave your comment


*

Unterstütze den Frickelblog!