Données et WPF
Personnalisez l'affichage des données avec la liaison de données et WPF
Josh Smith
Cet article aborde les sujets suivants :
- Liaison de données WPF
- Affichage des données et données hiérarchiques
- Utilisation des modèles
- Validation des entrées
|
Cet article utilise les technologies suivantes :
WPF, XAML, C#
|

Sommaire
Lorsque Windows® Presentation Foundation (WPF) est apparu pour la première fois sur le radar .NET, la plupart des articles et des applications de démonstration faisaient l'éloge de son moteur de rendu et de ses superbes capacités 3D. Bien que ces exemples soient amusants à lire et à essayer, ils ne démontrent pas la puissance pratique des fonctionnalités de WPF. La plupart d'entre nous n'avons pas besoin de créer des applications avec des cubes vidéo pivotants qui explosent en feu d'artifice lorsque l'on clique dessus. La plupart d'entre nous gagne son pain en créant des logiciels permettant d'afficher et d'éditer des grandes quantités de données métier ou scientifiques complexes.
La bonne nouvelle est que WPF offre un excellent support de gestion de l'affichage et d'édition de données complexes. Dans l'édition de décembre 2007 du MSDN
® Magazine, John Papa a écrit l'article « Liaison de données dans WPF » (
msdn.microsoft.com/magazine/cc163299), dans lequel il expliquait parfaitement les concepts essentiels de la liaison de données WPF. Ici, j'explorerai des scénarios de liaison des données plus avancés en me basant sur ce que John a présenté dans la rubrique Points sur les données. Après avoir lu cet article, vous connaîtrez les diverses façons permettant d'implémenter les exigences de liaison de données communes à la plupart des applications métier.
Lier dans du code
L'un des changements les plus importants introduits par WPF pour les développeurs d'applications de bureau sont l'emploi intensif et la prise en charge de la programmation déclarative. Les interfaces utilisateur et les ressources de WPF peuvent être déclarées à l'aide du langage XAML, un langage de balisage basé sur XML. La plupart des explications sur la liaison de données de WPF expliquent seulement comment travailler avec des liaisons dans du XAML. Puisque tout ce que l'on peut réaliser dans du XAML peut également s'obtenir dans du code, il est important que les développeurs professionnels du WPF apprennent à travailler avec la liaison de données aussi bien par programmation que de manière déclarative.
Dans de nombreuses situations, il est plus facile et rapide de déclarer des liaisons dans XAML. Puisque les systèmes deviennent de plus en plus complexes et dynamiques, il est parfois plus raisonnable d'utiliser des liaisons en code. Avant de poursuivre, revoyons quelques classes et méthodes communes impliquées dans la liaison de données par programmation.
Les éléments de WPF héritent les méthodes SetBinding et GetBindingExpression de FrameworkElement ou de FrameworkContentElement. Celles-ci ne sont que des méthodes pratiques qui appellent des méthodes du même nom dans la classe d'utilitaire BindingOperations. Le code suivant illustre comment utiliser la classe BindingOperations pour lier la propriété Text d'une zone de texte à l'une des propriétés d'un autre objet :
static void BindText(TextBox textBox, string property)
{
DependencyProperty textProp = TextBox.TextProperty;
if (!BindingOperations.IsDataBound(textBox, textProp))
{
Binding b = new Binding(property);
BindingOperations.SetBinding(textBox, textProp, b);
}
}
Vous pouvez séparer facilement une propriété en utilisant le code indiqué ici :
static void UnbindText(TextBox textBox)
{
DependencyProperty textProp = TextBox.TextProperty;
if (BindingOperations.IsDataBound(textBox, textProp))
{
BindingOperations.ClearBinding(textBox, textProp);
}
}
En éliminant la liaison, vous supprimez également la valeur liée à la propriété cible.
Déclarer une liaison de données dans XAML dissimule certains des détails sous-jacents. Lorsque vous commencez à travailler avec des liaisons en code, ces détails commencent à ressortir. Parmi ceux-ci, le fait que la relation entre une source et une cible de liaison est maintenue par une instance de la classe BindingExpression, et non pas par la liaison même. La classe Binding contient des informations de haut niveau que plusieurs BindingExpressions peuvent partager, mais la protection d'une liaison entre deux propriétés liées est garantie par une expression sous-jacente. Le code suivant illustre comment vous pouvez utiliser BindingExpression pour vérifier par programmation si la propriété Text d'une zone de texte est en cours de validation :
static bool IsTextValidated(TextBox textBox)
{
DependencyProperty textProp = TextBox.TextProperty;
var expr = textBox.GetBindingExpression(textProp);
if (expr == null)
return false;
Binding b = expr.ParentBinding;
return b.ValidationRules.Any();
}
Puisqu'une BindingExpression ne sait pas si elle est en cours de validation, il faut demander à sa liaison parent J'examinerai les techniques de validation des entrées un peu plus loin.
Utilisation de modèles
Une interface utilisateur efficace présente des données brutes de manière telle que l'utilisateur peut y trouver intuitivement les informations utiles. Ceci est l'essence de la visualisation de données. La liaison des données n'est qu'une pièce du grand puzzle de la visualisation des données. À l'exception des plus primitifs, tous les programmes WPF exigent un mode de présentation des données plus puissant que la simple liaison d'une propriété d'un contrôle à une propriété d'un objet de données. Les vrais objets de données ont beaucoup de valeurs associées et ces diverses valeurs doivent s'intégrer dans une représentation visuelle cohérente. C'est pourquoi WPF dispose de modèles de données.
La classe System.Windows.DataTemplate n'est qu'un des formulaires de modèle dans WPF. En général, un modèle est comme une forme de découpage que l'infrastructure WPF utilise pour créer des éléments visuels et faciliter le rendu d'objets qui n'ont pas de représentation visuelle intrinsèque. Lorsqu'un élément tente d'afficher un objet qui n'a pas une représentation visuelle intrinsèque, comme un objet métier personnalisé par exemple, vous pouvez dire à l'élément comment afficher l'objet en lui donnant un modèle DataTemplate.
Le DataTemplate peut produire autant d'éléments visuels que nécessaire pour afficher l'objet de données. Ces éléments utilisent des liaisons de données pour afficher les valeurs de propriété de l'objet de données. Si un élément ne sait pas comment afficher l'objet qu'il doit afficher, il lui applique simplement la méthode ToString et affiche le résultat dans un TextBlock.
Supposons que vous avez une classe simple appelée FullName qui contient un nom de personne. Vous voulez afficher une liste de noms dans laquelle le nom de famille est mis en évidence. Pour faire cela, vous pourriez créer un DataTemplate qui décrit comment afficher un objet FullName. Le code de la figure 1 indique la classe FullName ainsi que le codebehind pour une fenêtre qui affichera une liste de noms.

