Architecture de WPF

Cette rubrique propose une visite guidée de la hiérarchie des classes Windows Presentation Foundation (WPF). Elle couvre la plupart des principaux sous-systèmes de WPF, et décrit la façon dont ils interagissent. Elle passe également en revue certains choix opérés par les architectes de WPF.

Cette rubrique comprend les sections suivantes.

  • System.Object
  • System.Threading.DispatcherObject
  • System.Windows.DependencyObject
  • System.Windows.Media.Visual
  • System.Windows.UIElement
  • System.Windows.FrameworkElement
  • System.Windows.Controls.Control
  • Résumé
  • Rubriques connexes

System.Object

Le modèle de programmation WPF principal est exposé via le code managé. Très tôt dans la phase de conception de WPF, il y a eu un certain nombre de débats quant à l'endroit où tracer la ligne de démarcation entre les composants managés du système et les composants non managés. Le CLR fournit un certain nombre de fonctions qui rendent le développement plus productif et robuste (notamment la gestion de la mémoire, la gestion des erreurs, le système de type commun (CTS, Common Type System), etc.), mais elles ne sont pas sans 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 du code de WPF. Parmi celles-ci, une seule correspond à un composant non managé – milcore. Milcore est écrit en code non managé afin de permettre une forte intégration avec DirectX. Tout l'affichage dans WPF s'effectue via le moteur DirectX, pour un rendu matériel et logiciel efficace. WPF nécessitait également un contrôle renforcé sur la mémoire et l'exécution. Le moteur de composition dans milcore est extrêmement sensible aux performances, et nécessitait le renoncement à de nombreux avantages de CLR pour réaliser un gain de performances.

Position de WPF dans le .NET Framework.

La communication entre les parties managée et non managée 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 la gestion de la concurrence et des threads. WPF est basé sur un système de messagerie implémenté par le répartiteur. Ceci fonctionne un peu à l'image de la bien connue pompe de messages Win32 ; en fait, le répartiteur WPF utilise des messages User32 pour effectuer des appels à travers les threads.

Il existe en fait deux concepts fondamentaux qu'il faut bien comprendre quand il s'agit de l'accès concurrentiel WPF – le répartiteur et l'affinité de thread.

Pendant la phase de conception WPF, le but était de passer à un thread unique d'exécution, mais un modèle « affinitisé » non-thread. L'affinité de thread se produit quand un composant utilise l'identité du thread d'exécution pour stocker un type d'état. Sa forme la plus courante est d'utiliser le stockage local des threads ou TLS (Thread Local Store) pour stocker l'état. L'affinité de thread requiert que chaque thread logique d'exécution soit la propriété d'un seul thread physique dans le système d'exploitation, qui peut devenir à forte intensité de mémoire. Au final, le modèle de thread WPF est resté synchronisé avec le modèle existant de thread User32 d'exécution thread unique avec affinité de thread. La raison principale de ceci est l'interopérabilité – des systèmes comme OLE 2.0, le Presse-papiers et Internet Explorer qui exigent tous une exécution de type STA (Single Thread Affinity).

Dans la mesure où vous avez des objets avec des threads STA, vous avez besoin d'un mode de communication entre les threads, et de valider que vous êtes sur le thread approprié. C'est là que se situe le rôle du répartiteur. Le répartiteur est un système de répartition de messages de base, avec plusieurs files d'attente de priorités différentes. Les exemples de messages incluent les notifications d'entrée brute (souris déplacée), des fonctions d'infrastructure (disposition) ou des commandes utilisateur (exécuter cette méthode). En dérivant de DispatcherObject, vous créez un objet CLR doté d'un comportement STA , et obtenez un pointeur vers un répartiteur au moment de la création.

System.Windows.DependencyObject

