WPF-Architektur
Dieses Thema enthält eine geführte Tour durch die Windows Presentation Foundation (WPF)-Klassenhierarchie. Es behandelt die meisten wichtigen Subsysteme von WPF und beschreibt, wie sie interagieren. Außerdem werden einige der von den Architekten von WPF getroffenen Entscheidungen beschrieben.
System.Object
Das primäre WPF-Programmiermodell wird über verwalteten Code verfügbar gemacht. In der Entwurfsphase von WPF gab es eine Reihe von Debatten darüber, wo die Linie zwischen den verwalteten Komponenten des Systems und den nicht verwalteten komponenten gezeichnet werden sollte. Die CLR bietet eine Reihe von Features, die die Entwicklung produktiver und robuster machen (einschließlich Speicherverwaltung, Fehlerbehandlung, gemeinsames Typsystem usw.), aber sie kostenaufwendig sind.
Die Hauptkomponenten von WPF sind in der folgenden Abbildung dargestellt. Die roten Abschnitte des Diagramms (PresentationFramework, PresentationCore und Milcore) sind die wichtigsten Codebereiche von WPF. Von diesen ist nur eine nicht verwaltete Komponente – Milcore. Milcore wird in nicht verwalteten Code geschrieben, um eine enge Integration in DirectX zu ermöglichen. Die gesamte Anzeige in WPF erfolgt über die DirectX-Engine, was ein effizientes Hardware- und Softwarerendering ermöglicht. WPF erforderte auch eine feine Kontrolle über Arbeitsspeicher und Ausführung. Die Kompositions-Engine in Milcore ist extrem leistungsanfällig und erforderte das Aufgeben vieler Vorteile der CLR, um Leistung zu gewinnen.
Die Kommunikation zwischen verwalteten und nicht verwalteten Teilen von WPF wird weiter unten in diesem Thema erläutert. Der Rest des verwalteten Programmiermodells wird unten beschrieben.
System.Threading.DispatcherObject
Die meisten Objekte in WPF werden von DispatcherObjectabgeleitet, das die grundlegenden Konstrukte für den Umgang mit Parallelität und Threading bereitstellt. WPF basiert auf einem Messagingsystem, das vom Dispatcher implementiert wird. Dies funktioniert ähnlich wie die vertraute Win32-Nachrichtenpumpe; tatsächlich verwendet der WPF-Dispatcher User32-Nachrichten zum Ausführen von übergreifenden Threads-Aufrufen.
Um Parallelität in WPF zu verstehen, sind zwei Kernkonzepte entscheidend: der Dispatcher und die Threadaffinität.
Während der Entwurfsphase von WPF bestand das Ziel darin, zu einem einzelnen Ausführungsthread zu wechseln, jedoch ein Modell zu verwenden, das nicht von der Threadaffinität abhängt. Threadaffinität geschieht, wenn eine Komponente die Identität des ausgeführten Threads verwendet, um einen bestimmten Zustandstyp zu speichern. Die häufigste Form ist die Verwendung des lokalen Threadspeichers (TLS) zum Speichern des Zustands. Threadaffinität erfordert, dass jeder logische Ausführungsthread nur einem physischen Thread im Betriebssystem gehört, der speicherintensiv werden kann. Am Ende wurde das Threadingmodell von WPF mit dem vorhandenen User32-Threadingmodell der Ausführung durch einen einzelnen Thread und Threadaffinität synchronisiert. Der Hauptgrund hierfür war Interoperabilität – Systeme wie OLE 2.0, die Zwischenablage und Internet Explorer erfordern die Ausführung mit STA (Single-Thread-Affinität).
Da Sie über Objekte mit STA-Threading verfügen, benötigen Sie eine Möglichkeit, zwischen Threads zu kommunizieren und zu überprüfen, ob Sie sich im richtigen Thread befinden. Hier liegt die Rolle des Verteilers. Der Verteiler ist ein einfaches Nachrichtenversandsystem mit mehreren priorisierten Warteschlangen. Beispiele für Nachrichten sind Rohdaten-Eingabebenachrichtigungen (Maus verschoben), Framework-Funktionen (Layout) oder Benutzerbefehle (führe diese Methode aus). Indem Sie von DispatcherObjectableiten, erstellen Sie ein CLR-Objekt mit STA-Verhalten und erhalten zum Zeitpunkt der Erstellung einen Zeiger auf einen Dispatcher.
System.Windows.DependencyObject
Eine der wichtigsten Architekturphilosophien, die beim Erstellen von WPF verwendet werden, war eine Präferenz für Eigenschaften gegenüber Methoden oder Ereignissen. Eigenschaften sind deklarativ und ermöglichen es Ihnen, absichten anstelle von Aktionen einfacher anzugeben. Dies unterstützt auch ein modellgesteuertes oder datengesteuertes System zum Anzeigen von Benutzeroberflächeninhalten. Diese Philosophie hatte die beabsichtigte Wirkung, mehr Eigenschaften zu erstellen, an die Sie binden konnten, um das Verhalten einer Anwendung besser zu steuern.
Um mehr von dem System durch Eigenschaften steuern zu lassen, war ein umfangreicheres Eigenschaftensystem erforderlich als jene, die von der CLR bereitgestellt werden. Ein einfaches Beispiel für diese Fülle sind Änderungsbenachrichtigungen. Um eine bidirektionale Bindung zu ermöglichen, benötigen Sie beide Seiten der Bindung, um die Änderungsbenachrichtigung zu unterstützen. Damit das Verhalten an Eigenschaftswerte gebunden ist, müssen Sie benachrichtigt werden, wenn sich der Eigenschaftswert ändert. Das Microsoft .NET Framework verfügt über eine Schnittstelle, INotifyPropertyChange, die es einem Objekt ermöglicht, Änderungsbenachrichtigungen zu veröffentlichen, ist jedoch optional.
WPF stellt ein umfangreicheres Eigenschaftssystem bereit, das vom DependencyObject-Typ abgeleitet ist. Das Eigenschaftensystem ist wirklich ein "Abhängigkeits"-Eigenschaftssystem, in dem Abhängigkeiten zwischen Eigenschaftsausdrücken nachverfolgt und Eigenschaftswerte automatisch revalidiert werden, wenn sich Abhängigkeiten ändern. Wenn Sie beispielsweise über eine Eigenschaft verfügen, die erbt (z. B. FontSize), wird das System automatisch aktualisiert, wenn sich die Eigenschaft für ein übergeordnetes Element eines Elements ändert, das den Wert erbt.
Die Grundlage des WPF-Eigenschaftssystems ist das Konzept eines Eigenschaftsausdrucks. In dieser ersten Version von WPF ist das System für Eigenschaftsausdrücke geschlossen, und alle Ausdrücke werden als Teil des Frameworks bereitgestellt. Ausdrücke sind der Grund, warum das Eigenschaftensystem keine hartcodierte Datenbindung, Formatierung oder Vererbung aufweist, sondern von späteren Ebenen innerhalb des Frameworks bereitgestellt wird.
Das Eigenschaftensystem bietet auch einen geringen Speicher von Eigenschaftswerten. Da Objekte Dutzende (wenn nicht Hunderte) von Eigenschaften aufweisen können und sich die meisten Werte im Standardzustand befinden (geerbt, durch Stile festgelegt usw.), muss nicht jede Instanz eines Objekts die komplette Menge aller definierten Eigenschaften aufweisen.
Das letzte neue Merkmal des Eigenschaftensystems ist das Konzept angefügter Eigenschaften. WPF-Elemente basieren auf dem Prinzip der Komposition und der Wiederverwendung von Komponenten. Es ist häufig der Fall, dass einige enthaltende Elemente (z. B. ein Grid Layoutelement) zusätzliche Daten für untergeordnete Elemente benötigen, um das Verhalten (z. B. die Zeilen-/Spalteninformationen) zu steuern. Anstatt alle diese Eigenschaften jedem Element zuzuordnen, darf jedes Objekt Eigenschaftsdefinitionen für jedes andere Objekt bereitstellen. Dies ähnelt den "expando"-Features von JavaScript.
System.Windows.Media.Visual
Wenn ein System definiert ist, besteht der nächste Schritt darin, die Pixel auf dem Bildschirm anzuzeigen. Die Visual-Klasse ermöglicht den Aufbau einer Baumstruktur visueller Objekte, die optional Zeichnungsanweisungen und Metadaten darüber enthält, wie diese Anweisungen gerendert werden sollen (Clipping, Transformation usw.). Visual ist so konstruiert, dass sie extrem leicht und flexibel ist, sodass die meisten Features keine öffentliche API-Exposition aufweisen und stark auf geschützte Callback-Funktionen angewiesen sind.
Visual ist der Einstiegspunkt zum WPF-Kompositionssystem. Visual ist der Verbindungspunkt zwischen diesen beiden Subsystemen, der verwalteten API und dem nicht verwalteten Milcore.
WPF zeigt Daten an, indem die nicht verwalteten Datenstrukturen durchlaufen werden, die von der Milcore verwaltet werden. Diese Strukturen, die als Kompositionsknoten bezeichnet werden, stellen eine hierarchische Anzeigestruktur mit Renderinganweisungen an jedem Knoten dar. Auf diesen Baum, der auf der rechten Seite der Abbildung unten dargestellt ist, kann nur über ein Messaging-Protokoll zugegriffen werden.
Beim Programmieren von WPF erstellen Sie Visual Elemente und abgeleitete Typen, die intern über dieses Messagingprotokoll mit dem Kompositionsbaum kommunizieren. Jede Visual in WPF kann eine, keine oder mehrere Kompositionsknoten erstellen.
Hier ist ein sehr wichtiges architektonisches Detail zu beachten – die gesamte Struktur der visuellen Elemente und Zeichnungsanweisungen wird zwischengespeichert. In grafischen Begriffen verwendet WPF ein persistentes Rendering-System. Auf diese Weise kann das System bei hohen Aktualisierungsraten neuzeichnen, ohne dass das Kompositionssystem durch Rückrufe an den Benutzercode blockiert wird. Dadurch wird verhindert, dass eine nicht reagierende Anwendung auftritt.
Ein weiteres wichtiges Detail, das im Diagramm nicht wirklich spürbar ist, ist, wie das System die Komposition tatsächlich ausführt.
In User32 und GDI arbeitet das System in einem Sofortmodus-Ausschnittsystem. Wenn eine Komponente gerendert werden muss, richtet das System eine Schnittgrenze ein, außerhalb derer die Komponente die Pixel nicht berühren darf, und dann wird die Komponente aufgefordert, Pixel in diesem Bereich zu zeichnen. Dieses System funktioniert sehr gut in Systemen mit begrenztem Speicher, da, wenn sich etwas ändert, nur die betroffene Komponente angepasst werden muss – nie tragen zwei Komponenten gleichzeitig zur Farbe eines einzelnen Pixels bei.
WPF verwendet ein "Painter's Algorithm"-Malmodell. Dies bedeutet, dass nicht jede Komponente abgeschnitten wird, sondern jede Komponente von der Rückseite bis zur Vorderseite der Anzeige gerendert wird. Auf diese Weise kann jede Komponente über die Anzeige der vorherigen Komponente zeichnen. Der Vorteil dieses Modells besteht darin, dass Sie komplexe, teilweise transparente Formen haben können. Mit der heutigen modernen Grafikhardware ist dieses Modell relativ schnell (was nicht der Fall war, als User32/GDI erstellt wurde).
Wie bereits erwähnt, besteht eine Kernphilosophie von WPF darin, zu einem deklarativeren, "eigenschaftsorientierten" Programmiermodell zu wechseln. Im visuellen System zeigt sich dies an einigen interessanten Stellen.
Wenn Sie über das Grafiksystem für den beibehaltenen Modus nachdenken, bewegt sich dies wirklich von einem imperativen DrawLine/DrawLine-Typmodell zu einem datenorientierten Modell – neue Linie()/neue Linie(). Durch diesen Wechsel zum datengesteuerten Rendering können komplexe Vorgänge in den Zeichnungsanweisungen mithilfe von Eigenschaften ausgedrückt werden. Die typen, die von Drawing abgeleitet werden, sind effektiv das Objektmodell für das Rendern.
Zweitens, wenn Sie das Animationssystem beurteilen, sehen Sie, dass es fast vollständig deklarativ ist. Anstatt dass ein Entwickler die nächste Position oder die nächste Farbe berechnen muss, können Sie Animationen als Eine Reihe von Eigenschaften für ein Animationsobjekt ausdrücken. Diese Animationen können dann die Absicht des Entwicklers oder Designers ausdrücken (verschieben Sie diese Schaltfläche von hier nach dort in 5 Sekunden), und das System kann die effizienteste Möglichkeit ermitteln, um dies zu erreichen.
System.Windows.UIElement
UIElement definiert Kernsubsysteme, einschließlich Layout, Eingabe und Ereignisse.
Layout ist ein Kernkonzept in WPF. In vielen Systemen gibt es entweder einen festen Satz von Layoutmodellen (HTML unterstützt drei Modelle für Layout, Fluss, Absolut und Tabellen) oder kein Modell für layout (User32 unterstützt wirklich nur absolute Positionierung). WPF begann mit der Annahme, dass Entwickler und Designer ein flexibles, erweiterbares Layoutmodell wollten, das von Eigenschaftswerten und nicht von imperativer Logik gesteuert werden kann. Auf der UIElement-Ebene wird der grundlegende Vertrag für das Layout eingeführt – ein zweiphasiges Modell mit Measure- und Arrange-Durchläufen.
Measure ermöglicht es einer Komponente zu bestimmen, wie viel Größe sie einnehmen möchte. Dies ist eine separate Phase von Arrange, da es viele Situationen gibt, in denen ein Elternelement ein Kindelement mehrfach auffordern wird, sich auszumessen, um seine bestmögliche Position und Größe zu bestimmen. Die Tatsache, dass übergeordnete Elemente untergeordnete Elemente zur Messung auffordern, zeigt eine weitere wichtige Philosophie von WPF – Größe für Inhalte. Alle Steuerelemente in WPF unterstützen die Möglichkeit, die Größe auf die natürliche Größe ihres Inhalts zu ändern. Dies erleichtert die Lokalisierung und ermöglicht das dynamische Layout von Elementen, da sich die Größe der Elemente ändert. Die Arrange-Phase ermöglicht es einem übergeordneten Element, die Position und die endgültige Größe jedes untergeordneten Elements zu bestimmen.
Viel Zeit wird häufig damit verbracht, über die Ausgabeseite von WPF zu sprechen – Visual und verwandte Objekte. Es gibt jedoch auch eine enorme Menge an Innovationen auf der Inputseite. Wahrscheinlich ist die grundlegendste Änderung des Eingabemodells für WPF das konsistente Modell, mit dem Eingabeereignisse über das System geleitet werden.
Die Eingabe beginnt als Signal auf einem Kernelmodus-Gerätetreiber und wird durch einen komplizierten Prozess, der den Windows-Kernel und User32 umfasst, an den richtigen Prozess und Thread geleitet. Sobald die User32-Nachricht, die der Eingabe entspricht, an WPF weitergeleitet wird, wird sie in eine unformatierte WPF-Eingabenachricht konvertiert und an den Verteiler gesendet. WPF ermöglicht die Konvertierung von Roh-Eingabeereignissen in mehrere konkrete Ereignisse, sodass Funktionen wie "MouseEnter" auf niedriger Systemebene mit garantierter Zustellung implementiert werden können.
Jedes Eingabeereignis wird in mindestens zwei Ereignisse konvertiert – ein "Vorschau"-Ereignis und das tatsächliche Ereignis. Alle Ereignisse in WPF haben ein Konzept von Routing durch den Elementbaum. Ereignisse werden als "Blase" bezeichnet, wenn sie von einem Ziel bis zur Wurzel durchqueren und "tunneln", wenn sie am Stamm beginnen und bis zu einem Ziel durchqueren. Tunnel für Eingabevorschauereignisse, wodurch jedes Element in der Struktur eine Möglichkeit zum Filtern oder Ausführen von Aktionen für das Ereignis ermöglicht wird. Die regulären Ereignisse (nicht in der Vorschau) blasen dann vom Ziel bis zum Stamm.
Diese Aufteilung zwischen Tunnel- und Blasenphase sorgt dafür, dass Funktionen wie Tastenkombinationen in einer zusammengesetzten Welt konsistent funktionieren. In User32 würden Sie Zugriffstasten implementieren, indem Sie eine einzelne globale Tabelle mit allen Zugriffstasten haben, die Sie unterstützen wollten (STRG+N-Zuordnung zu "Neu"). Im Dispatcher für Ihre Anwendung würden Sie TranslateAccelerator aufrufen, die Eingabemeldungen in User32 überwacht und feststellt, ob sie mit einer registrierten Tastenkombination übereinstimmen. In WPF funktioniert dies nicht, da das System vollständig "komponierbar" ist – jedes Element kann jedes Tastaturkürzel verarbeiten und verwenden. Mit diesem zweistufigen Modell für Eingaben können Komponenten ihren eigenen "TranslateAccelerator" implementieren.
Um einen Schritt weiter zu gehen, führt UIElement auch das Konzept von "CommandBindings" ein. Mit dem WPF-Befehlssystem können Entwickler Funktionen in Bezug auf einen Befehlsendpunkt definieren – etwas, das ICommandimplementiert. Befehlsbindungen ermöglichen es einem Element, eine Zuordnung zwischen einer Eingabegeste (STRG+N) und einem Befehl (Neu) zu definieren. Sowohl die Eingabegesten als auch die Befehlsdefinitionen sind erweiterbar und können bei der Verwendung miteinander verbunden werden. Dies macht es trivial, z. B. es einem Endbenutzer zu ermöglichen, die Tastenbindungen anzupassen, die er in einer Anwendung verwenden möchte.
Zu diesem Punkt des Themas standen die "Kernfunktionen" von WPF – Funktionen, die in der PresentationCore-Assembly implementiert wurden, im Mittelpunkt. Beim Erstellen von WPF war eine klare Trennung zwischen Grundlagenstücken (z. B. der Vertrag für das Layout mit Measure und Arrange) und Framework-Komponenten (z. B. die Implementierung eines bestimmten Layouts wie Grid) das gewünschte Ergebnis. Ziel war es, einen erweiterbaren Punkt niedrig im Stapel bereitzustellen, der es externen Entwicklern ermöglichen würde, bei Bedarf eigene Frameworks zu erstellen.
System.Windows.FrameworkElement
FrameworkElement können auf zwei verschiedene Arten betrachtet werden. Es führt eine Reihe von Richtlinien und Anpassungen für die Subsysteme ein, die in niedrigeren Ebenen von WPF eingeführt wurden. Außerdem wird eine Reihe neuer Subsysteme eingeführt.
Die primäre Richtlinie, die von FrameworkElement eingeführt wurde, ist das Anwendungslayout. FrameworkElement baut auf dem grundlegenden Layoutvertrag auf, der von UIElement eingeführt wurde, und fügt den Begriff eines Layout-"Slots" hinzu, der es Layoutautoren erleichtert, eine konsistente, eigenschaftsgesteuerte Layoutsemantik zu verwenden. Eigenschaften wie HorizontalAlignment, VerticalAlignment, MinWidthund Margin (um nur einige zu nennen) sorgen dafür, dass alle Komponenten, die von FrameworkElement abgeleitet sind, innerhalb von Layout-Containern ein konsistentes Verhalten aufweisen.
FrameworkElement bietet auch eine einfachere API-Zugänglichkeit zu vielen Funktionen in den Kernschichten von WPF. Beispielsweise bietet FrameworkElement direkten Zugriff auf Animationen über die BeginStoryboard-Methode. Eine Storyboard bietet eine Möglichkeit, mehrere Animationen gegen eine Reihe von Eigenschaften zu programmieren.
Die beiden wichtigsten Punkte, die FrameworkElement einführt, sind Datenbindung und Stile.
Das Datenbindungssubsystem in WPF sollte für alle Benutzer, die Windows Forms oder ASP.NET zum Erstellen einer Benutzeroberfläche (Application User Interface, UI) verwendet haben, relativ vertraut sein. In jedem dieser Systeme gibt es eine einfache Möglichkeit anzugeben, dass Sie eine oder mehrere Eigenschaften eines bestimmten Elements an eine Datenmenge binden möchten. WPF unterstützt die Eigenschaftenbindung, Transformation und Listenbindung vollständig.
Eines der interessantesten Features der Datenbindung in WPF ist die Einführung von Datenvorlagen. Mit Datenvorlagen können Sie deklarativ angeben, wie ein Datenteil visualisiert werden soll. Anstatt eine benutzerdefinierte Benutzeroberfläche zu erstellen, die an Daten gebunden werden kann, können Sie stattdessen das Problem umgehen und es den Daten ermöglichen, die Anzeige zu bestimmen, die erstellt wird.
Das Styling ist tatsächlich eine leichte Form der Datenbindung. Mithilfe der Formatierung (Styling) können Sie eine Gruppe von Eigenschaften aus einer gemeinsam genutzten Definition an eine oder mehrere Instanzen eines Elements binden. Formatvorlagen werden auf ein Element entweder durch expliziten Verweis (durch Festlegen der Style-Eigenschaft) oder implizit durch Zuordnen einer Formatvorlage zum CLR-Typ des Elements angewendet.
System.Windows.Controls.Control
Die bedeutendste Eigenschaft der Steuerung ist der Vorlagenmechanismus. Wenn Sie das Kompositionssystem von WPF als ein Rendering-System im Beibehaltungsmodus betrachten, ermöglicht das Templating einem Steuerelement, sein Rendering in parametrischer, deklarativer Weise zu beschreiben. Ein ControlTemplate ist eigentlich nichts anderes als ein Skript zum Erstellen einer Reihe von untergeordneten Elementen, mit Bindungen an vom Steuerungselement bereitgestellte Eigenschaften.
Control bietet eine Reihe von vordefinierten Eigenschaften wie Foreground, Background, Padding, um nur einige zu nennen, mit denen Vorlagenautoren die Anzeige eines Steuerelements anpassen können. Die Implementierung eines Steuerelements stellt ein Datenmodell und ein Interaktionsmodell bereit. Das Interaktionsmodell definiert eine Reihe von Befehlen (z. B. Schließen für ein Fenster) und Bindungen an Eingabegesten (z. B. Klicken auf das rote X in der oberen Ecke des Fensters). Das Datenmodell stellt eine Reihe von Eigenschaften bereit, um das Interaktionsmodell anzupassen oder die Anzeige anzupassen (bestimmt durch die Vorlage).
Diese Aufteilung zwischen dem Datenmodell (Eigenschaften), dem Interaktionsmodell (Befehlen und Ereignissen) und dem Anzeigemodell (Vorlagen) ermöglicht eine vollständige Anpassung des Aussehens und Verhaltens eines Steuerelements.
Ein allgemeiner Aspekt des Datenmodells von Steuerelementen ist das Inhaltsmodell. Wenn Sie ein Steuerelement wie Buttonbetrachten, sehen Sie, dass es eine Eigenschaft mit dem Namen "Content" vom Typ Objecthat. In Windows Forms und ASP.NET wäre diese Eigenschaft in der Regel eine Zeichenfolge, die jedoch den Inhaltstyp einschränkt, den Sie in eine Schaltfläche einfügen können. Inhalt für eine Schaltfläche kann entweder eine einfache Zeichenfolge, ein komplexes Datenobjekt oder eine gesamte Elementstruktur sein. Bei einem Datenobjekt wird die Datenvorlage (Template) verwendet, um eine Darstellung zu erstellen.
Zusammenfassung
WPF ist so konzipiert, dass Sie dynamische, datengesteuerte Präsentationssysteme erstellen können. Jeder Teil des Systems ist so konzipiert, dass Objekte durch Eigenschaftensätze erstellt werden, die das Verhalten steuern. Die Datenbindung ist ein grundlegender Bestandteil des Systems und ist auf jeder Ebene integriert.
Herkömmliche Anwendungen erstellen eine Anzeigeoberfläche und binden dann an bestimmte Daten. In WPF wird alles über das Steuerelement, jeder Aspekt der Anzeige, durch eine Art von Datenbindung generiert. Der in einer Schaltfläche gefundene Text wird angezeigt, indem ein zusammengesetztes Steuerelement in der Schaltfläche erstellt und dessen Anzeige an die Inhaltseigenschaft der Schaltfläche gebunden wird.
Wenn Sie mit der Entwicklung von WPF-basierten Anwendungen beginnen, wird es sich sehr vertraut anfühlen. Sie können Eigenschaften festlegen, Objekte verwenden und Datenbindungen in ähnlicher Weise wie bei Windows Forms oder ASP.NET durchführen. Mit einer tieferen Untersuchung der Architektur von WPF werden Sie feststellen, dass die Möglichkeit besteht, viel umfangreichere Anwendungen zu erstellen, die Daten grundsätzlich als Kerntreiber der Anwendung behandeln.
Siehe auch
.NET Desktop feedback