Figure 1 Affichage de FullNames avec un DataTemplate
public class FullName
{
public string FirstName { get; set; }
public char MiddleInitial { get; set; }
public string LastName { get; set; }
}
public partial class WorkingWithTemplates : Window
{
// This is the Window's constructor.
public WorkingWithTemplates()
{
InitializeComponent();
base.DataContext = new FullName[]
{
new FullName
{
FirstName = "Johann",
MiddleInitial = 'S',
LastName = "Bach"
},
new FullName
{
FirstName = "Gustav",
MiddleInitial = ' ',
LastName = "Mahler"
},
new FullName
{
FirstName = "Alfred",
MiddleInitial = 'G',
LastName = "Schnittke"
}
};
}
}
Comme indiqué à la figure 2, le fichier XAML de la fenêtre contient un ItemsControl. Il crée une liste simple d'éléments que l'utilisateur ne peut ni sélectionner ni supprimer. L'ItemsControl dispose d'un DataTemplate attribué à la propriété ItemTemplate, grâce à laquelle il affiche chaque instance FullName créée dans le constructeur de fenêtre. Remarquez que la propriété Text de la plupart des éléments TextBlock dans le DataTemplate est liée aux propriétés de l'objet FullName qu'ils représentent.

Figure 2 Affichage d'objets FullName avec un DataTemplate
<!-- This displays the FullName objects. -->
<ItemsControl ItemsSource="{Binding Path=.}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock FontWeight="Bold" Text="{Binding LastName}" />
<TextBlock Text=", " />
<TextBlock Text="{Binding FirstName}" />
<TextBlock Text=" " />
<TextBlock Text="{Binding MiddleInitial}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Lorsque cette application de démonstration s'exécute, elle ressemble à la figure 3. En utilisant un DataTemplate pour afficher le nom, il est facile d'accentuer le nom de famille de chaque personne parce que le FontWeight du TextBlock correspondant est en gras. Cet exemple simple démontre la relation fondamentale entre la liaison de données WPF et les modèles. En approfondissant ce sujet, je combinerai ces fonctionnalités pour visualiser des objets complexes de manière encore plus puissante.
Figure 3 Affichage de FullNames par un DataTemplate
Utilisation d'un DataContext hérité
Sauf mention contraire, toutes les liaisons sont liées implicitement à une propriété DataContext d'un élément. Le DataContext d'un élément renvoie, pour ainsi dire, à sa source de données. Comprendre comment DataContext fonction est particulièrement intéressant. Une fois que vous avez compris cet aspect subtil de DataContext, la conception d'interfaces utilisateur liées à des données complexes devient beaucoup plus simple.
La propriété DataContext d'un élément ne doit pas être définie pour qu'elle renvoie à un objet source de données. Si une valeur est attribuée au DataContext d'un élément ancêtre dans l'arborescence d'éléments (techniquement, l'arborescence logique), cette valeur sera automatiquement héritée par chaque élément descendant dans l'interface utilisateur. En d'autres termes, si un DataContext de fenêtre est défini pour renvoyer à un objet Foo, le DataContext de chaque élément dans la fenêtre renverra par défaut à ce même objet Foo. Vous pouvez facilement attribuer une valeur DataContext différente à tous les éléments de la fenêtre et, alors, tous les éléments descendants de l'élément hériteront de cette nouvelle valeur DataContext. Ceci est comparable à une propriété ambiante dans Windows Forms.
Dans la section précédente, j'ai examiné comment utiliser DataTemplates pour créer des visualisations d'objets de données. Les propriétés des éléments créés par le modèle de la figure 2 sont liées aux propriétés d'un objet FullName. Ces éléments se lient implicitement à leur DataContext. Le DataContext d'éléments créés par un Data-Template renvoie à l'objet de données pour lequel le modèle est utilisé, comme un objet FullName par exemple.
L'héritage de valeur de la propriété DataContext n'a rien de miraculeux Ce n'est que le résultat de la prise en charge des propriétés de dépendance héritées intégrée à WPF. Toute propriété de dépendance peut être une propriété héritée. Il suffit de spécifier un indicateur dans les métadonnées fournies lors de l'enregistrement de cette propriété dans le système de propriété de dépendance de WPF.
Un autre exemple de propriété de dépendance héritée est FontSize, que possèdent tous les éléments. Si vous définissez la propriété de dépendance FontSize sur une fenêtre, par défaut tous les éléments dans cette fenêtre afficheront leur texte à cette taille. L'infrastructure utilisée pour propager la valeur FontSize dans l'arborescence d'éléments est la même que celle qui propage le DataContext.
Le terme « héritage » assume, dans ce cas, un sens différent de celui qu'il véhicule lorsqu'il s'agit d'objets (dans ce cas, une sous-classe hérite des membres de la classe parent). L'expression « héritage de valeur de propriété » indique seulement la propagation de valeurs dans l'arborescence d'élément au moment de l'exécution. Naturellement, une classe peut hériter une propriété de dépendance qui prend en charge l'héritage de valeur, dans le sens orienté objet.
Utilisation d'affichages de collection
Lorsque les contrôles WPF se lient à une collection de données, ils ne sont pas liés directement à la collection même. Ils sont liés implicitement à un affichage wrappé automatiquement autour de cette collection. L'affichage implémente l'interface ICollectionView et peut être l'une de plusieurs implémentations concrètes telles que ListCollectionView.
Un affichage de collection a plusieurs responsabilités. Il suit le progrès de l'élément actuel dans la collection, c'est-à-dire, normalement, l'élément actif/sélectionné dans un contrôle de liste. Les affichages de collection sont également un moyen générique de triage, filtrage et groupage des éléments d'une liste. Différents contrôles peuvent être liés au même affichage autour d'une collection de façon telle qu'ils sont tous coordonnés les uns aux autres. Le code suivant démontre certaines fonctionnalités d'ICollectionView :
// Get the default view wrapped around the list of Customers.
ICollectionView view = CollectionViewSource.GetDefaultView(allCustomers);
// Get the Customer selected in the UI.
Customer selectedCustomer = view.CurrentItem as Customer;
// Set the selected Customer in the UI.
view.MoveCurrentTo(someOtherCustomer);
La propriété IsSynchronizedWithCurrentItem de tous les contrôles de liste (zone de liste, zone de liste déroulante et affichage des listes, par exemple) doit être définie sur true pour que les contrôles restent synchronisés avec la propriété Current-Item de l'affichage de collection. La classe abstraite Selector définit cette propriété. Si cette propriété n'est pas définie sur true, la sélection d'un élément dans le contrôle de liste ne permettra pas de mettre à jour le CurrentItem et l'attribution d'une nouvelle valeur à CurrentItem ne sera pas reflétée dans ce contrôle de liste.
Utilisation de données hiérarchiques
Le monde réel est plein de données hiérarchiques. Un client place des commandes multiples, une molécule est composée de nombreux atomes, un service compte de nombreux employés et un système solaire contient un ensemble de corps célestes. Cette gestion magistrale des détails vous est certainement connue.
WPF propose différentes façons de travailler avec des structures de données hiérarchiques dont chacune s'adapte à différentes situations. Il existe essentiellement deux solutions : utiliser de nombreux contrôles pour afficher des données ou à afficher différents niveaux d'une hiérarchie de données dans un contrôle. Examinons maintenant ces deux approches.
Utilisation de plusieurs contrôles pour afficher des données XML
Une façon très commune de travailler avec des données hiérarchiques est d'utiliser un contrôle séparé pour afficher chaque niveau de la hiérarchie. Par exemple, supposons que nous ayons un système qui représente des clients, des commandes et des détails de commande. Dans cette situation, nous pourrions avoir une zone de liste déroulante pour afficher des clients, une zone de liste pour afficher toutes les commandes du client sélectionné et, enfin, un ItemsControl pour afficher les détails de la commande sélectionnée. C'est une bonne façon d'afficher des données hiérarchiques très facile à implémenter dans WPF.
Inspirée du scénario décrit précédemment, la figure 4 montre un exemple simplifié de données qu'une application pourrait traiter, wrappé dans le composant XmlDataProvider de WPF. Une interface utilisateur comparable à celle de la figure 5 peut afficher ces données. Remarquez que les clients et les commandes sont sélectionnables, mais que les détails relatifs à une commande apparaissent dans une liste en lecture seule. Ceci est logique puisqu'un objet visuel doit être sélectionnable seulement s'il affecte l'état de l'application ou s'il est modifiable.

