Traduction

Cet article est la traduction la plus fidèle possible de l'article original de Josh Smith, A Guided Tour of WPF - Part 3 (Data binding).

I. Introduction

Cet article est le troisième d'une série d'introduction à Windows Presentation Foundation. Dans l'article précédent, nous avons étudié les panneaux de disposition et la façon dont ils sont utilisés pour créer des interfaces utilisateur en WPF. Dans cet article, nous explorerons le monde de la liaison de données (data binding) et la manière dont elle est utilisée dans l'application de démonstration WPF Horse Race (disponible en téléchargement dans le premier article de la série).

Pour une revue exhaustive de la liaison de données en WPF, n'oubliez pas de consulter les liens listés dans la section Liens externes en bas de la page. Cet article couvre juste les fondamentaux de la liaison de données en WPF et démontre diverses façons de l'utiliser mises en œuvre dans l'application WPF Horse Race.

II. Contexte

La notion de liaison de données dans la couche d'interface utilisateur n'est pas quelque chose de nouveau. Cela existe depuis assez longtemps, sur différentes plateformes d'IHM, aussi bien pour des applications de bureau que pour des applications Web. L'idée de base est de « lier » les éléments visuels (contrôles) d'une interface aux objets de données qu'ils doivent afficher. L'infrastructure de liaison s'occupe alors de gérer les interactions de données, de façon à ce que les modifications sur les contrôles de l'IHM soient reflétées sur les objets de données, et vice versa. L'avantage majeur de l'utilisation de la liaison de données est que cela réduit la quantité de code que le développeur doit écrire.

Les architectes de certaines plateformes d'IHM plus anciennes ont fait du bon travail en intégrant la liaison de données avec le reste de leur framework. Les architectes de WPF ont fait un travail extraordinaire en intégrant la liaison de données avec tous les aspects de leur framework. La liaison de données en WPF est omniprésente et parfaitement intégrée. Ce système est si puissant et flexible qu'il vous fait littéralement changer la façon dont vous envisagez la conception et le développement d'interfaces utilisateur.

Avec une seule API très simple, vous pouvez lier des objets métier, des données XML, des éléments visuels, des conteneurs de données ADO.NET et à peu près tout ce que vous pouvez imaginer. Vous pouvez utiliser des convertisseurs de valeur pour exécuter des manipulations arbitraires sur les données quand les valeurs liées sont transférées dans un sens ou dans l'autre. Vous pouvez effectuer la validation des données en créant des règles de validation personnalisées et en les appliquant à un binding. La liste est encore longue. La liaison de données en WPF est vraiment un énorme pas en avant.

III. Propriétés de dépendance

Avant de plonger dans les entrailles de la liaison de données en WPF, il est nécessaire de faire un détour pour parler brièvement d'une autre fonctionnalité fondamentale de WPF qui la rend possible. En WPF, une propriété ne peut être liée que si elle est une propriété de dépendance (dependency property).

Les propriétés de dépendance sont comme des propriétés .NET normales, mais dopées aux stéroïdes. Il y a toute une infrastructure de support des propriétés de dépendance, qui fournit toute une gamme de fonctionnalités pour faciliter la vie des développeurs. Dans le cadre de cet article, il est important de savoir les choses suivantes à propos des propriétés de dépendance :

  • elles peuvent déterminer leur valeur en la récupérant d'un objet Binding (c'est-à-dire qu'elles peuvent être liées) ;
  • elles peuvent participer à l'héritage de valeur de propriété, ce qui signifie que si une propriété de dépendance d'un élément n'a pas de valeur, elle utilisera la valeur définie sur un élément parent (dans l'arbre logique). C'est un peu similaire aux propriétés ambiantes dans le monde Windows Forms ;
  • elles peuvent être définies en XAML, comme des propriétés normales.

Les raisons pour lesquelles ces caractéristiques sont importantes deviendront bientôt plus claires, au fur et à mesure que nous examinerons comment la liaison de données en tire parti. Pour plus d'informations sur les propriétés de dépendance, consultez la section Liens Externes en bas de la page.

IV. DataContext

Les éléments d'interface graphique en WPF ont une propriété de dépendance DataContext (contexte de données). Cette propriété utilise l'héritage de valeur mentionné plus haut, donc si vous affectez un objet Foo au DataContext d'un élément, la propriété DataContext de tous les descendants logiques de cet élément référencera également cet objet Foo. Cela signifie que toutes les liaisons de données contenues dans l'arborescence de cet élément racine se feront par rapport à l'objet Foo, à moins que l'on ne spécifie explicitement de faire la liaison sur autre chose. Cette fonctionnalité est extrêmement utile, comme nous allons bientôt le voir.

