Nachdem wir uns im Teil 1 der Performance-Optimierungen bei WPF Anwendungen ObservableCollections in ItemsControl-basierten Views, Binding-Overheads und das ICommand.CanExecute() angeschaut haben, werfen wir nun einen Blick auf Visuals, die Verringerung der ResourceDictionary Lookups sowie die Entlastung von UI-Threads.

Anzahl der Visuals optimieren

Je mehr Visuals WPF in einer Anwendung verwalten muss, umso langsamer wird die Oberfläche. Dazu zählen LayoutUpdates mit Measure()/Arrange des UI-Threads und Zeichnen durch den Renderthread. Daher lohnt es sich mit Tools wie dem Visual Studio Live Visual Tree oder Third Party Tools wie Snoop einen Blick auf die Anzahl der Visuals zu werfen:

Unnötige Visuals entfernen

Manchmal bleiben aus einem Refactoring unnötige Layouter und Controls übrig, die ähnlich wie Dead Code gar keine Funktion erfüllen. Es gibt einige typische Fälle, die auftreten können, sofern Sie die unnötigen Visuals nicht entfernen:

  • TextBlock anstelle von Labels verwenden: TextBlock ist nur ein einziger Visual, Labels sind komplexer.
  • TextBlock.Text direkt setzen anstelle eines TextBlocks mit einem Run.
  • Visibility=“Collapsed“ entfernt das Element aus dem VisualTree, Hidden nicht und wird somit weiterhin in Measure-Aufrufen berechnet.

Für reine Darstellungen den VisualTree durch Custom Control mit OnRender() optimieren

Sollten Sie eine große Menge Visuals haben, können Sie die Performance signifikant erhöhen, indem sie das Xaml-Template durch ein einzelnes Control ersetzen, welches ein ViewModel als DataContext erhält und in OnRender() alles manuell zeichnet. Ein gutes Beispiel sind sehr komplexe Diagramme oder koordinatenbasierte Formulare zur reinen Ansicht.

Ich habe in solchen Fällen den Aufbau der Oberfläche für diese Elemente von gefühlten ein bis zwei Sekunden auf 40ms optimieren können.
Natürlich ist hier der Aufwand hoch, weil Sie selbst das Layouten auf Basis von Koordinaten durchführen müssen. Aber für einen sehr kritischen Teil der Anwendung lässt sich die User Experience dadurch nachhaltig verbessern.

ResourceDictionary Lookups verringern

Nutzen Sie ein User Control oder ein Fenster, welches viele ResourceDictionaries lädt, kann das Laden und Entladen des Controls relativ lange dauern. In einem Fall hatte ich ein User Control, das fünf ResourceDctionaries mit jeweils einer Hand voll DataTemplates lud. Das führte dazu, dass sich das Control in etwa drei bis vier Sekunden aufbaute und in etwa derselben Zeit wieder benötigte, um sich auszublenden. Nachdem ich alle ResourceDictionaries direkt in das Control als Resource hinterlegt hatte, öffnete und schloss sich das Control wieder nahezu ohne Verzögerung.

UI-Thread entlasten

Im eigentlichen Sinne ist die Entlastung von US-Threads keine Performance-Optimierung, dennoch erhöht sie die Reaktivität des Systems merklich. Oftmals gibt es Operationen, die direkt auf die Reaktion eines WPF-Events, ICommand.Execute() oder einem Property-Change aus dem View in das ViewModel ausgelöst werden. Wenn diese Operationen potenziell länger als ein paar 100 ms dauern, wirkt die Anwendung träge.

Potenziell längere Operationen im Hintergrund ausführen

Ein gutes, simples Beispiel ist das Aktualisieren einer Liste von Datensätzen von einem Server. Wenn der Benutzer einen Button drückt, wird die Liste aktualisiert. In der Regel dauert das ca. 500ms, bei schlechter Netzwerkverbindung oder erhöhter Last auf den Servern sowie in Abhängigkeit der Datenmenge manchmal auch mehrere Sekunden – ganz abgesehen von einem Re-Trial wenn der Server kurzzeitig nicht verfügbar ist.