Figure 4 Customers XML Clients, Orders et OrderDetails
<XmlDataProvider x:Key="xmlData">
<x:XData>
<customers >
<customer name="Customer 1">
<order desc="Big Order">
<orderDetail product="Glue" quantity="21" />
<orderDetail product="Fudge" quantity="32" />
</order>
<order desc="Little Order">
<orderDetail product="Ham" quantity="1" />
<orderDetail product="Yarn" quantity="2" />
</order>
</customer>
<customer name="Customer 2">
<order desc="First Order">
<orderDetail product="Mousetrap" quantity="4" />
</order>
</customer>
</customers>
</x:XData>
</XmlDataProvider>
Figure 5 Une façon d'afficher les données XML
Le XAML de la figure 6 décrit comment utiliser ces divers contrôles pour afficher les données hiérarchiques ci-dessus. Cette fenêtre ne nécessite pas de code. Il existe entièrement dans XAML.

Figure 6 XAML pour lier des données XML hiérarchiques à une interface utilisateur
<Grid DataContext=
"{Binding Source={StaticResource xmlData},
XPath=customers/customer}"
Margin="4"
>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<!-- CUSTOMERS -->
<DockPanel Grid.Row="0">
<TextBlock DockPanel.Dock="Top" FontWeight="Bold" Text="Customers" />
<ComboBox
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding}"
>
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding XPath=@name}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</DockPanel>
<!-- ORDERS -->
<DockPanel Grid.Row="1">
<TextBlock DockPanel.Dock="Top" FontWeight="Bold" Text="Orders" />
<ListBox
x:Name="orderSelector"
DataContext="{Binding Path=CurrentItem}"
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding XPath=order}"
>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding XPath=@desc}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
<!-- ORDER DETAILS -->
<DockPanel Grid.Row="2">
<TextBlock DockPanel.Dock="Top" FontWeight="Bold"
Text="Order Details" />
<ItemsControl
DataContext=
"{Binding ElementName=orderSelector, Path=SelectedItem}"
ItemsSource="{Binding XPath=orderDetail}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock>
<Run>Product:</Run>
<TextBlock Text="{Binding XPath=@product}" />
<Run>(</Run>
<TextBlock Text="{Binding XPath=@quantity}" />
<Run>)</Run>
</TextBlock>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DockPanel>
</Grid>
Remarquez l'utilisation intensive de brèves requêtes XPath pour dire à WPF où obtenir les valeurs liées. La classe Binding expose une propriété XPath à laquelle vous pouvez attribuer n'importe quelle requête XPath prise en charge par la méthode XmlNode.SelectNodes. En réalité, WPF utilise cette méthode pour exécuter des requêtes XPath. Malheureusement, ceci signifie que, puisque XmlNode.SelectNodes ne prend actuellement pas en charge l'utilisation des fonctions XPath, la liaison de données WPF ne les prend pas en charge non plus.
La liste déroulante de clients et la zone de liste des commandes se lient toutes les deux à un jeu de nœuds résultant de la requête XPath exécutée par la liaison racine Grid's DataContext. La zone de liste DataContext renverra automatiquement le CurrentItem de l'affichage de collection wrappé autour de la collection de XmlNodes générée pour le Grid's DataContext. En d'autres termes, le DataContext de la zone de liste est le client actuellement sélectionné. Puisque l'ItemsSource de cette zone de liste est implicitement lié à son propre DataContext (parce qu'aucune autre source n'a été spécifiée) et que sa liaison ItemsSource exécute une requête de XPath pour obtenir les éléments de <order> du DataContext, l'ItemsSource est efficacement lié à la liste de commandes du client.
N'oubliez ne pas que, en créant une liaison avec les données XML, vous établissez, en réalité, une liaison avec les objets créés par un appel à XmlNode.SelectNodes. Si ne vous faites pas attention, vous risquez de vous retrouver avec des liaisons entre de multiples contrôles et des jeux de nœuds XmlNodes logiquement équivalents, mais physiquement distincts. Ceci est dû au fait que chaque appel à XmlNode.SelectNodes génère un nouveau jeu de nœuds XmlNodes, même si à chaque fois vous transmettez la même requête XPath au même XmlNode. Ceci est un problème spécifique à la liaison de données XML. Donc, si vous créez des liaisons vers des objets métier, vous pouvez tranquillement l'ignorer.
Utilisation de plusieurs contrôles pour afficher des objets métier
Supposons maintenant que vous souhaitiez créer une liaison avec les mêmes données que dans l'exemple précédent, mais que ces données existent en tant qu'objets métier au lieu de XML. En quoi cela modifierait la création d'une liaison avec les différents niveaux de la hiérarchie de données ? En quoi la technique utilisée serait similaire et en quoi serait-elle différente ?
Le code de la figure 7 illustre les classes simples utilisées pour créer des objets métier contenant les données avec lesquelles nous souhaitons créer une liaison. Ces classes forment le même schéma logique que les données XML utilisées dans la section précédente.