L'une des principales philosophies d'architecture utilisées dans la construction de WPF reposait sur une préférence pour les propriétés par rapport aux méthodes ou événements. Les propriétés sont déclaratives et permettent de spécifier plus facilement l'intention au lieu de l'action. Cela prenait également en charge un système piloté par un modèle, ou piloté par des données, pour afficher le contenu de l'interface utilisateur. Cette philosophie produisait l'effet voulu de créer davantage de propriétés qu'il n'en fallait pour établir des liaisons, afin de mieux contrôler le comportement d'une application.

Pour que le système soit davantage piloté par les propriétés, un système de propriétés plus riche que celui fourni par CLR était nécessaire. Un exemple simple de cette richesse est celui des notifications des modifications. Pour permettre une liaison bidirectionnelle, les deux côtés de la liaison doivent prendre en charge la notification de changement. Pour que le comportement soit lié aux valeurs de propriétés, vous devez être notifié en cas de changement d'une valeur de propriété. Le Microsoft .NET Framework possède une interface, INotifyPropertyChange, qui permet à un objet de publier des notifications de changement, même s'il s'agit d'une option.

WPF fournit un système de propriétés plus riche, dérivé du type DependencyObject. Le système de propriétés est véritablement un système de propriétés de « dépendance » dans la mesure où il suit les dépendances entre les expressions de propriété et revalide automatiquement les valeurs des propriétés 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 fondation du système de propriétés WPF est le concept d'une expression de propriété. Lors de la 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. Les expressions expliquent pourquoi le système de propriétés n'a pas de liaison de données, de styles ou d'héritage codé en dur, mais tout ceci est fourni par des couches ultérieures dans l'infrastructure.

Le système de propriétés fournit également un stockage limité des valeurs de propriétés. Dans la mesure où les objets ont des douzaines (voire des centaines) de propriétés, et que la plupart des valeurs conservent leur état par défaut (hérité, défini par les styles, etc.), toutes les instances d'un objet n'ont pas besoin d'avoir tout le poids de chaque propriété défini sur elles.

La nouvelle fonctionnalité définitive du système de propriétés est la notion de propriétés jointes. Les éléments WPF reposent sur le principe de composition et de réutilisation des composants. Il arrive souvent qu'un élément conteneur (par exemple, un élément de disposition Grid) ait besoin de données supplémentaires sur des éléments enfants pour contrôler son comportement (informations Ligne/Colonne, par exemple). Au lieu d'associer toutes ces propriétés dans chaque élément, tout objet est autorisé à fournir des définitions de propriétés pour tout autre objet. Ceci est similaire aux fonctions « expando » de JavaScript.

System.Windows.Media.Visual

Lorsqu'un système est défini, la prochaine étape est l'obtention de pixels dessinés sur l'écran. La classe Visual assure la génération d'une arborescence d'objets visuels, chacun contenant éventuellement des instructions de dessin et des métadonnées relatives au rendu de ces instructions (découpage, transformation, etc.). Visual est conçu pour être extrêmement léger et flexible. Par conséquent, la plupart des fonctionnalités n'ont pas d'exposition d'API publique et dépendent fortement 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, les API managées et le milcore non managé.

WPF affiche les données en parcourant les structures de données non managées managées par le milcore. Ces structures, appelées nœuds de composition, représentent un arborescence d'affichage hiérarchique avec des instructions de rendu à chaque nœud. Cette arborescence, illustrée côté droit de la figure ci-dessus, est accessible uniquement par l'intermédiaire 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 avec l'arborescence de composition par le biais du protocole de messagerie. Chaque Visual dans WPF peut créer un ou plusieurs nœuds de composition, ou aucun.

Arborescence d'éléments visuels de Windows Presentation Foundation.

Il convient de noter un détail architectural très important ici – toute l'arborescence des objets 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 se régénérer à des fréquences d'actualisation plus élevées sans que le système de composition se bloque lors de rappels au code utilisateur. Cela aide à prévenir l'apparition d'une application qui ne répond pas.

Un autre détail important qui n'est pas vraiment évident dans le diagramme est la façon dont le système exécute réellement la composition.