V. La classe Binding

Tout le mécanisme de liaison de données en WPF s'articule autour de la classe Binding. Cette classe est le soleil du système solaire qu'est la liaison de données, en quelque sorte.

La classe Binding a une API assez simple et intuitive, mais elle est très puissante. L'application de démonstration WPF Horse Race est loin d'utiliser toutes les fonctionnalités de la classe Binding, mais elle utilise certaines des plus courantes. Jetons un œil aux membres de Binding que nous verrons en action plus loin dans l'article :

  • Source : fait référence à la source de la liaison de données. Par défaut, la source est le DataContext de l'élément, qui peut être hérité d'un élément parent, comme expliqué dans la section précédente. Si vous affectez une valeur non nulle à cette propriété, alors l'opération de liaison de données traitera cette valeur comme l'emplacement où les données sont récupérées et mises à jour ;
  • Path : utilisé pour indiquer quelle propriété de l'objet source est utilisée pour récupérer et mettre à jour la valeur liée. Cette propriété est de type PropertyPath, ce qui lui permet de supporter toute une palette d'expressions complexes ;
  • ElementName : peut être utilisé comme alternative à la propriété Source décrite précédemment. Cette propriété permet de spécifier le nom d'un élément à utiliser comme source de données. Cela peut être utile pour lier une propriété d'un élément à une propriété d'un autre élément, en particulier quand la liaison est déclarée en XAML ;
  • Converter : de type IValueConverter. Vous pouvez affecter à cette propriété une instance d'une classe qui implémente cette interface pour intercepter tous les transferts de données de la source vers la cible de la liaison, ou le contraire. Les convertisseurs de valeurs sont très pratiques et utilisés assez souvent.

VI. Comment WPF Horse Race utilise la liaison de données

Maintenant que nous savons à peu près comment la liaison de données est structurée, il est temps de voir comment l'application de démonstration WPF Horse Race l'utilise. Nous n'examinerons pas chaque cas d'utilisation de la liaison de données dans l'application ; certains seront mieux traités ultérieurement dans un autre article de la série. Toute la logique de liaison de données dans l'application est exprimée en XAML, mais gardez à l'esprit qu'il est aussi tout à fait possible de faire la même chose dans le code-behind.

VI-A. Affichage du nom d'un cheval

Ouvrez le fichier RaceHorseDataTemplate.xaml, qui contient le DataTemplate pour la classe RaceHorse (ne vous inquiétez pas si vous ne savez pas encore ce qu'est un DataTemplate, le prochain article couvre ce sujet). Dans ce fichier se trouve un TextBlock nommé 'horseName' qui est lié à la propriété Name d'un objet RaceHorse. Voici une version abrégée du XAML en question :

 
Sélectionnez
<TextBlock x:Name="horseName" Text="{Binding Name}" />

Quand un cheval nommé 'Sweet Fate' est affiché dans l'IHM, ce TextBlock affiche son nom comme on peut le voir ci-dessous :

Displaying a horse's name

Remarquez que l'instruction de liaison dans le XAML ci-dessus ne mentionne pas explicitement que c'est la propriété Path du Binding qui est affectée. Les markup extensions, comme Binding, peuvent avoir une propriété « par défaut » quand on les utilise en XAML. La propriété par défaut de Binding est Path. Si vous préférez ne pas utiliser de notations implicites comme celle-ci, le XAML ci-dessous est équivalent à l'extrait précédent :

 
Sélectionnez
<TextBlock x:Name="horseName" Text="{Binding Path=Name}" />

VI-B. Rotation du champ de course

Dans le XAML de la fenêtre principale (Window1.xaml), il y a un contrôle Slider qui influe sur l'angle de rotation de l'ItemsControl qui sert de champ de course. L'ItemsControl a sa propriété LayoutTransform définie par une ressource RotateTransform, donc le Slider doit se lier à cette RotateTransform pour affecter la rotation de l'ItemsControl. Le résultat de cette liaison est que, quand l'utilisateur déplace le curseur du Slider, le champ de course tourne comme ceci :

Rotating the race track

Le XAML suivant correspond à la structure générale de la fenêtre principale, en ne montrant que les balises qui nous intéressent pour ce binding spécifique :

 
Sélectionnez
<Window>
  <Grid>
    <Grid.RowDefinitions>
      <!-- The top row is for the race track. -->
      <RowDefinition Height="*" />
      <!-- The bottom row is for the command strip. -->
      <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>

    <Grid.Resources>
      <RotateTransform x:Key="trans" Angle="0" />
    </Grid.Resources>

    <!-- The 'Race Track' area. -->
    <ItemsControl LayoutTransform="{StaticResource trans}" />
    <!-- The 'Command Strip' area -->
    <Border Grid.Row="1">
      <Slider Value="{Binding Source={StaticResource trans}, Path=Angle}" />
    </Border>
  </Grid>
</Window>

VI-C. Affichage de l'angle de rotation

Juste en dessous du Slider dans le XAML de la fenêtre principale se trouve un TextBlock dont la propriété Text est liée à la propriété Value du Slider. Le rôle de ce TextBlock est d'afficher l'angle de rotation appliqué au champ de course. Puisqu'on veut afficher cette valeur de façon conviviale, et que la valeur du Slider est un double, on utilise un convertisseur pour supprimer les décimales (en convertissant simplement le double en int). Voici comment ça fonctionne :

 
Sélectionnez
<StackPanel>
  <StackPanel.Resources>
    <local:DoubleToIntegerConverter x:Key="conv" />
  </StackPanel.Resources>
  ...
  <Slider x:Name="rotationSlider" />

  <TextBlock Text="{Binding
    ElementName=rotationSlider,
    Path=Value,
    Converter={StaticResource conv}}"
    />
  ...