Figure 7 Classes pour créer une hiérarchie d'objets métier
public class Customer
{
public string Name { get; set; }
public List<Order> Orders { get; set; }
public override string ToString()
{
return this.Name;
}
}
public class Order
{
public string Desc { get; set; }
public List<OrderDetail> OrderDetails { get; set; }
public override string ToString()
{
return this.Desc;
}
}
public class OrderDetail
{
public string Product { get; set; }
public int Quantity { get; set; }
}
Le XAML de la fenêtre qui affiche ces objets se trouve à la figure 8. Il est très semblable au XAML de la figure 6, mais présente quelques différences qu'il est important de souligner. Par rapport au XAML, il dispose d'un constructeur de fenêtre qui crée des objets de données et définit le DataContext (alors que le XAML renvoyait au DataContext en tant que ressource). Remarquez qu'aucun DataContext des contrôles n'est explicitement défini. Ils héritent tous du même DataContext, qui est une instance List<Customer>.

Figure 8 XAML pour lier des objets métier hiérarchiques dans une interface utilisateur
<Grid Margin="4">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<!-- CUSTOMERS -->
<DockPanel Grid.Row="0">
<TextBlock DockPanel.Dock="Top" FontWeight="Bold" Text="Customers"
/>
<ComboBox
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding Path=.}"
/>
</DockPanel>
<!-- ORDERS -->
<DockPanel Grid.Row="1">
<TextBlock DockPanel.Dock="Top" FontWeight="Bold" Text="Orders" />
<ListBox
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding Path=CurrentItem.Orders}"
/>
</DockPanel>
<!-- ORDER DETAILS -->
<DockPanel Grid.Row="2">
<TextBlock DockPanel.Dock="Top" FontWeight="Bold"
Text="Order Details" />
<ItemsControl
ItemsSource="{Binding Path=CurrentItem.Orders.CurrentItem.
OrderDetails}"
>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock>
<Run>Product:</Run>
<TextBlock Text="{Binding Path=Product}" />
<Run>(</Run>
<TextBlock Text="{Binding Path=Quantity}" />
<Run>)</Run>
</TextBlock>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DockPanel>
</Grid>
Lors de la liaison à des objets métier au lieu de XML, une autre différence significative est que l'ItemsControl qui héberge les détails des commandes n'a pas besoin de créer une liaison avec le SelectedItem de la zone de liste de la commande. Cette approche était nécessaire dans le scénario de liaison XML parce qu'il n'offre aucun moyen générique de référencer l'élément actuel d'une liste dont les éléments proviennent d'une requête XPath locale.
En créant une liaison avec des objets métier au lieu de XML, il est simple de lier des niveaux imbriqués d'éléments sélectionnés. La liaison ItemsSource d'ItemsControl utilise cette fonctionnalité pratique en spécifiant deux fois CurrentItem dans le chemin de liaison : une fois pour le client sélectionné et une deuxième fois pour la commande sélectionnée. La propriété CurrentItem est un membre de l'ICollectionView sous-jacent wrappé autour de la source de données, comme mentionné précédemment dans l'article.
Il y a encore un point intéressant concernant la différence des méthodes de travail de XML et de l'objet métier. Puisque l'exemple XML crée une liaison avec XmlElements, vous devez fournir DataTemplates pour expliquer comment afficher les clients et les commandes. En créant une liaison avec des objets métier personnalisés, vous pouvez éviter cette surcharge en remplaçant simplement la méthode ToString des classes Customer et Order et en autorisant WPF à afficher la sortie de cette méthode pour ces objets. Cette ruse ne fonctionne que pour des objets qui peuvent avoir des représentations textuelles simples. Il n'est peut-être pas judicieux de l'utiliser si l'on traite des objets de données complexes.
Un contrôle pour afficher une hiérarchie entière
Jusque là, nous n'avons traité que des manières d'afficher des données hiérarchiques en affichant chaque niveau de la hiérarchie dans des contrôles séparés. Il est souvent utile et nécessaire d'afficher tous les niveaux d'une structure de données hiérarchique dans le même contrôle. L'exemple canonique de cette approche est le contrôle TreeView, qui prend en charge l'affichage et la traversée latérale de niveaux arbitraires de données imbriquées.
Vous pouvez remplir le TreeView WPF d'éléments de deux façons différentes. Une option consiste à ajouter manuellement les éléments, en code ou XAML, et l'autre à les créer via une liaison de données.
Le XAML suivant illustre comment l'on peut ajouter manuellement certains TreeViewItems à un TreeView dans XAML :
<TreeView>
<TreeViewItem Header="Item 1">
<TreeViewItem Header="Sub-Item 1" />
<TreeViewItem Header="Sub-Item 2" />
</TreeViewItem>
<TreeViewItem Header="Item 2" />
</TreeView>
La technique de création manuelle les éléments dans un TreeView est utile dans des situations dans lesquelles le contrôle affichera toujours un petit ensemble statique d'éléments. Si vous devez afficher de grandes quantités de données qui peuvent varier dans le temps, il est nécessaire d'utiliser une approche plus dynamique. Vous avez alors deux options. Vous pouvez écrire un code qui examine une structure de données, crée des TreeViewItems à partir des objets de données qu'il trouve et qui, enfin, ajoute ces éléments au TreeView. Sinon, vous pouvez profiter des modèles de données hiérarchiques et laisser que WPF fasse toute le travail.
Utilisation de modèles de données hiérarchiques
Vous pouvez exprimer par déclaration comment WPF doit afficher des données hiérarchiques via des modèles de données hiérarchiques. La classe HierarchicalDataTemplate est un outil qui comble le fossé entre une structure de données complexes et une représentation visuelle de ces données. Elle ressemble beaucoup à un DataTemplate normal, mais elle permet de spécifier d'ou proviennent des éléments enfants d'un objet de données. Vous pouvez également fournir au HierarchicalDataTemplate un modèle avec lequel afficher ces éléments enfants.
Supposons que vous souhaitiez afficher les données présentées à la figure 7 dans un contrôle TreeView. Le TreeView ressemblerait à peu près à la figure 9. Implémenter ceci implique l'utilisation de deux HierarchicalDataTemplates et d'un DataTemplate.
Figure 9 Affichage d'une hiérarchie de données entière dans un TreeView
Les deux modèles hiérarchiques affichent les objets Customer et Order. Puisque les objets OrderDetail n'ont pas d'élément enfant, vous pouvez les afficher avec un DataTemplate non hiérarchique. La propriété ItemTemplate de TreeView utilise le modèle pour des objets du type Customer puisque Customers sont les objets de données contenus au niveau racine dans le TreeView. Le XAML de la figure 10 indique comment toutes les pièces de ce puzzle s'imbriquent.