Dans User32 et GDI, le système fonctionne sur un système de découpage en mode immédiat. Quand un composant doit être rendu, le système établit des limites de découpage en dehors desquelles le composant n'a pas le droit de toucher aux pixels, puis il est demandé au composant de peindre les pixels dans cette zone. Cette approche fonctionne très bien sur les systèmes soumis à des contraintes de mémoire car lorsque quelque chose change, vous devez intervenir uniquement au niveau du composant concerné – deux composants ne contribuent jamais ensemble à la couleur d'un pixel unique.

WPF utilise le modèle de peinture « algorithme du peintre ». Cela signifie qu'au lieu de découper chaque composant, il est demandé à chaque composant d'effectuer un rendu de l'arrière vers l'avant de l'affichage. Chaque composant peut ainsi peindre par-dessus l'affichage du précédent composant. L'avantage de ce modèle est que vous pouvez avoir des formes complexes partiellement transparentes. Grâce au matériel graphique moderne d'aujourd'hui, ce modèle est relativement rapide (ce qui n'était pas le cas lors de la création de User32/ GDI).

Comme indiqué plus haut, une philosophie de base de WPF est de passer à un modèle de programmation plus déclaratif, plus « axé sur les propriétés ». Dans le système visuel, ceci apparaît à deux emplacements intéressants.

Tout d'abord, si vous réfléchissez au système graphique retenu, cela revient en fait à s'éloigner d'un modèle de type DrawLine/DrawLine, pour aller vers un modèle orienté données – new Line()/new Line(). Ce transfert vers un rendu piloté par les données permet d'exprimer des opérations plus complexes sur les instructions de dessin en utilisant des propriétés. Les types dérivés de Drawing sont effectivement le modèle de l'objet pour le rendu.

