Bei Windows Desktop Anwendungen wie sie zum Beispiel mit WPF Mitteln erstellt werden können, werden häufig Listen oder Baumansichten verwendet. In einigen Fällen ist es dann erforderlich bei der Auswahl eines Eintrages detailliertere Daten zu diesem Eintrag anzuzeigen oder mehr Informationen zu ermitteln. Ebenso könnte eine Suche über ein große Datenmenge so auszugestalten sein, dass bei der Eingabe der Filterkriterien, die Suche automatisiert ausgeführt werden soll, d.h. es gibt kein “Suche” Button. >> mehr…
Nun ist es nicht unbedingt nötig die Logik hinter der Selektion eines Eintrages oder die automatisierte Suche sofort anzustoßen, weil a) der Anwender mit dem Cursor noch durch die Liste oder den Baum läuft bzw. b) noch am Tippen der Eingabe ist. Würde die Logik sofort angestoßen, wäre auch eine erhöhte Last bei der Anwendung vorhanden, denn eigentlich werden unnötige Informationen beschaff, weil der Anwender noch nicht “fertig” ist. Wird die Logik auch noch auf dem UI Thread ausgeführt, ist die UI blockiert und die Anwendung reagiert nicht auf Anwendereingaben.
Beim Stöbern im Internet bin ich unter anderem auf den Blog Beitrag [WPF] Delay the Execution of a method gestoßen. Hierbei war weniger die vorgestellte Klasse die Grundlage meiner Überlegungen als viel mehr der Beitrag in einem Kommentar. Denn dort heißt es
What about:
var dt = new DispatcherTimer();
dt.Tick += (s, e) => {MyMethod(); dt.Stop();};
dt.Interval = new TimeSpan(0,0,2);
dt.Start();
Mit diesem Code-Snippet wird ein DispatcherTimer gestartet und nach Ablauf des Intervalls von 2 Sekunden die Methode “MyMethod” ausgeführt. Wird dieses Code-Snippet bei jedem Auslösen eines WPF Commands ausgeführt, wird auch “MyMethod” jedes Mal ausgeführt. Dauert “MyMethod” auch noch länger, so ist die UI blockiert, da die Ausführung auf dem UI Thread durchgeführt wird. Ziel meiner Überlegungen war,
- dass die Ausführung des letzten DispatchTimer gestoppt wird, so dass es nicht zu unnötigen Ausführungen von “MyMethod” kommt.
- dass die Ausführung nicht auf dem UI Thread ausgeführt werden, sondern nur die Verarbeitung des Ergebnisses, so dass die UI nicht durch langlaufende Methoden blockiert wird
- dass nur das letzte Ergebnis in der UI verarbeitet wird, falls MyMethod mehrmals ausgeführt wird, denn die vorangegangenen Abfragen haben keine Relevanz mehr
Dies führte zu einer generischen und allgemeinen Methode
1: // Delegate für die Verarbeitung des Ergebnisses auf dem UI Thread
2: public delegate void UICallback<TResult>(TResult arg);
3:
4: private void DelayedExecutionAsync<TArgument, TResult>
5: (
6: Func<TArgument, TResult> delayedFunction,
7: UICallback<TResult> uiCallbackDelegate,
8: TimeSpan timespan
9: )
10: {
11: DispatcherTimer<TArgument, TResult> dispatcherTimer;
12: // DispatcherTimer setup
13: if (this.LastDispatcherTimer == null)
14: {
15: // Anlegen eines neuen DispatcherTimer mit einer
16: // System.Windows.Threading.Dispatcher Instanz,
17: // zum Ausführen des Delegate auf dem UI Thread
18: dispatcherTimer = new DispatcherTimer<TArgument, TResult>(this.UIDispatcher);
19: this.LastDispatcherTimer = dispatcherTimer;
20: }
21: else
22: {
23: System.Diagnostics.Debug.WriteLine("Stop last Timer");
24: dispatcherTimer = this.LastDispatcherTimer;
25: dispatcherTimer.Stop();
26: }
27: dispatcherTimer.DelayedFunction = delayedFunction;
28: dispatcherTimer.UICallbackDelegate = uiCallbackDelegate;
29: dispatcherTimer.Interval = timespan;
30: dispatcherTimer.Start();
31: }
Diese generische Methode erwartet als Parameter ein Methode “delayedFunction” mit “TArgument” als Input und “TResult” als Rückgabewert, die verzögert ausgeführt werden soll, Der Parameter “uiCallbackDelegate” stellt die Methode dar, die nach Vollendung der “delayedFunction” mit dem “TResult” als Input auf dem UI-Thread ausgeführt werden soll. Der Timespan ist das Verzögerungsintervall. In der Methode wird eine eigene generische DispatcherTimer Klasse verwendet, die ein System.Windows.Threading.DispatcherTimer kapselt und die Ausführung der Methoden übernimmt.
1: // Methode, die nach Ablauf des Intervalls am DispatcherTimer ausgeführt wird
2: // EventHandler des Tick-Event von System.Windows.Threading.DispatcherTimer
3: private void DispatchTimerTick(object sender, EventArgs e)
4: {
5: if (this.DelayedFunction != null)
6: {
7: this.LastWorker = new BackgroundWorker();
8: this.LastWorkerHashCode = this.LastWorker.GetHashCode();
9: this.LastWorker.WorkerSupportsCancellation = true;
10: this.LastWorker.DoWork
11: += new DoWorkEventHandler(DoBackgroundWork);
12: this.LastWorker.RunWorkerCompleted
13: += new RunWorkerCompletedEventHandler(RunBackgroundWorkerCompleted);
14: this.LastWorker.RunWorkerAsync();
15: }
16: this.DispatcherTimer.Stop();
17: }
18: // Methode, die ausgeführt wird, wenn der BackgroundWorker startet
19: private void DoBackgroundWork
20: (object sender, DoWorkEventArgs e)
21: {
22: // Pseudo-Argument, in der Methode kommt das Argument des Aufrufes an
23: TArgument argument = default(TArgument);
24: e.Result = this.DelayedFunction(argument);
25: }
26: // Methode, die ausgeführt wird, wenn der BackgroundWorker beendet ist
27: private void RunBackgroundWorkerCompleted
28: (object sender, RunWorkerCompletedEventArgs e)
29: {
30: BackgroundWorker worker = sender as BackgroundWorker;
31: // Ausführen des Callbacks nur wenn diese Completed-Methode
32: // vom zuletzt ausgeführten BackgroundWorker kommt,
33: // denn nur das Ergebnis des letzten Aufrufes ist für die UI von Interesse
34: if (worker.GetHashCode() == this.LastWorkerHashCode
35: && !worker.CancellationPending && !e.Cancelled && e.Error == null)
36: {
37: //Invoke on UI Thread !!!
38: if (this.UICallbackDelegate != null)
39: {
40: this.UIDispatcher.Invoke(this.UICallbackDelegate, e.Result);
41: }
42: }
43: }
Ist das Zeitintervall abgelaufen, wird ein BackgroundWorker erzeugt, um die Ausführung der Funktion im Hintergrund auszuführen (siehe DoBackgroundWork). Ist der BackgroundWorker beendet, so wird der UICallbackDelegate mit Hilfe des Dispatchers auf dem UI Thread ausgeführt und das Ergebnis übergeben (siehe RunBackgroundWorkerCompleted).
Mit diesen Mitteln nun ausgestattet, könnte ein Aufruf innerhalb eines WPF Commands nun so aussehen.
1: private void ExecuteSearchCommand()
2: {
3: SearchFilterCriteria filter = new SearchFilterCriteria()
4: {
5: PageNumber = this.PageNumber,
6: PageSize = this.Count,
7: FirstName = this.FirstNamePattern,
8: LastName = this.LastNamePattern
9: };
10: DelayedExecutionAsync<SearchFilterCriteria, SearchResult>(a => GetEmployees(filter), this.RefillList, new Timespan(0,0,2));
11: }
12:
13: private SearchResult GetEmployees(SearchFilterCriteria filter)
14: {
15: SearchResult result = new EmployeeResult();
16: using (WCFServiceReference.WCFServiceClient wcfClient = new WCFServiceReference.WCFServiceClient())
17: {
18: result.PageNumber = filter.PageNumber;
19: result.TotalNumber =
20: wcfClient.GetEmployeeCount(filter.FirstName, filter.LastName);
21: result.Employees =
22: wcfClient.GetEmployees(filter.PageNumber, filter.PageSize, filter.FirstName, filter.LastName);
23: }
24: return result;
25: }
26:
27: private void RefillList(SearchResult result)
28: {
29: this.Employees.Clear();
30: this.EmployeesCount = result.TotalNumber;
31: this.PageNumber = result.PageNumber;
32: if (result.TotalNumber > 0 && result.Employees != null)
33: {
34: foreach (var emp in result.Employees)
35: this.Employees.Add(emp);
36: }
37: }
Fazit
Mit den vorgestellten Code-Snippets ist es nun möglich eine Methode verzögert im Hintergrund auszuführen, wobei die UI der Anwendung reaktiv bleibt und bei schneller Eingabe durch den Benutzer zuvor ausgelöste Timer nicht ausgeführt werden. Sollte es doch zu mehreren Ausführungen kommen, so wird nur das letzte Ergebnis in der UI verarbeitet. Die Umsetzung beachtet noch nicht alle Fallstricke, so ist zum Beispiel das Fehlerhandling vollkommen außer Acht gelassen worden. Auch die Ausführung verschiedener Commands ist hier nicht berücksichtigt. Gegebenenfalls können die Anforderungen auch andere sein, so dass die vorgestellten Code-Snippets als Grundlage eigener Überlegungen dienen können.
Sie sehen gerade einen Platzhalterinhalt von Facebook. Um auf den eigentlichen Inhalt zuzugreifen, klicken Sie auf die Schaltfläche unten. Bitte beachten Sie, dass dabei Daten an Drittanbieter weitergegeben werden.
Mehr InformationenSie sehen gerade einen Platzhalterinhalt von Instagram. Um auf den eigentlichen Inhalt zuzugreifen, klicken Sie auf die Schaltfläche unten. Bitte beachten Sie, dass dabei Daten an Drittanbieter weitergegeben werden.
Mehr InformationenSie sehen gerade einen Platzhalterinhalt von X. Um auf den eigentlichen Inhalt zuzugreifen, klicken Sie auf die Schaltfläche unten. Bitte beachten Sie, dass dabei Daten an Drittanbieter weitergegeben werden.
Mehr Informationen