Figure 10 XAML Au-delà de l'affichage TreeView
<Grid>
<Grid.DataContext>
<!--
This sets the DataContext of the UI
to a Customers returned by calling
the static CreateCustomers method.
-->
<ObjectDataProvider
xmlns:local="clr-namespace:VariousBindingExamples"
ObjectType="{x:Type local:Customer}"
MethodName="CreateCustomers"
/>
</Grid.DataContext>
<Grid.Resources>
<!-- ORDER DETAIL TEMPLATE -->
<DataTemplate x:Key="OrderDetailTemplate">
<TextBlock>
<Run>Product:</Run>
<TextBlock Text="{Binding Path=Product}" />
<Run>(</Run>
<TextBlock Text="{Binding Path=Quantity}" />
<Run>)</Run>
</TextBlock>
</DataTemplate>
<!-- ORDER TEMPLATE -->
<HierarchicalDataTemplate
x:Key="OrderTemplate"
ItemsSource="{Binding Path=OrderDetails}"
ItemTemplate="{StaticResource OrderDetailTemplate}"
>
<TextBlock Text="{Binding Path=Desc}" />
</HierarchicalDataTemplate>
<!-- CUSTOMER TEMPLATE -->
<HierarchicalDataTemplate
x:Key="CustomerTemplate"
ItemsSource="{Binding Path=Orders}"
ItemTemplate="{StaticResource OrderTemplate}"
>
<TextBlock Text="{Binding Path=Name}" />
</HierarchicalDataTemplate>
</Grid.Resources>
<TreeView
ItemsSource="{Binding Path=.}"
ItemTemplate="{StaticResource CustomerTemplate}"
/>
</Grid>
J'attribue une collection d'objets Customer au DataContext d'une grille qui contient le TreeView. Dans XAML, ceci est possible grâce à l'ObjectDataProvider, qui est une façon pratique d'appeler une méthode depuis XAML. Puisque le DataContext est hérité dans l'arborescence d'élément, le DataContext du ViewTree renvoie à cet ensemble d'objets Customer. C'est pourquoi nous pouvons attribuer à sa propriété ItemsSource une liaison de « {Binding Path=.}”, qui est une façon d'indiquer que la propriété ItemsSource est liée au DataContext du TreeView.
Si vous n'avez pas attribué la propriété ItemTemplate de TreeView, le TreeView affiche seulement les objets Customer de niveau supérieur. Puisque WPF ne sait pas du tout comment afficher un Customer, il appelle ToString sur chaque Client et affiche ce texte pour chaque élément. Il ne peut pas imaginer que chaque Customer est associé à une liste d'objets Orders et que chaque Order est associé à une liste d'objets OrderDetail. Puisque le WPF ne peut pas comprendre votre schéma de données comme par magie, vous devez lui expliquer le schéma pour qu'il puisse afficher la structure de données correctement.
HierarchicalDataTemplates entre en jeu précisément lorsqu'il s'agit d'expliquer la structure et l'apparence de vos données à WPF. Les modèles utilisés dans cette démonstration contiennent des arborescences d'éléments visuels très simples, le plus souvent juste quelques TextBlocks contenant des petits textes. Dans une application plus compliquée, les modèles pourraient présenter des modèles 3D interactifs, des images, des graphiques vectoriels, des UserControls complexes ou tout autre contenu WPF pour visualiser l'objet de données sous-jacent.
Il est important de remarquer l'ordre dans lequel les modèles sont déclarés. Vous devez déclarer un modèle avant de le référencer via l'extension StaticResource. Ceci est une exigence imposée par le lecteur XAML et elle s'applique à toutes les ressources, pas seulement aux modèles.
Il est possible de référencer les modèles en utilisant l'extension DynamicResource. Dans ce cas, l'ordre lexical des déclarations de modèle n'a aucune importance. Cependant, l'utilisation d'une référence DynamicResource au lieu d'une référence StaticResource entraîne une certaine surcharge d'exécution parce que les modifications du système de ressources sont contrôlées. Puisque nous ne remplaçons pas les modèles au moment de l'exécution, cette surcharge est inutile. Il est donc plus judicieux d'utiliser des références StaticResource et de mettre les déclarations de modèle dans le bon ordre.
Utilisation d'une entrée utilisateur
Pour la plupart des programmes, l'affichage des données n'est que le début. Les autres grands défis sont l'analyse, l'acceptation et le rejet des données entrées par l'utilisateur. Dans un monde idéal, où tous utilisateurs n'entreraient que des données logiques et précises, ceci serait une tâche simple. Dans la réalité, ce n'est pas le cas. Les vrais utilisateurs font des fautes de frappe, oublient d'entrer des valeurs obligatoires, entrent des valeurs au mauvais endroit, suppriment des enregistrements qu'il ne faut pas supprimer, ajoutent des enregistrements qu'il ne faut pas ajouter et observent, en général, la loi de Murphy à chaque fois que cela est humainement possible.
Notre travail, en tant que développeurs et architectes, consiste à combattre les entrées erronées et malveillantes qui seront inévitablement introduites par nos utilisateurs. L'infrastructure de liaison WPF prend en charge la validation des entrées. Dans les sections suivantes de cet article, j'examinerai comment utiliser la prise en charge de la validation WPF et comment afficher des messages d'erreur de validation pour l'utilisateur.
Validation des entrées par les règles de validation
La première version de WPF, qui faisait partie de Microsoft® .NET Framework 3.0, avait seulement limité la prise en charge de la validation des entrées. La classe Binding dispose d'une propriété ValidationRules, qui peut enregistrer un nombre quelconque de classes dérivées de ValidationRule. Chacune de ces règles peut contenir une certaine logique qui teste si la valeur liée est valide.
À l'époque, WPF n'était fourni qu'avec une sous-classe ValidationRule, appelée ExceptionValidationRule. Les développeurs pourraient ajouter cette règle à un ValidationRules de liaison. Ceci permettrait d'identifier des exceptions générées pendant la mise à jour de la source de données et d'autoriser l'interface utilisateur à afficher le message d'erreur de l'exception. L'utilité de cette approche à la validation des entrées est discutable si l'on considère qu'il est essentiel, pour créer un bon environnement de travail, de révéler le moins de détails techniques qui ne sont pas utiles à l'utilisateur. Les messages d'erreur dans les exceptions d'analyse des données sont généralement trop techniques pour la plupart des utilisateurs. Mais je m'égare.
Supposons que vous ayez une classe qui représente une ère, telle que la classe simple Era illustrée ici :
public class Era
{
public DateTime StartDate { get; set; }
public TimeSpan Duration { get; set; }
}
Si vous voulez autoriser l'utilisateur à éditer la date de début et la durée d'une ère, vous pouvez utiliser deux contrôles de zone de texte et lier leurs propriétés Text aux propriétés d'une instance Era. Puisque l'utilisateur peut entrer n'importe quel texte dans une zone de texte, vous ne pouvez pas avoir la certitude que le texte entré pourra être converti en une instance DateTime ou TimeSpan. Dans ce scénario, vous pouvez utiliser ExceptionValidationRule pour signaler des erreurs de conversion des données et puis afficher ces erreurs de conversion dans l'interface utilisateur. Le XAML de la figure 11 explique comment réaliser cette tâche.