Ensuite, si vous évaluez le système d'animation, vous verrez qu'il est presque entière déclaratif. Au lieu d'obliger un développeur à calculer l'emplacement suivant, ou la couleur suivante, vous pouvez exprimer des animations en tant que jeu de propriétés sur un objet d'animation. Ces animations peuvent alors exprimer l'intention du développeur ou du concepteur (déplacer ce bouton d'ici à là en 5 secondes), et le système peut déterminer la façon la plus efficace d'accomplir cela.

System.Windows.UIElement

UIElement définit les sous-systèmes de base, y compris Layout, Input et Events.

Layout est un concept fondamental dans WPF. Dans de nombreux systèmes, il existe soit un jeu fixe de modèles de disposition (le HTML prend en charge trois modèles de disposition : fluide, absolu, et par tables) ou aucun modèle de disposition (User32 prend uniquement en charge le positionnement absolu). Aux débuts de WPF, l'hypothèse était que les développeurs et concepteurs souhaitaient un modèle de disposition flexible et extensible qui pourrait reposer sur des valeurs de propriété plutôt que sur la logique impérative. Au niveau de UIElement, le contrat de base de disposition est introduit – un modèle à double phase avec des passes Measure et Arrange.

Measure permet à un composant de déterminer la taille qu'il souhaite prendre. Il s'agit d'une phase distincte de Arrange car il existe de nombreuses situations où un élément parent demandera à un enfant de mesurer plusieurs fois afin de 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 – la taille par rapport au contenu. Tous les contrôles WPF prennent en charge la possibilité de se dimensionner selon la taille naturelle de leur contenu. Cela facilite la localisation, et permet une disposition dynamique des éléments lors du redimensionnement. La phase Arrange permet à un parent de se positionner et de déterminer la taille finale de chaque enfant.

Une bonne partie du temps est consacrée à la discussion à propos du côté sortie de WPF – Visual et des objets connexes. Cependant, il existe également une extraordinaire quantité d'innovations côté entrée. Le changement le plus fondamental dans le modèle d'entrée pour WPF est probablement le modèle homogène par l'intermédiaire duquel les événements d'entrée sont routés dans le système.

L'entrée est générée en tant que signal sur un pilote de périphérique en mode noyau et est routée vers le processus et le thread appropriés via un processus complexe impliquant le noyau Windows et User32. Une fois le message User32 qui correspond à l'entrée routé vers WPF, il est converti en message d'entrée brut WPF et envoyé au répartiteur. WPF permet de convertir les événements d'entrée bruts en différents événements réels. Ce faisant, certaines fonctionnalités telles que « MouseEnter » peuvent être implémentées à un niveau inférieur du système afin de garantir leur transmission.

Chaque événement d'entrée est converti au moins en deux événements – un événement « aperçu » et l'événement actuel. Tous les événements dans WPF ont une notion de routage via l'arborescence des éléments. Les événements « se propagent » quand ils passent d'une cible en haut de l'arborescence à la racine, et « passent dans un tunnel » quand ils commencent à la racine et passent à une cible. Le tunnel des événements d'aperçu d'entrée, qui donne à tout élément dans l'arborescence la possibilité de filtrer ou de prendre une action sur l'événement. Les événements ordinaires (sans aperçu) se propagent de la cible en remontant vers la racine.

Cette différence entre les phases de tunnel et de propagation fait que l'implémentation de fonctions comme les accélérateurs de clavier fonctionne de façon homogène dans un monde composite. Dans User32, vous pouvez implémenter les accélérateurs de clavier en ayant une seule table globale contenant tous les accélérateurs que vous voulez prendre en charge (Ctrl+N correspondant à « Nouveau »). Dans le répartiteur de votre application, vous pouvez appeler TranslateAccelerator qui « renifle » les messages d'entrée dans User32 et détermine si l'un d'eux correspond à un accélérateur enregistré. Dans WPF, ceci ne fonctionnerait pas car le système est entièrement « composable » – tout élément peut gérer et utiliser un accélérateur de clavier. Le fait d'avoir ce modèle à deux phases pour l'entrée permet aux composants d'implémenter leur propre « TranslateAccelerator ».

Pour aller une étape plus loin, UIElement introduit également la notion de CommandBindings. Le système de commandes WPF permet aux développeurs de définir les 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 commande sont extensibles, et peuvent être liés ensemble au moment de l'utilisation. Cela rend banal le fait, par exemple, pour un utilisateur final de personnaliser les liaisons de clés qu'il veut utiliser dans une application.

À ce stade de la rubrique, les fonctions « fondamentales » de WPF, les fonctions implémentées dans l'assembly PresentationCore, ont monopolisé toute l'attente. Lors de la génération de WPF, une séparation nette entre les éléments fondamentaux (comme le contrat de disposition avec Mesurer et Disposer) et les éléments d'infrastructure (comme l'implémentation d'une disposition spécifique de type Grid) était le résultat voulu. L'objectif était de fournir un point d'extensibilité bas dans la pile qui permettrait aux développeurs externes de créer leurs propres infrastructures, en cas de besoin.

System.Windows.FrameworkElement

FrameworkElement peut être considéré de deux façons différentes. Il introduit un ensemble de stratégies et de personnalisations sur les sous-systèmes présents dans les couches inférieures de WPF. Il introduit également un ensemble de nouveaux sous-systèmes.

La stratégie principale présentée dans FrameworkElement porte sur la disposition d'application. FrameworkElement tire parti du contrat de disposition de base présenté par UIElement et lui ajoute la notion d'« emplacement » de disposition qui aide les auteurs de disposition à bénéficier d'un jeu cohérent de sémantiques de disposition orientées propriété. Des propriétés comme HorizontalAlignment, VerticalAlignment, MinWidth et Margin (pour n'en citer que quelques-unes) fournissent à tous les composants dérivés de FrameworkElement un comportement homogène dans les conteneurs de disposition.

FrameworkElement fournit également une exposition API plus facile pour de nombreuses fonctions qui se situent dans les couches centrales de WPF. Par exemple, FrameworkElement permet un accès direct à l'animation via la méthode BeginStoryboard. Un Storyboard permet d'écrire le script de plusieurs animations par rapport à un ensemble de propriétés.

