WPF Architecture
Cette rubrique fournit une visite guidée de la hiérarchie de classes Windows Presentation Foundation (WPF). Il couvre la plupart des principaux sous-systèmes de WPF et décrit comment ils interagissent. Il détaille également certains des choix effectués par les architectes de WPF.
System.Object
Le modèle de programmation WPF principal est exposé via du code managé. Au début de la phase de conception du WPF, il y avait plusieurs débats sur l’endroit où la ligne devait être dessinée entre les composants gérés du système et les composants non gérés. Le CLR fournit un certain nombre de fonctionnalités qui rendent le développement plus productif et plus robuste (notamment la gestion de la mémoire, la gestion des erreurs, le système de type commun, etc.), mais ils viennent à un coût.
Les principaux composants de WPF sont illustrés dans la figure ci-dessous. Les sections rouges du diagramme (PresentationFramework, PresentationCore et milcore) sont les principales parties de code de WPF. D’entre eux, un seul est un composant non managé - milcore. Milcore est écrit dans du code non managé afin d’activer une intégration étroite avec DirectX. Tout l’affichage dans WPF est effectué via le moteur DirectX, ce qui permet un rendu matériel et logiciel efficace. WPF exige également un contrôle précis de la mémoire et de l’exécution. Le moteur de composition dans milcore est extrêmement sensible aux performances et a dû renoncer à de nombreux avantages du CLR pour obtenir des performances.
La communication entre les parties managées et non managées de WPF est abordée plus loin dans cette rubrique. Le reste du modèle de programmation managé est décrit ci-dessous.
System.Threading.DispatcherObject
La plupart des objets dans WPF dérivent de DispatcherObject, qui fournit les constructions de base pour traiter la concurrence et le threading. WPF est basé sur un système de messagerie implémenté par le répartiteur. Cela fonctionne beaucoup comme la pompe de message Win32 familière ; en fait, le répartiteur WPF utilise des messages User32 pour effectuer des appels entre threads.
Il existe vraiment deux concepts fondamentaux à comprendre lors de la discussion sur la concurrence dans WPF : le répartiteur et l’affinité de thread.
Pendant la phase de conception de WPF, l’objectif était de passer à un seul thread d’exécution, mais un modèle non-thread « affinitized ». L’affinité de thread se produit lorsqu’un composant utilise l’identité du thread en cours d’exécution pour stocker un certain type d’état. La forme la plus courante consiste à utiliser le magasin local de threads (TLS) pour stocker l’état. L’affinité de thread nécessite que chaque thread logique d’exécution soit détenu par un seul thread physique dans le système d’exploitation, ce qui peut devenir gourmand en mémoire. En fin de compte, le modèle de threading de WPF a été aligné avec le modèle de threading User32 existant, qui repose sur une exécution monothread et l'affinité de thread. La principale raison de cela était l'interopérabilité : les systèmes comme OLE 2.0, le Presse-papiers et Internet Explorer nécessitent une exécution d’affinité de thread unique (STA).
Étant donné que vous avez des objets avec le thread STA, vous avez besoin d’un moyen de communiquer entre les threads et de vérifier que vous êtes sur le thread correct. Ici se trouve le rôle du répartiteur. Le répartiteur est un système de distribution de messages de base, avec plusieurs files d’attente hiérarchisées. Les exemples de messages incluent des notifications de saisie brute (mouvement de la souris), des fonctions de framework (disposition) ou des commandes utilisateur (exécuter cette méthode). En dérivant de DispatcherObject, vous créez un objet CLR qui a un comportement STA et recevrez un pointeur vers un répartiteur au moment de la création.
System.Windows.DependencyObject
L’une des philosophies architecturales principales utilisées dans la construction de WPF était une préférence pour les propriétés sur les méthodes ou les événements. Les propriétés sont déclaratives et vous permettent de spécifier plus facilement l’intention au lieu d’une action. Cela a également pris en charge un système piloté par un modèle ou piloté par les données pour afficher le contenu de l’interface utilisateur. Cette philosophie avait l’effet prévu de créer davantage de propriétés auxquelles vous pouviez établir une liaison, afin de mieux contrôler le comportement d’une application.
Afin d’avoir plus du système piloté par les propriétés, un système de propriétés plus riche que ce que le CLR fournit était nécessaire. Un exemple simple de cette richesse est les notifications de changement. Pour activer la liaison bidirectionnelle, vous avez besoin des deux côtés de la liaison pour prendre en charge la notification de modification. Pour avoir un comportement lié aux valeurs de propriété, vous devez être averti lorsque la valeur de propriété change. Microsoft .NET Framework a une interface, INotifyPropertyChange, ce qui permet à un objet de publier des notifications de modification, mais il est facultatif.
WPF fournit un système de propriétés plus riche, dérivé du type DependencyObject. Le système de propriétés est vraiment un système de propriétés « dépendance » dans lequel il suit les dépendances entre les expressions de propriété et revalide automatiquement les valeurs de propriété lorsque les dépendances changent. Par exemple, si vous avez une propriété qui hérite (comme FontSize), le système est automatiquement mis à jour si la propriété change sur un parent d’un élément qui hérite de la valeur.
La base du système de propriétés WPF est le concept d’une expression de propriété. Dans cette première version de WPF, le système d’expressions de propriété est fermé et les expressions sont toutes fournies dans le cadre de l’infrastructure. C'est grâce aux expressions que le système de propriétés ne dispose pas de liaison de données, de style ou d'héritage codés en dur, mais qu'ils sont plutôt fournies par des couches ultérieures au sein du cadre.
Le système de propriétés prévoit également un stockage parcimonieux des valeurs de propriété. Étant donné que les objets peuvent avoir des dizaines (si pas des centaines) de propriétés, et la plupart des valeurs sont dans leur état par défaut (hérité, défini par des styles, etc.), pas chaque instance d’un objet doit avoir le poids total de chaque propriété définie dessus.
La dernière fonctionnalité du système de propriétés est la notion de propriétés jointes. Les éléments WPF sont basés sur le principe de la composition et de la réutilisation des composants. Il arrive souvent que certains éléments contenant (comme un élément de disposition Grid) nécessitent des données supplémentaires sur les éléments enfants pour contrôler leur comportement (comme les informations de ligne/colonne). Au lieu d’associer toutes ces propriétés à chaque élément, tout objet est autorisé à fournir des définitions de propriétés pour tout autre objet. Ceci est similaire aux fonctionnalités « expando » de JavaScript.
System.Windows.Media.Visual
Avec un système défini, l'étape suivante consiste à afficher des pixels à l'écran. La classe Visual fournit une arborescence d’objets visuels, chacune contenant éventuellement des instructions de dessin et des métadonnées sur le rendu de ces instructions (découpage, transformation, etc.). Visual est conçu pour être extrêmement léger et flexible, de sorte que la plupart des fonctionnalités n’ont pas d’exposition d’API publique et s’appuient fortement sur des fonctions de rappel protégées.
Visual est vraiment le point d’entrée du système de composition WPF. Visual est le point de connexion entre ces deux sous-systèmes, l’API managée et le milcore non managé.
WPF affiche les données en parcourant les structures de données non managées gérées par le milcore. Ces structures, appelées nœuds de composition, représentent une arborescence d’affichage hiérarchique avec des instructions de rendu à chaque nœud. Cette arborescence, illustrée à droite de la figure ci-dessous, n’est accessible qu’à l’aide d’un protocole de messagerie.
Lors de la programmation de WPF, vous créez des éléments Visual et des types dérivés, qui communiquent en interne à l’arborescence de composition via ce protocole de messagerie. Chaque Visual dans WPF peut créer un, aucun ou plusieurs nœuds de composition.
Il existe ici un détail architectural très important : l’arborescence entière des visuels et des instructions de dessin est mise en cache. En termes graphiques, WPF utilise un système de rendu conservé. Cela permet au système de repeindre à des taux d’actualisation élevés sans que le système de composition ne bloque les fonctions de rappel au code utilisateur. Cela permet d’empêcher l’apparition d’une application non réactive.
Un autre détail important qui n’est pas vraiment visible dans le diagramme est la façon dont le système effectue réellement la composition.
Dans User32 et GDI, le système fonctionne sur un système de découpage en mode immédiat. Lorsqu’un composant doit être rendu, le système établit une limite de découpage en dehors de laquelle le composant n’est pas autorisé à toucher les pixels, puis le composant est invité à peindre des pixels dans cette zone. Ce système fonctionne très bien dans les systèmes limités en mémoire, car lorsque quelque chose change, vous n’avez qu’à toucher le composant concerné , aucun deux composants ne contribuent jamais à la couleur d’un seul pixel.
WPF utilise un modèle de peinture « algorithme du peintre ». Cela signifie qu’au lieu de découper chaque composant, chaque composant est invité à effectuer le rendu de l’arrière vers l’avant de l’affichage. Cela permet à chaque composant de peindre sur l’affichage du composant précédent. L’avantage de ce modèle est que vous pouvez avoir des formes complexes et partiellement transparentes. Avec le matériel graphique moderne d’aujourd’hui, ce modèle est relativement rapide (ce qui n’était pas le cas lorsque User32/ GDI a été créé).
Comme mentionné précédemment, une philosophie fondamentale de WPF consiste à passer à un modèle de programmation plus déclaratif et centré sur les propriétés. Dans le système visuel, cela s’affiche dans quelques endroits intéressants.
Tout d’abord, si vous pensez au système graphique en mode conservé, cela s’éloigne vraiment d’un modèle de type DrawLine/DrawLine impératif, vers un modèle orienté données – nouvelle ligne()/nouvelle ligne(). Ce déplacement vers le rendu piloté par les données permet d’exprimer des opérations complexes sur les instructions de dessin à l’aide de propriétés. Les types dérivés de Drawing sont effectivement le modèle objet pour le rendu.
Deuxièmement, si vous évaluez le système d’animation, vous verrez qu’il est presque complètement déclaratif. Au lieu d’exiger qu’un développeur calcule l’emplacement suivant ou la couleur suivante, vous pouvez exprimer des animations sous la forme d’un ensemble de propriétés sur un objet d’animation. Ces animations peuvent ensuite exprimer l’intention du développeur ou du concepteur (déplacer ce bouton d’ici jusqu’à là en 5 secondes), et le système peut déterminer la façon la plus efficace d’y parvenir.
System.Windows.UIElement
UIElement définit les sous-systèmes principaux, notamment La disposition, l’entrée et les événements.
La disposition est un concept de base dans WPF. Dans de nombreux systèmes, il existe un ensemble fixe de modèles de disposition (HTML prend en charge trois modèles pour la disposition ; flux, absolu et tables) ou aucun modèle pour la disposition (User32 ne prend vraiment en charge que le positionnement absolu). WPF a commencé avec l’hypothèse que les développeurs et les concepteurs voulaient un modèle de disposition flexible et extensible, qui pourrait être piloté par des valeurs de propriété plutôt que par une logique impérative. Au niveau UIElement, le contrat de base pour la disposition est introduit : un modèle en deux phases avec les passages Measure et Arrange.
Measure permet à un composant de déterminer la taille à prendre. Il s’agit d’une phase distincte de Arrange, car il existe de nombreuses situations où un élément parent demande à un enfant de mesurer plusieurs fois pour déterminer sa position et sa taille optimales. Le fait que les éléments parents demandent aux éléments enfants de mesurer illustre une autre philosophie clé de WPF : taille selon le contenu. Tous les contrôles dans le WPF permettent de s'adapter à la taille naturelle de leur contenu. Cela facilite beaucoup la localisation et permet une disposition dynamique des éléments à mesure que les éléments sont redimensionnés. La phase Arrange permet à un parent de positionner et de déterminer la taille finale de chaque enfant.
Beaucoup de temps est souvent passé à parler du côté sortie de WPF - Visual et objets connexes. Toutefois, il y a également une quantité immense d'innovations du côté de l'entrée. Probablement le changement le plus fondamental dans le modèle d’entrée pour WPF est le modèle cohérent par lequel les événements d’entrée sont routés via le système.
L’entrée provient d’un signal sur un pilote de périphérique en mode noyau et est acheminée vers le processus et le thread appropriés via un processus complexe impliquant le noyau Windows et User32. Une fois que le message User32 correspondant à l’entrée est routé vers WPF, il est converti en message d’entrée brut WPF et envoyé au répartiteur. WPF permet aux événements d’entrée brutes d’être convertis en plusieurs événements réels, ce qui permet aux fonctionnalités telles que « MouseEnter » d’être implémentées à un bas niveau du système avec une livraison garantie.
Chaque événement d’entrée est converti en au moins deux événements : un événement « aperçu » et l’événement réel. Tous les événements dans WPF ont une notion de routage via l’arborescence d’éléments. Les événements sont dits « bulles » s’ils traversent d’une cible vers la racine, et sont dits « tunnel » s’ils commencent à la racine et traversent jusqu’à une cible. Tunnel d’événements d’aperçu d’entrée, permettant à n’importe quel élément de l’arborescence de filtrer ou de prendre des mesures sur l’événement. Les événements réguliers (non-apercçu) remontent ensuite de la cible jusqu’à la racine.
Cette répartition entre la phase de tunnel et de bulle permet à l’implémentation de fonctionnalités telles que les accélérateurs clavier de fonctionner de manière cohérente dans un monde composite. Dans User32, vous implémentez des accélérateurs clavier en ayant une table globale unique contenant tous les accélérateurs que vous souhaitez prendre en charge (mappage Ctrl+N sur « Nouveau »). Dans le répartiteur de votre application, vous appelez TranslateAccelerator qui analyse les messages d’entrée dans User32 et détermine si un accélérateur enregistré correspond. Dans WPF, cela ne fonctionne pas car le système est entièrement « composable » : tout élément peut gérer et utiliser n’importe quel accélérateur clavier. Le fait de disposer de ce modèle de deux phases pour l’entrée permet aux composants d’implémenter leur propre « TranslateAccelerator ».
Pour aller plus loin, UIElement introduit également la notion de CommandBindings. Le système de commande WPF permet aux développeurs de définir des fonctionnalités en termes de point de terminaison de commande , quelque chose qui implémente ICommand. Les liaisons de commande permettent à un élément de définir un mappage entre un mouvement d’entrée (Ctrl+N) et une commande (Nouveau). Les mouvements d’entrée et les définitions de commandes sont extensibles et peuvent être câblés ensemble au moment de l’utilisation. Cela rend trivial, par exemple, de permettre à un utilisateur final de personnaliser les liaisons de clé qu’il souhaite utiliser dans une application.
À ce stade de la rubrique, les fonctionnalités « principales » de WPF – fonctionnalités implémentées dans l’assembly PresentationCore ont été le focus. Lors de la création de WPF, une séparation propre entre les éléments fondamentaux (comme le contrat pour la disposition avec Measure et Arrange) et les éléments de framework (comme l’implémentation d’une disposition spécifique comme Grid) était le résultat souhaité. L’objectif était de fournir un point d’extensibilité à un niveau inférieur de la pile qui permettrait aux développeurs externes de créer leurs propres cadres si nécessaire.
System.Windows.FrameworkElement
FrameworkElement peut être examiné de deux façons différentes. Il introduit un ensemble de stratégies et de personnalisations sur les sous-systèmes introduits dans les couches inférieures de WPF. Il introduit également un ensemble de nouveaux sous-systèmes.
La stratégie principale introduite par FrameworkElement est autour de la disposition de l’application. FrameworkElement s’appuie sur le contrat de disposition de base introduit par UIElement et ajoute la notion d’un « emplacement » de disposition qui permet aux auteurs de disposition de disposer d’un ensemble cohérent de sémantiques de disposition pilotées par les propriétés. Les propriétés telles que HorizontalAlignment, VerticalAlignment, MinWidthet Margin (pour ne nommer que quelques-uns) donnent à tous les composants dérivés de FrameworkElement comportement cohérent à l’intérieur des conteneurs de disposition.
FrameworkElement offre également une exposition d’API plus facile à de nombreuses fonctionnalités trouvées dans les couches principales de WPF. Par exemple, FrameworkElement fournit un accès direct à l’animation via la méthode BeginStoryboard. Un Storyboard permet d'écrire plusieurs animations pour un ensemble de propriétés à l'aide d'un script.
Les deux éléments les plus critiques introduits par FrameworkElement sont des liaisons de données et des styles.
Le sous-système de liaison de données dans WPF doit être relativement familier à toute personne qui a utilisé Windows Forms ou ASP.NET pour créer une interface utilisateur d’application. Dans chacun de ces systèmes, il existe un moyen simple d’exprimer que vous souhaitez qu’une ou plusieurs propriétés d’un élément donné soient liées à une partie de données. WPF prend entièrement en charge la liaison de propriétés, la transformation et la liaison de liste.
L’une des fonctionnalités les plus intéressantes de la liaison de données dans WPF est l’introduction de modèles de données. Les modèles de données vous permettent de spécifier de manière déclarative la façon dont un élément de données doit être visualisé. Au lieu de créer une interface utilisateur personnalisée qui peut être liée aux données, vous pouvez à la place transformer le problème et laisser les données déterminer l’affichage qui sera créé.
Le style est véritablement une forme légère de liaison des données. En utilisant le style, vous pouvez lier un ensemble de propriétés d’une définition partagée à une ou plusieurs instances d’un élément. Les styles sont appliqués à un élément par référence explicite (en définissant la propriété Style) ou implicitement en associant un style au type CLR de l’élément.
System.Windows.Controls.Control
La caractéristique la plus significative de Control est la modélisation. Si vous pensez au système de composition de WPF comme un système de rendu en mode conservé, la création de modèles permet à un contrôle de décrire son rendu de manière paramétrable et déclarative. Un ControlTemplate n'est qu'un simple script pour créer un ensemble d'éléments enfants, avec des liaisons aux propriétés offertes par le contrôleur.
Control fournit un ensemble de propriétés de stock, Foreground, Background, Padding, pour nommer quelques-uns, que les auteurs de modèles peuvent ensuite utiliser pour personnaliser l’affichage d’un contrôle. L’implémentation d’un contrôle fournit un modèle de données et un modèle d’interaction. Le modèle d’interaction définit un ensemble de commandes (comme Close pour une fenêtre) et des liaisons aux mouvements d’entrée (comme cliquer sur le X rouge dans le coin supérieur de la fenêtre). Le modèle de données fournit un ensemble de propriétés pour personnaliser le modèle d’interaction ou personnaliser l’affichage (déterminé par le modèle).
Ce fractionnement entre le modèle de données (propriétés), le modèle d’interaction (commandes et événements) et le modèle d’affichage (modèles) permet une personnalisation complète de l’apparence et du comportement d’un contrôle.
Un aspect commun du modèle de données des contrôles est le modèle de contenu. Si vous examinez un contrôle comme Button, vous verrez qu’il a une propriété nommée « Contenu » de type Object. Dans Windows Forms et ASP.NET, cette propriété est généralement une chaîne. Toutefois, cela limite le type de contenu que vous pouvez placer dans un bouton. Le contenu d’un bouton peut être une chaîne simple, un objet de données complexe ou une arborescence d’éléments entière. Dans le cas d’un objet de données, le modèle de données est utilisé pour construire un affichage.
Résumé
WPF est conçu pour vous permettre de créer des systèmes de présentation dynamiques pilotés par les données. Chaque partie du système est conçue pour créer des objets à travers des jeux de propriétés qui déterminent le comportement. La liaison de données est une partie fondamentale du système et est intégrée à chaque couche.
Les applications traditionnelles créent un affichage, puis se lient à certaines données. Dans WPF, tout ce qui concerne le contrôle, chaque aspect de l’affichage, est généré par un type de liaison de données. Le texte trouvé à l’intérieur d’un bouton s’affiche en créant un contrôle composé à l’intérieur du bouton et en liant son affichage à la propriété de contenu du bouton.
Lorsque vous commencez à développer des applications WPF, cela devrait vous sembler très familier. Vous pouvez définir des propriétés, utiliser des objets et lier des données de la même façon que vous pouvez utiliser Windows Forms ou ASP.NET. Avec un examen approfondi de l’architecture de WPF, vous constaterez que la possibilité existe de créer des applications beaucoup plus riches qui traitent fondamentalement les données comme le pilote principal de l’application.
Voir aussi
- Visual
- UIElement
- ICommand
- FrameworkElement
- DispatcherObject
- CommandBinding
- Control
- Vue d’ensemble de la liaison de données
- Disposition du schéma
- Vue d'ensemble de l'animation
.NET Desktop feedback