Figure 11 Une classe simple qui représente une ère
<!-- START DATE -->
<TextBlock Grid.Row="0">Start Date:</TextBlock>
<TextBox Grid.Row="1">
<TextBox.Text>
<Binding Path="StartDate" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<ExceptionValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<!-- DURATION -->
<TextBlock Grid.Row="2">Duration:</TextBlock>
<TextBox
Grid.Row="3"
Text="{Binding
Path=Duration,
UpdateSourceTrigger=PropertyChanged,
ValidatesOnExceptions=True}"
/>
Ces deux zones de texte démontrent les deux façons d'ajouter une ExceptionValidationRule aux ValidationRules d'une liaison dans XAML. La zone de texte Start Date utilise une syntaxe verbeuse de type élément de propriété pour ajouter explicitement la règle. La zone de texte Duration utilise une syntaxe sténographique en définissant simplement la propriété ValidatesOnExceptions de la liaison sur true. La propriété UpdateSourceTrigger des deux liaisons est définie sur PropertyChanged de sorte que l'entrée est validée à chaque fois qu'une nouvelle valeur est attribuée à la propriété Text de la zone de texte au lieu d'attendre que le contrôle perde le focus. Une capture d'écran est présentée à la figure 12.
Figure 12 ExceptionValidationRule affiche des erreurs de validation
Affichage des erreurs de validation
Comme nous l'avons vu à la figure 13, la zone de texte Duration contient une valeur non valide. La chaîne qu'elle contient ne peut pas être convertie en une instance TimeSpan. L'info-bulle de la zone de texte affiche un message d'erreur et une petite icône d'erreur rouge s'affiche à droite du contrôle. Ce comportement n'est pas automatique, mais son implémentation et personnalisation sont faciles.

