Windows Presentation Foundation (WPF) gibt es seit 16 Jahren, oder, in Ermangelung einer besseren Referenz, seit .Net Framework 3.0. Sie wurde im Laufe der Jahre stark verbessert, aber es gibt immer noch einige Bereiche, die unter schlechter Leistung leiden können. Dies ist vor allem auf schlechte Codierungspraktiken, fehlerhafte Bindungen oder komplexe Layouts zurückzuführen. Glücklicherweise gibt es nichts, was nicht mit einem gründlichen Verständnis der WPF-Plattform und ein wenig Planung behoben werden kann.
Dieser Artikel kann für Anfänger nützlich sein, die von Anfang an alles richtig machen wollen. Denn es entspricht dem gesunden Menschenverstand, dass Vorbeugen viel einfacher (und billiger) ist als Behandeln.
Dennoch können diese Tipps hilfreich sein, wenn Sie eine bestehende Anwendung haben und mit deren Leistung nicht zufrieden sind. So erging es mir vor Jahren, als ich als Entwickler für die Luftfahrtindustrie tätig war und die Aufgabe hatte, die von uns entwickelte Anwendung auf ein „Formel 1“-Leistungsniveau zu bringen. Das bedeutete, dass ich mich in den Code stürzte, unabhängig davon, ob er von mir oder von anderen geschrieben worden war, und alles tat, was möglich war, um jede Millisekunde Ladezeit aus der Anwendung herauszuquetschen.
Ich habe schon immer versucht, auf die Leistung meines Codes zu achten, aber diese spezielle Mission hat mir noch mehr bewusst gemacht, was getan werden kann, um die Leistung meiner WPF-Anwendungen zu verbessern.
Hier sind die häufigsten Gründe, die ich gefunden habe, um die Anwendungsleistung zu verlangsamen und wie man sie angehen kann:
Bindungsfehler
Bindungsfehler sind für die Verlangsamung einer Anwendung während der Laufzeit verantwortlich. Wenn ein Bindungsfehler auftritt, schreibt die Anwendung den Fehler in das Trace-Protokoll. Je mehr Bindungsfehler auftreten, desto mehr Einträge werden in das Protokoll geschrieben. Dies führt zu einer schlechten Leistung.
Diese Art von Problem wurde für mich offensichtlich, weil es in die Ausgabe geschrieben wurde, und bis die Anwendung alle Fehler geschrieben hatte, tat sie nichts mehr. Später habe ich gelernt, dass XAML-Code sehr von seiner speziellen Syntax abhängt, und wenn man sie nicht zu 100 % einhält, werden Fehler über Fehler auftreten. Und diese Fehler sind Ressourcenfresser.
Ich arbeite derzeit an einer großen Anwendung mit Dutzenden von XAML-Dateien. Stellen Sie sich vor, dass jede dieser Dateien 2-3 System Windows Data Errors auslöst. Sie häufen sich, und die Anwendung braucht viel mehr Zeit zum Laden. Dies sind keine Fehler, die zum Anhalten oder Absturz der Anwendung führen können. Aber sie verschlingen Ressourcen.
Die Behebung eines solchen Fehlers dauert vielleicht nicht lange. Aber der Unterschied wird sichtbar, wenn man alle Fehler behebt. Am Ende summiert sich jede Millisekunde und Sie können 2-3 Sekunden für die gesamte Anwendung gewinnen.
Wonach Sie suchen müssen:
System.Windows.Data Error: 40: BindingExpression path error: ‘NonExistingProperty’ property not found on ‘object’ ”Grid’ (Name=’pnlMain’)’. BindingExpression:Path=NonExistingProperty; DataItem=’Grid’ (Name=’pnlMain’); target element is ‘TextBlock’ (Name=”); target property is ‘Text’ (type ‘String’)
Tipp:
Überprüfen Sie immer das Ausgabefenster und lesen Sie den Fehler. Verwenden Sie in diesem Fall die Syntax des Binding-Trace-Fehlers.
Achten Sie auf Speicherlecks
Wenn Sie mehr Erfahrung mit WPF haben, werden Sie mit dem Begriff „Ereignis“ konfrontiert. Ein Ereignis ist z. B. die Aktion, die stattfindet, wenn der Benutzer auf eine Schaltfläche klickt.
Es gibt verschiedene Möglichkeiten, diese Aktionen zu definieren. Viele Anfänger in der WPF-Programmierung registrieren sich für ein Ereignis, vergessen aber, die Registrierung aufzuheben. In C++ gibt es das Zeigerkonzept und die Speicherzuweisung. Wenn Sie Speicher zuweisen, müssen Sie ihn auch wieder freigeben. Microsoft hat einen „Garbage Collector“-Mechanismus entwickelt, der prüft, ob das Fenster mit der Schaltfläche gelöscht werden kann. Wenn es jedoch noch aktiv ist, kann es nicht zerstört werden.
In C# können Sie sich mit += für das Ereignis anmelden und mit -= wieder abmelden.
Wenn Sie also keine Speicher- oder Ereignisverwaltung vornehmen, werden sich diese Probleme häufen.
Microsoft hat auch hier eine Lösung gefunden: eine bestimmte Art von Ereignissen, bei denen man sich nicht abmelden muss, der Garbage Collector entscheidet, ob dies notwendig ist. Aber diese Ereignistypen werden von Anfängern/Entwicklern, die mit der Verwendung von Ereignissen nicht vertraut sind, nicht so häufig verwendet.
Der Garbage Collector ist eine .NET-Eigenschaft, die sich an die Microsoft-Philosophie „The Lazy Way“ hält. Diese Philosophie besagt, dass man als Entwickler keine Zeit mit technischen Details verschwenden sollte, man wird sogar davon abgehalten, diese Eigenschaft zu erzwingen. Sie sollten sich auf Code und Kreativität konzentrieren, und Microsoft versucht, Lösungen zu finden, die Ihnen bei der Verwaltung von Speicher, Ereignissen usw. helfen. Voraussetzung ist, dass Sie genau die Ereignisse verwenden, die Sie benötigen.
Der WeakEvent-Manager ist das Werkzeug, das diese Art von Ereignissen verwaltet, die keine Entregistrierung erfordern. Diese Art von Ereignissen löst einfach den Garbage Collector aus, der sie zerstört, wenn er sie findet.
Das Hauptproblem besteht darin, dass Nachwuchsentwickler eine Websuche zum Hinzufügen eines Ereignisses durchführen und höchstwahrscheinlich die +=-Methode finden.
Wenn man vergisst, die Registrierung von Ereignissen aufzuheben, werden zum Beispiel einige Fenster mehr als einmal geöffnet. Dies wiederum führt dazu, dass mehr Speicher zugewiesen wird und mehr Zeit zum Laden der Anwendung benötigt wird.
Worauf Sie achten sollten:
Abonnieren von EventHandler – und vergessen, sich abzumelden oder Verhaltensweisen zu verwenden (achten Sie auf Attach/Detach-Ereignisse).
Falsche WPF-Bindungen (binden Sie immer an ein DO oder an ein Objekt, das INotifyPropertyChanged implementiert – andernfalls erstellt WPF eine starke Referenz auf die Bindungsquelle.
Tipp:
Abbestellen von Ereignissen. Verwenden Sie schwache Handler von subscribe, wenn möglich, mit einer anonymen Funktion.
Halten Sie den visuellen Baum einfach
Bei jeder Codeüberprüfung, die wir durchführen, kann ich nicht genug betonen: Es hat keinen Sinn, ein Gitter in einem Gitter in einem Gitter zu haben. Ein Raster reicht aus und erledigt die Arbeit genauso gut.
Hier ist ein Beispiel:
In diesem Fall können Sie das Raster weglassen, da es dieselbe Funktion erfüllt wie das Stapel-Panel, nämlich die Schaltfläche zu beherbergen. Wenn Sie also beide verwenden, verdoppeln Sie lediglich die Funktion.
Wonach Sie suchen müssen:
Grid
StackPanel
Grid
Button
Tipp:
Vermeiden Sie das Hinzufügen unnötiger Elemente in der Baumstruktur (z. B. ein Grid, das auf einem StackPanel gehostet wird, das in einem anderen Grid gehostet wird)
StaticResource vs. DynamicResource
Wenn Sie eine Farbe in XAML definieren, können Sie entweder StaticResource oder DynamicResource verwenden. Der Unterschied zwischen den beiden besteht darin, dass statische Ressourcen nur einmal, zum Zeitpunkt des Ladens, ausgewertet werden, während dynamische Ressourcen jedes Mal ausgewertet werden, wenn sie angefordert werden.
Wenn sich die Farbe im Farbbeispiel nie ändert, wird die Verwendung von StaticResource empfohlen. Wenn Sie aber zum Beispiel einen Tooltip haben, der ein dynamisches Element ist, dann können Sie DynamicResource verwenden. Es ist also am besten, wenn Sie Ihren Code unter Berücksichtigung des Kontexts schreiben.
Wonach Sie suchen sollten:
BorderBrush=“{DynamicResource GeneralBorderColor}“
Tipp:
Wenn möglich, verwenden Sie StaticResource. StaticResources werden nur einmal vom Element zur Ladezeit ausgewertet. DynamicResources werden jedes Mal ausgewertet, wenn sie vom Element angefordert werden, wodurch die Anwendung langsamer arbeitet.
ElementName vs. RelativeSource
Ich habe festgestellt, dass RelativeSource auf Stack Overflow als Lösungsvorschlag für ElementName beliebter ist. Das ist ein bisschen kontraintuitiv, weil die RelativeSource-Syntax länger ist als die für ElementName.
Wenn ich nach „relativesource wpf“ suche, wird als erstes Ergebnis der Bindungspfad als Elementname und der Name des Rasters oder des übergeordneten Elements vorgeschlagen.
Wonach Sie suchen müssen:
Binding=“{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Expander}}, Path=IsExpanded}“
Tipp:
Im Allgemeinen sollte, wenn möglich, der ElementName verwendet werden. RelativeSource durchläuft den Visual-Baum, bis es das Ziel findet.
TextBlock vs. Label
Ein weiterer Bereich, in dem Sie versuchen können, etwas Leistung herauszuholen, ist der, in dem TextBlock Label ersetzen kann. Wenn wir ein Element beschreiben müssen, neigen wir oft dazu, Label zu verwenden, weil unser Gehirn dieses Wort mit einem kurzen, prägnanten Text assoziiert. Aber vom Standpunkt der Programmierung aus betrachtet, hat Label mehr Funktionalität als TextBlock und ist daher ressourcenintensiver für WPF.
Wonach Sie suchen müssen:
<Border x:Name=”TextBlockBorder” Grid.Row=”0″ Grid.Column=”1″ BorderThickness=”0″>
<Label x:Name=”TextBlockApplyToAll” Content=”Apply to all” VerticalAlignment=”Center” HorizontalAlignment=”Center”/>
</Border>
Tipp:
Verwenden Sie TextBlock, wenn möglich. TextBlock ist von FrameworkElements abgeleitet, während Label von ContentControl abgeleitet ist, was bedeutet, dass Label schwerer ist als TextBlock.
Warum (das Gerede über) Leistung wichtig ist
Zunächst sollte ich darauf hinweisen, dass die Leistung an der Ladezeit der Anwendung gemessen wird. Und da gibt es nicht die eine große Änderung, die man vornehmen kann, sondern Hunderte und Tausende von kleinen Änderungen, die jeweils ein paar Millisekunden einsparen, und erst wenn sie sich summieren, hat man eine spürbare Verbesserung, sagen wir um 2 oder 3 Sekunden.
Das mag nicht viel erscheinen. Aber bei sich wiederholenden Vorgängen wächst dieser Wert exponentiell an. In einer Fabrik, in der sich die Prozesse vervielfachen, würde dies auch zu einem erheblichen Energieverbrauch führen. Bei einer Anwendung wie Office Timeline wird der Endbenutzer weniger frustriert sein, weil er auf den Abschluss von Backend-Prozessen warten muss, und es wird Zeit gespart, die für unnötige Vorgänge aufgewendet wird.
Diese Dinge sind nicht leicht zu erkennen. Erst als ich eine konkrete leistungsbezogene Aufgabe erhielt, erkannte ich das Problem und begann, nach Lösungen zu suchen.
Deshalb versuche ich, meine Erkenntnisse bei jeder sich bietenden Gelegenheit mitzuteilen. Was ich mit Sicherheit sagen kann, ist, dass die Leute in der Regel aufgeschlossen sind und nach und nach damit beginnen, diese kleinen Änderungen bei der Planung und Erstellung des Codes anzuwenden. Es ist viel einfacher, es von Anfang an richtig zu machen, als es später zu korrigieren.
Wo Sie helfen können
Dies sind nur einige wenige Tipps zur Verbesserung der Leistung, die wichtigsten, die mir eingefallen sind. Aber es gibt sicher noch mehr. Wenn Sie eine Idee haben, wie man die Leistung verbessern kann, schreiben Sie mir bitte unten eine Nachricht, und ich werde die Liste ergänzen.
Wenn Sie sich für WPF begeistern, verweise ich Sie auf eine meiner Lieblingsressourcen, 2000 Things WPF. Hier finden Sie Tausende von kurzen Artikeln darüber, wie Sie bestimmte Probleme in WPF lösen können.