Les deux choses les plus critiques introduite par FrameworkElement sont les liaisons de données et les styles.

Le sous-système de liaison de données dans WPF devrait être relativement familier à quiconque a utilisé Windows Forms ou ASP.NET pour la création d'une application user interface (UI). Dans chacun de ces systèmes, il existe une méthode simple pour exprimer que vous souhaitez lier une ou plusieurs propriétés d'un élément spécifique à une donnée. WPF assure la prise en charge complète de la liaison de propriété, de la transformation et de la liaison de liste.

L'une des fonctions les plus intéressantes de la liaison des données dans WPF est l'introduction des modèles de données. Les modèles de données vous permettent de spécifier de manière déclarative comment les données peuvent être visualisées. Au lieu de créer une interface utilisateur personnalisée pouvant être liée à des données, vous pouvez plutôt contourner le problème et laisser les données déterminer l'affichage qui sera créé.

Les styles constituent une forme légère de liaison de données. En utilisant les styles, vous pouvez lier un ensemble de propriétés à partir d'une définition partagée avec une ou plusieurs instances d'un élément. Les styles sont appliqués à un élément soit par référence explicite (en définissant la propriété Style) soit implicitement en associant un style avec le type CLR de l'élément.

System.Windows.Controls.Control

La fonction la plus importante du contrôle est la création de modèles. Si vous considérez le système de composition WPF comme un système de rendu en mode Conservation, la création de modèles permet à un contrôle de décrire son rendu d'une manière déclarative et paramétrable. Un ControlTemplate n'est vraiment rien d'autre qu'un ensemble d'éléments enfants, avec des liaisons aux propriétés offertes par le contrôle.

Control fournit un ensemble de propriétés stock, Foreground, Background, Padding, pour n'en citer que quelques-unes, que les créateurs de modèles peuvent utiliser pour personnaliser l'affichage d'un contrôle. L'implementation 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 jeu de commandes (Fermer une fenêtre, par exemple) et des liaisons aux mouvements d'entrée (comme le fait de cliquer sur le X rouge dans l'angle droit supérieur d'une fenêtre). Le modèle de données fournit un ensemble de propriétés soit pour personnaliser le modèle d'interaction soit pour personnaliser l'affichage (déterminé par le modèle).

Cet écart 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 des contrôles de modèle de données est le modèle de contenu. Si vous considérez un contrôle comme un Button, vous verrez qu'il a une propriété nommée « Content » de type Object. Dans Windows Forms et ASP.NET, cette propriété est généralement une chaîne – cependant, cela limite le type de contenu que vous pouvez mettre dans un bouton. Le contenu d'un bouton peut être une simple chaîne, un objet données complexe ou une arborescence complète d'éléments. Dans le cas d'un objet de données, le modèle de données permet de construire un affichage.

Résumé

WPF est conçu de manière à vous permettre de créer des systèmes de présentation dynamiques, pilotés par données. Chaque partie du système est destinée à créer des objets via des ensembles de propriétés qui pilotent le comportement. La liaison de données est une partie fondamentale du système, et est intégrée au niveau de chaque couche.

Les applications classiques créent un affichage, puis effectuent une liaison avec certaines données. Dans WPF, tout ce qui concerne le contrôle, chaque aspect de l'affichage, est généré par un certain type de liaison de données. Le texte placé dans un bouton est affiché 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 basées sur WPF, l'opération devrait sembler très familière. Vous pouvez définir des propriétés, utiliser des objets et lier de données, un peu de la même façon que vous pouvez utiliser Windows Forms ou ASP.NET. Avec un examen plus approfondi de l'architecture de WPF, vous trouverez qu'il existe la possibilité de créer des applications plus riches qui traitent fondamentalement les données en tant qu'élément moteur de l'application.

Voir aussi

Référence

Visual

UIElement

ICommand

FrameworkElement

DispatcherObject

CommandBinding

Control

Concepts

Vue d'ensemble de la liaison de données

Système de disposition

Vue d'ensemble de l'animation