Figure 13 Affiche des erreurs de validation des entrées pour l'utilisateur
<!--
The template which renders a TextBox
when it contains invalid data.
-->
<ControlTemplate x:Key="TextBoxErrorTemplate">
<DockPanel>
<Ellipse
DockPanel.Dock="Right"
Margin="2,0"
ToolTip="Contains invalid data"
Width="10" Height="10"
>
<Ellipse.Fill>
<LinearGradientBrush>
<GradientStop Color="#11FF1111" Offset="0" />
<GradientStop Color="#FFFF0000" Offset="1" />
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
<!--
This placeholder occupies where the TextBox will appear.
-->
<AdornedElementPlaceholder />
</DockPanel>
</ControlTemplate>
<!--
The Style applied to both TextBox controls in the UI.
-->
<Style TargetType="TextBox">
<Setter Property="Margin" Value="4,4,10,4" />
<Setter
Property="Validation.ErrorTemplate"
Value="{StaticResource TextBoxErrorTemplate}"
/>
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="ToolTip">
<Setter.Value>
<Binding
Path="(Validation.Errors)[0].ErrorContent"
RelativeSource="{x:Static RelativeSource.Self}"
/>
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
La classe statique Validation établit une relation entre un contrôle et toute erreur de validation qu'il contient, en utilisant certaines propriétés et méthodes statiques associées. Vous pouvez référencer ces propriétés associées dans XAML pour créer des descriptions marquage uniquement pour indiquer comment l'interface utilisateur doit présenter les erreurs de validation d'entrée à l'utilisateur. La responsabilité du XAML de la figure 13 est d'expliquer comment afficher des messages d'erreurs d'entrée pour les deux contrôles de zone de texte dans l'exemple précédent.
Le Style de la figure 13 cible toutes les instances d'une zone de texte dans l'interface utilisateur. Il applique trois paramètres à une zone de texte. Le premier Setter concerne la propriété Margin de la zone de texte. La propriété Margin est définie sur une valeur qui fournit assez d'espace pour afficher l'icône d'erreur du côté droit.
Le Setter suivant dans le Style attribue le ControlTemplate utilisé pour afficher la zone de texte lorsqu'il contient des données non valides. Il définit la propriété associée Validation.ErrorTemplate sur le ControlTemplate déclaré au-dessus de Style. Lorsque la classe Validation signale que la zone de texte comporte une ou plusieurs erreurs de validation, la zone de texte affiche avec ce modèle. L'icône d'erreur rouge vient de là, comme illustré à la figure 12.
Le Style contient également un déclencheur qui contrôle la propriété Validation.HasError associée sur la zone de texte. Lorsque la classe Validation définit la propriété HasError associée sur true, le déclencheur de style s'active et attribue une info-bulle à la zone de texte. Le contenu de l'info-bulle est lié au message d'erreur de l'exception générée en tentant de traiter le texte de la zone de texte dans une instance du type de données de la propriété source.
Validation des entrées par IDataErrorInfo
Avec Microsoft .NET Framework 3.5, la prise en charge WPF de la validation des entrées a été considérablement améliorée. L'approche ValidationRule est utile pour des applications simples, mais, dans la réalité, les applications traitent la complexité de données réelles et de règles métier. L'encodage des règles métier dans des objets ValidationRule non seulement lie ce code à la plate-forme WPF, mais empêche à la logique métier d'exister à l'endroit auquel elle est destinée : dans les objets métier !
De nombreuses applications présentent une couche métier, où la complexité du traitement des règles métier est contenue dans un ensemble d'objets métier. Lorsque vous compilez sur Microsoft .NET Framework 3.5, vous pouvez utiliser l'interface IDataErrorInfo pour que WPF demande aux objets métier s'ils sont dans un état valide ou pas. Ceci élimine la nécessité de placer une logique métier dans des objets séparés de la couche métier et vous permet de créer des objets métier indépendants de la plate-forme interface utilisateur. Puisque l'interface IDataErrorInfo a été utilisée pendant de nombreuses années, il est très facile de réutiliser des objets métier depuis une application Windows Forms ou ASP.NET héritée.
Supposons que vous deviez fournir, pour une ère, une validation qui dépasse la simple vérification de convertibilité du texte entré par l'utilisateur en données de type propriété source. Il est logique que la date de début d'une ère ne peut pas se situer dans le futur puisque nous ne savons pas encore quelles ères existeront encore dans le futur. Il est peut-être logique aussi d'exiger qu'une ère dure au moins une milliseconde.
Ce type de règles est comparable à l'idée générique de logique métier du fait qu'elles sont toutes les deux des exemples de règles de domaine. Il convient d'implémenter des règles de domaine dans les objets qui contiennent leur état : objets domaine. Le code de la figure 14 illustre la classe SmartEra, qui expose des messages d'erreur de validation via l'interface IDataErrorInfo.

