Laden...
FAQ

[FAQ] Bestimmte Aktionen bis nach der laufenden GUI-Event-Behandlung verzögern

Erstellt von herbivore vor 11 Jahren Letzter Beitrag vor 11 Jahren 14.253 Views
herbivore Themenstarter:in
49.485 Beiträge seit 2005
vor 11 Jahren
[FAQ] Bestimmte Aktionen bis nach der laufenden GUI-Event-Behandlung verzögern

Hallo Community!

Das Problem

Immer wieder taucht die Frage auf, wie man bestimmte Aktionen bis nach der laufenden GUI-Event-Behandlung verzögern kann oder es werden Probleme geschildert, die sich durch eine solche Verzögerung leicht lösen lassen.

Eine solche Verzögerung ist besonders dann nötig, wenn man mitten in der Event-Behandlung deren Ziel ändern will, also wenn man zum Beispiel während eines laufenden Fokuswechsels, doch einem anderen Control den Fokus geben will oder während eines Wechsels der TabPage, doch eine andere Tabpage selektieren will.

Genereller kann man sagen, eine Verzögerung ist dann nötig, wenn die laufende Behandlung erst komplett abgeschlossen sein muss, bevor die gewünschte Änderung vorgenommen werden kann, weil vorher die Voraussetzungen noch nicht gegeben sind oder die Änderung gleich wieder zunichte gemacht werden würde. Wenn man zum Beispiel im TextBox.KeyDown oder DataGridView.EditingControlShowing die Selektion des Textfeldes ändern will, die sonst durch die noch laufende Event-Bearbeitung gleich wieder aufgehoben werden würde. Auch TabControl.Selected kommt zu früh, wenn der Wechsel der TabPage noch gar nicht vollständig abgeschlossen ist.

Manchmal werden durch eine Benutzereingabe viele gleichartige Events ausgelöst, obwohl man nur einmal auf die Benutzereingabe als Ganzes reagieren will. Bei Multiselection-ListViews kommt zum Beispiel für jeden Eintrag einzeln ListView.SelectedIndexChanged, auch wenn der Benutzer mit Shift-Click alle Einträge auf einen Rutsch selektiert hat.

Man sieht, es gibt eine ganze Reihe von Situationen und sicher noch viele weitere, in der man erst das Ende der laufenden Event-Behandlung abwarten muss, bevor man die gewünschte eigene Aktion durchführen kann.

Die Lösung (Windows Forms)

Dabei ist es ganz einfach. Statt die Methode, die die gewünschte Aktion ausführt, direkt aufzurufen, ruft man sie per Control.BeginInvoke auf. Statt

MyAction1 ();
MyAction2 ("Hallo, Welt!");

also einfach

this.BeginInvoke (new Action (MyAction1));
this.BeginInvoke (new Action <String> (MyAction2), "Hallo, Welt!");

wobei this typischerweise das aktuelle Form selbst ist, aber auch ein beliebiges anderes Form oder Control sein kann.

Die Lösung (WPF)

Unter WPF funktioniert das mit Dispatcher.BeginInvoke statt Control.BeginInvoke analog.

Die Hintergründe

Nun wird man sagen, Control.BeginInvoke ist doch dazu da, um die Ausführung einer Methode aus einem Worker-Thread in den GUI-Thread zu verlagern, und das ist auch der Hauptzweck, siehe [FAQ] Controls von Thread aktualisieren lassen (Control.Invoke/Dispatcher.Invoke). Aber man darf und kann Control.BeginInvoke auch aufrufen, wenn man sich bereits im GUI-Thread befindet. Und dann passiert genau das, was man sich wünscht, die auszuführende Aktion wird verzögert, bis die laufende Event-Behandlung abgeschlossen ist.

Warum?

Hinter den Kulissen arbeitet Windows Forms mit Windows-Nachrichten. Jede Benutzereingabe wird zu einer Windows-Nachricht (z.B. WM_KEYDOWN), die in die Nachrichtenschlange des Forms eingestellt wird. Application.Run wartet in einer Schleife darauf, dass eine neue Nachricht eingestellt wurde, um diese ans Form zu schicken (technisch gesehen wird Form.WndProc mit der Nachricht als Parameter aufgerufen). Damit beginnt die Nachrichtenverarbeitung. Und sie geschieht single-threaded. Solange die Verarbeitung läuft, hat Application.Run also wieder Pause.

Wenn während der Nachrichtenverarbeitung weitere Events ausgelöst werden, zum Beispiel weil der Benutzer die Tab-Taste gedrückt hat und dadurch ein Fokuswechsel ausgelöst wird (Enter, GotFocus, Leave, Validating, Validated, LostFocus), läuft das nicht über die Nachrichtenschlange, sondern per direktem Funktionsaufruf.

Control.BeginInvoke tut nun nichts anderes, als eine passende Nachricht in die Nachrichtenschlange einzustellen, so dass diese vom Form bei nächster Gelegenheit - also eben sobald die laufende Nachrichtenverarbeitung abgeschlossen ist und keine anderen Nachrichten davor in der Schlange warten - bearbeitet werden kann.

Win32

Wer die Win32-Funktionen PostMessage und SendMessage kennt, kann sich das ganze auch so erklären: Control.BeginInvoke verwendet intern PostMessage, welches die Nachricht in die Queue einstellt, wogegen innerhalb einer laufenden Nachrichtenverarbeitung weitere Events normalerweise per SendMessage, also per direkten Funktionsaufruf, angestoßen werden. Dadurch verzögert Control.BeginInvoke die Ausführung im Normalfall, bis die laufende Event-Behandlung (im Beispiel also der komplette Fokuswechsel) abgeschossen ist.

Eine Warnung zum Schluss

"bis nach der laufenden GUI-Event-Behandlung verzögern" bedeutet nicht zwangsläufig, dass die Aktion unmittelbar nach der laufenden GUI-Event-Behandlung ausgeführt wird, denn wenn zu dem Zeitpunkt, an dem Control.BeginInvoke eine Nachricht in die Queue einstellt, sich schon oder noch andere Nachrichten dort befinden, werden diese zuerst abgearbeitet, bevor die neue Nachricht an die Reihe kommt.

Siehe auch

Control.BeginInvoke-Methode
Dispatcher.BeginInvoke-Methode
Application.Idle-Ereignis
[FAQ] Event nur bei Benutzeraktion auslösen, nicht bei programmtechnischer Änderung
[FAQ] Controls von Thread aktualisieren lassen (Control.Invoke/Dispatcher.Invoke)
[FAQ] Warum blockiert mein GUI?

herbivore