</StackPanel>

Ce binding utilise la propriété ElementName pour faire référence à l'élément Slider. Il utilise aussi un convertisseur personnalisé pour convertir la propriété Value du Slider en nombre entier. Voici le code de ce convertisseur :

 
Sélectionnez
public class DoubleToIntegerConverter : IValueConverter
{
 public object Convert(
  object value, Type targetType,
  object parameter, CultureInfo culture )
 {
  return (int)(double)value;
 }

 public object ConvertBack(
  object value, Type targetType,
  object parameter, CultureInfo culture )
 {
  throw new NotSupportedException( "Cannot convert back" );
 }
}

VI-D. Calcul de la largeur de l'indicateur de progression d'un cheval

Au fur et à mesure qu'un cheval avance sur le champ de course, il est suivi par un indicateur de progression vert. La largeur de cet indicateur représente le pourcentage de la course que le cheval a déjà parcouru, comme on peut le voir ci-dessous :

The progress indicator in action

Pour déterminer la largeur de l'indicateur de progression, il faut deux informations : le pourcentage de la course déjà parcouru par le cheval et la distance totale que le cheval doit parcourir pour atteindre la ligne d'arrivée. D'après ce que nous avons vu de la liaison de données en WPF jusqu'ici, ça semble être un problème. Comment lier la largeur d'un élément à une valeur dont le calcul nécessite deux nombres ? C'est là que les types MultiBinding et IMultiValueConverter entrent en jeu.

Voilà à peu près comment ça fonctionne :

 
Sélectionnez
<Border x:Name="racePit">
  <Grid>
    <StackPanel>
      <StackPanel.Resources>
        <local:RaceHorseProgressIndicatorWidthConverter x:Key="WidthConv" />
      </StackPanel.Resources>
      <!-- This Rectangle "follows" a horse as it runs the race. -->
      <Rectangle x:Name="progressIndicator"
        Fill="{StaticResource RaceInProgressBrush}"
        >
        <!-- The progress indicator width is calculated by an instance
             of the RaceHorseProgressIndicatorWidthConverter class. -->
        <Rectangle.Width>
          <MultiBinding Converter="{StaticResource WidthConv}">
            <Binding Path="PercentComplete" />
            <Binding ElementName="racePit" Path="ActualWidth" />
          </MultiBinding>
        </Rectangle.Width>
      </Rectangle>
    </StackPanel>
  </Grid>
</Border>

Le multi-convertisseur utilisé pour calculer la largeur de l'indicateur de progression est le suivant :

 
Sélectionnez
public class RaceHorseProgressIndicatorWidthConverter : IMultiValueConverter
{
 public object Convert(
  object[] values, Type targetType,
  object parameter, CultureInfo culture )
 {
  int percentComplete = (int)values[0];
  double availableWidth = (double)values[1];
  return availableWidth * (percentComplete / 100.0);
 }
 public object[] ConvertBack(
  object value, Type[] targetTypes,
  object parameter, CultureInfo culture )
 {
  throw new NotSupportedException( "Cannot convert back" );
 }
}

VII. Liens externes

Remerciements

Je tiens ici à remercier Josh Smith pour son aimable autorisation de traduire l'article, ainsi que Karzoff pour la relecture orthographique.