Figure 14 IDataErrorInfo expose des messages d'erreur de validation
public class SmartEra
: System.ComponentModel.IDataErrorInfo
{
public DateTime StartDate { get; set; }
public TimeSpan Duration { get; set; }
#region IDataErrorInfo Members
public string Error
{
get { return null; }
}
public string this[string property]
{
get
{
string msg = null;
switch (property)
{
case "StartDate":
if (DateTime.Now < this.StartDate)
msg = "Start date must be in the past.";
break;
case "Duration":
if (this.Duration.Ticks == 0)
msg = "An era must have a duration.";
break;
default:
throw new ArgumentException(
"Unrecognized property: " + property);
}
return msg;
}
}
#endregion // IDataErrorInfo Members
}
Utiliser la prise en charge de validation de la classe SmartEra à partir d'une interface utilisateur WPF est très simple. Tout ce que vous devez faire, c'est de dire aux liaisons qu'elles doivent respecter l'interface IDataErrorInfo sur l'objet auquel elles sont liées. Pour cela vous avez deux possibilités, comme illustré à la figure 15.

Figure 15 Utiliser la logique de validation
<!-- START DATE -->
<TextBlock Grid.Row="0">Start Date:</TextBlock>
<TextBox Grid.Row="1">
<TextBox.Text>
<Binding Path="StartDate" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<ExceptionValidationRule />
<DataErrorValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<!-- DURATION -->
<TextBlock Grid.Row="2">Duration:</TextBlock>
<TextBox
Grid.Row="3"
Text="{Binding
Path=Duration,
UpdateSourceTrigger=PropertyChanged,
ValidatesOnDataErrors=True,
ValidatesOnExceptions=True}"
/>
Comme vous pouvez ajouter l’ExceptionValidationRule explicitement ou implicitement à une collection de ValidationRules de liaison, vous pouvez ajouter la DataErrorValidationRule directement aux ValidationRules d'une liaison ou vous pouvez simplement définir juste la propriété ValidatesOnDataErrors sur true. Les deux approches ont pour résultat le même effet net : le système de liaison recherche des erreurs de validation dans l'interface IDataErrorInfo de la source des données.
Conclusion
Si de nombreux développeurs affirment que leur fonctionnalité préférée, dans WPF, est la prise en charge des données de liaison, c'est qu'il y a une bonne raison. L'utilisation de liaison dans WPF est si puissante et répandue qu'elle pousse de nombreux développeurs de logiciels à revoir leur façon de considérer la relation entre les données et les interfaces utilisateur. De nombreuses fonctionnalités fondamentales de WPF coopèrent pour prendre en charge des scénarios liés aux donnés complexes, tels que des modèles, des styles et des propriétés associées.
Avec relativement peu de lignes de XAML, vous pouvez exprimer comment vous souhaitez afficher une structure de données hiérarchique et valider les entrées utilisateur. Dans des situations avancées, vous pouvez exploiter toute la puissance du système de liaison en y accédant par programmation. Avec une infrastructure de cette puissance, la création d'expériences utilisateur extraordinaires et de visualisations de données remarquables, éternel objectif des développeurs qui créent les applications métier modernes, sont enfin à portée de main.