Hierfür können Sie im Command-Handler die Anfrage zum Server bspw. über eine Task asynchron laden und am Ende das Ergebnis über den Dispatcher wieder im UI-Thread zurückführen, so dass sich die Oberfläche aktualisiert. Während die Daten asynchron geladen werden, sollten Sie eine Information anzeigen, dass die Daten gerade geladen werden und entsprechend die Oberfläche sperren, so dass keine weiteren asynchronen Anfragen oder inkonsistente Zustände erzeugt werden können.

Ein weitere Vorteil neben erhöhter Reaktivität ist, dass andere im UI-Thread dispatchte Aufgaben eher eine Chance haben, verarbeitet zu werden, was auch die Abarbeitung anderer Aufgaben im UI-Thread ebenfalls beschleunigen kann – z.B. Dispatcher-BeginInvoke() Aufrufe von niedriger Priorität.

Debouncing auf Basis von DispatcherTimer

Ein Debouncer ist ein Instrument, mit dem ein Ereignis erst ausgelöst wird, wenn es nicht mindestens innerhalb eines gewissen Zeitraums wieder ausgelöst wird. Ein ganz phantastisches Beispiel ist die Suche bei Google: Sobald ich anfange Text einzugeben, werden asynchron Suchergebnisse herausgesucht. Sollte ich allerdings zu schnell tippen, wird erst wieder gesucht, sobald ich aufhöre zu tippen. Dadurch werden nicht permanent neue Suchanfragen losgeschickt und die Suchergebnisse eingetragen, was unnötige Ausführung von teuren Operationen unterbindet.

Nehmen wir bspw. an, Sie haben eine Selektion in einer Liste. Wenn Sie ein Element aus der Liste ausgewählt haben, wollen Sie detaillierte Informationen anzeigen, die leider im UI-Thread berechnet werden müssen und etwas Zeit beanspruchen. Wenn Sie sich jetzt mit der Tastatur schnell von einem Element zum nächsten bewegen, wird die Anwendung etwas hakelig, da Sie erst zum nächsten Element springen können, wenn die Berechnung und Darstellung für die Details fertig ist. Hier könnten Sie über einen Debouncer dafür sorgen, dass erst dann die Details geladen werden, wenn die Selektion einen Moment nicht geändert wurde, bspw. nach 500ms. 

Beispiel:¹

Die folgende Klasse repräsentiert ein ViewModel, welches beim Aufruf von OnSelectedElementChanged() über eine Serviceklasse Daten aufbereitet und ein ViewModel erstellt. Die Serviceklasse braucht potenziell lange und muss aufgrund ihrer Implementierung im UI-Thread ausgeführt werden und kann daher nicht asynchron geladen werden.

Der Debouncer benutzt den DispatcherTimer, um über eine einstellbare Priorität nach einem Zeitintervall eine Action aufzurufen:

Die Action wird erst ausgeführt, wenn der Timer abgelaufen ist und in der Zwischenzeit nicht noch einmal die Debounce() Methode aufgerufen wurde. Wenn die Debounce-Methode aufgerufen wird, bevor der Timer abläuft, wird der Timer neu gestartet.

Mit dieser Klasse ist die Anpassung an unserem ViewModel sehr einfach:

Das Praktische an dieser Lösung ist, dass das Ein- und Ausbauen sehr einfach funktioniert und sich wie ein Adapter zwischen die Logik hängt.

Haben Sie Rückfragen oder Hinweise zu den Optimierungen von WPF Anwendungen? Über Feedback würde ich mich sehr freuen.

 

Hinweis:

[1] Das Beispiel basiert auf der Veröffentlichung unter https://weblog.west-wind.com/posts/2017/Jul/02/Debouncing-and-Throttling-Dispatcher-Events und wurde um einige Codezeilen ergänzt, um den gewünschten Effekt zu erzeugen.

Gefällt Ihnen dieser Beitrag?

Melden Sie sich für unsere News-Updates an und erhalten Sie regelmäßig Tipps von bekannten Autoren direkt in Ihren Posteingang.

Gefällt Ihnen dieser Beitrag?

Sie haben sich für unsere News-Updates angemeldet. In kürze Erhalten Sie eine E-Mail mit einem Bestätigungslink. Erst wenn Sie darauf geklickt haben, werden wir Ihnen regelmäßig unsere Updates in Ihren Posteingang senden.

Share This