0. Traduction

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

I. Introduction

Cet article est le quatrième d'une série d'introduction à Windows Presentation Foundation. Dans l'article précédent, nous avons étudié la liaison de données et la façon dont elle est utilisée pour afficher des informations dans les interfaces utilisateur en WPF. Dans cet article, nous examinerons les modèles de données et déclencheurs (data templates and triggers), et la façon dont ils sont utilisés dans l'application de démonstration WPF Horse Race (disponible en téléchargement dans le premier article de la série).

Tout comme les autres articles de la série, cet article ne couvre son sujet de manière exhaustive. Au lieu de cela nous examinerons suffisamment les bases pour que nous puissions voir comment ces fonctionnalités sont mises à profit dans l'application de démonstration. Si vous voulez en savoir plus sur les façons dont les modèles de données et déclencheurs peuvent être utilisés, reportez-vous à la section Liens externes pour plus d'informations.

II. Contexte

Jusqu'ici dans cette série d'articles, nous avons vu comment le XAML, les panneaux de disposition et la liaison de données sont utilisés pour créer des interfaces utilisateur qui affichent des données simples. Cependant, ces blocs de construction fondamentaux servent de fondement sur lequel des fonctionnalités plus puissantes et attrayantes de WPF dépendent.

L'une de ces fonctionnalités de haut niveau vous permet de décrire facilement comment un objet de n'importe quel type peut être rendu. En d'autres termes, il vous permet de créer, en XAML, un modèle des éléments visuels qui peuvent être « élargis » à l'exécution pour rendre un objet de données. Bien sûr, la fonctionnalité à laquelle je fais référence est connue comme « data templating ».

Les modèles de données sont un seul type de modèle dans WPF. Cet article, en fait, toute cette série d'articles, n'examine pas les autres types de modèles, car l'application de démonstration WPF Horse Race ne les utilise pas. Au cas où vous seriez intéressé, il y a aussi les modèles de contrôle, les modèles de panneaux d'éléments et les modèles de données hiérarchiques. Reportez-vous à la sous-section Autres modèlesde la section Liens externes vers le bas de cet article pour des liens vers des informations à leur sujet.

Une autre fonctionnalité de WPF traitée dans cet article est appelée « déclencheurs ». Les déclencheurs sont un autre élément fondamental de WPF, sur lequel de nombreuses parties du framework dépendent. Ils sont, en général, un moyen d'appliquer conditionnellement des valeurs aux propriétés. Les déclencheurs sont particulièrement utiles lorsque vous écrivez du XAML car ils fournissent un moyen d'évaluer les propriétés à l'exécution et prendre certaines mesures en fonction de leurs valeurs. En ce sens, les déclencheurs sont une zone grise, quelque part entre le monde déclaratif du balisage XAML et le monde impératif du code-behind.

III. La classe DataTemplate

Toutes les fonctionnalités de création de modèles WPF sont basées sur la classe FrameworkTemplate. DataTemplate dérive de FrameworkTemplate, comme le font toutes les autres classes utilisées à des fins de création de modèles. Ce lien de parenté entre les classes de « templating » est largement hors de propos pour les scénarios courants de développement WPF, car dans la pratique, vous pourrez créer des modèles en XAML et ne pas les utiliser d'une manière polymorphe.

Il est beaucoup plus simple et rapide de créer un DataTemplate en XAML que d'en créer un par programmation. Le code pour créer des modèles est en fait très lourd, alors que le code XAML pour créer le même modèle est propre et simple. Il est techniquement possible de construire des modèles de données par programmation, mais même Microsoft ne le recommande pas.

IV. Sans un DataTemplate

Avant de nous plonger dans le modèle de données utilisé dans l'application WPF Horse Race, nous allons voir comment fonctionne un exemple simple. Jetons d'abord un coup d'oeil à une classe simple qui représente une rivière :

 
Sélectionnez
namespace WPF_Test
{
 public class River
 {
  string name;
  int milesLong;
  public string Name
  {
   get { return name; }
   set { name = value; }
  }
  public int MilesLong
  {
   get { return milesLong; }
   set { milesLong = value; }
  }
 }
}

Si nous devions afficher une instance de cette classe dans une ContentControl, le XAML serait quelque chose comme ceci :

 
Sélectionnez
<StackPanel>
  <StackPanel.Resources>
    <local:River x:Key="theRiver" Name="Colorado River" MilesLong="1450" />
  </StackPanel.Resources>
  <ContentControl Content="{StaticResource theRiver }" />
</StackPanel>

L'interface utilisateur créée par ce XAML se présente comme suit :

image

Ce n'est certainement pas trop impressionnant. Ce que vous voyez ci-dessus est ce qui est affiché en tant que résultat de l'appel ToString () sur l'objet River référencé par le ContentControl. Prenons un moment pour examiner ce qui se passe ici.

L'exemple ci-dessus crée une instance de la classe River, qui représente la rivière Colorado (Colorado River). Il crée également un ContentControl qui est instruit pour afficher cet objet River d'une quelconque sorte. Le ContentControl examine l'objet River et tente de comprendre comment le rendre, mais comme River ne dérive pas de UIElement il n'a aucun moyen de savoir comment le faire. Une fois que ContentControl est dépourvu d'options, il finit par appeler ToString () sur l'objet River, puis affiche ce texte.

V. Avec un DataTemplate

Maintenant que nous avons vu à quel point un objet River apparaît ennuyeux en l'absence de modèles de données, il est temps d'ajouter un modèle de données dans le mélange. Voici le même code XAML utilisé avant, sauf que cette fois il y a un modèle pour la classe River:

 
Sélectionnez
<StackPanel>
  <StackPanel.Resources>
    <DataTemplate DataType="{x:Type local:River}">
      <Border BorderBrush="Blue" BorderThickness="3" CornerRadius="12">
        <Grid Margin="4">
          <TextBlock>
            <Run Text="The"/>
            <TextBlock Text="{Binding Name}"/>
            <Run Text="is"/>
            <TextBlock Text="{Binding MilesLong}" />
            <Run Text="miles long." />
          </TextBlock>
        </Grid>
      </Border>
    </DataTemplate>

    <local:River x:Key="theRiver" Name="Colorado River" MilesLong="1450" />
  </StackPanel.Resources>

  <ContentControl Content="{StaticResource theRiver}" />
</StackPanel>


C'est ainsi que rend ce XAML :

image

L'objet River est affiché beaucoup plus intelligemment quand un modèle de données est appliqué. Les informations qui y sont détenues sont maintenant affichées dans le cadre d'une phrase, et cette phrase est enveloppée dans une bordure en courbe bleue. Gardez à l'esprit que la restitution d'un objet River comme vu au-dessus est complètement arbitraire. Il peut être affiché par tous les moyens considérés comme approprié pour l'application dans laquelle il existe.

VI. Les déclencheurs (triggers)

Une autre caractéristique de WPF souvent utilisée en conjonction avec les modèles s'appelle les «déclencheurs». En général, un déclencheur est un peu comme un bloc if dans du code procédural ; il exécute ce qu'il contient lorsqu'une certaine condition s'évalue à true.

Par exemple, voici une version modifiée du code XAML vu dans la section précédente. Cette fois, le DataTemplate a un Trigger en lui qui définit la propriété Background du Border à 'LightGray' et la propriété Foreground du TextBlock à 'Red' lorsque le curseur de la souris est n'importe où sur le Border.

 
Sélectionnez
<StackPanel>
  <StackPanel.Resources>
    <DataTemplate DataType="{x:Type local:River}">
      <Border x:Name="bdr"
        BorderBrush="Blue" BorderThickness="3" CornerRadius="12"
        >
        <Grid Margin="4">
          <TextBlock x:Name="txt">
            <Run Text="The"/>
            <TextBlock Text="{Binding Name}"/>
            <Run Text="is"/>
            <TextBlock Text="{Binding MilesLong}" />
            <Run Text="miles long." />
          </TextBlock>
        </Grid>
      </Border>

      <DataTemplate.Triggers>
        <Trigger SourceName="bdr" Property="IsMouseOver" Value="True">
         <Setter TargetName="bdr" Property="Background" Value="LightGray"/>
         <Setter TargetName="txt" Property="Foreground" Value="Red"/>
        </Trigger>
      </DataTemplate.Triggers>
    </DataTemplate>

    <local:River x:Key="theRiver" Name="Colorado River" MilesLong="1450" />
  </StackPanel.Resources>

  <ContentControl Content="{StaticResource theRiver}" />
</StackPanel>

Lorsque que ce code XAML est utilisé, l'interface utilisateur ressemble à ceci lorsque le curseur est sur les visuels de River:

image

Lorsque le curseur est déplacé loin des visuels de River, les couleurs de fond et de premier plan reviennent automatiquement à leurs valeurs précédentes. Les déclencheurs gèrent l'ensemble de cette fonctionnalité pour nous, comme on le voit ci-dessous :

image

Il existe plusieurs types de déclencheurs, chacun a une façon différente de déterminer quand il doit s'exécuter. Dans le reste de cet article, nous allons nous concentrer uniquement sur les deux types de déclencheurs utilisés dans l'application de démo WPF Horse Race : DataTrigger et MultiDataTrigger.

Les déclencheurs peuvent être utilisés dans d'autres endroits en dehors des modèles, comme dans les Styles et sur toute la sous-classe FrameworkElement, mais cet article ne les montre qu'étant utilisés dans un DataTemplate. Pour plus d'informations sur les déclencheurs, consultez la sous-section Déclencheurs de la section Liens externes vers le bas de cet article.

VII. Comment le WPF Horse Race utilise les modèles de données (Data Templates) et les déclencheurs (Triggers)

L'application de démo WPF Horse Race a un DataTemplate personnalisé, qui est utilisé pour rendre les instances de la classe RaceHorse. La déclaration du modèle peut être trouvée dans le fichier RaceHorseDataTemplate.xaml. Le XAML du modèle complet n'est pas montré dans cet article parce qu'il est assez long, et cela ne servirait à rien de le diffuser dans son intégralité. Au lieu de cela, nous l'examinerons au coup par coup.

VII-A. Les visuels pour un RaceHorse

Les éléments visuels dans le modèle de données RaceHorse peuvent être ramenés à cette structure de base :

 
Sélectionnez
<Border>
  <Grid>
    <StackPanel Orientation="Horizontal">
      <!-- This Rectangle is the "progress indicator"
           which follows the horse. -->
      <Rectangle />
      <!--This Image displays the picture of a race horse. -->
      <Image />
    </StackPanel>
    <!-- Displays the horse's name. -->
    <TextBlock />
  </Grid>
</Border>

Voici une explication visuelle sur la façon dont les éléments dans le modèle correspondent à ce que vous voyez comme un RaceHorse parcours une course :

image

(Note : Le rectangle jaune dans l'image ci-dessus, qui représente le StackPanel, a été ajouté pour une simple raison de clarté, il n'apparaît pas à l'écran lorsque l'application est en train de tourner.)

L'élément racine Border définit les limites du « race pit » (la zone dans laquelle il va courir) du RaceHorse. Le Border contient un panneau Grid, qui à son tour, contient un StackPanel et un TextBlock. Le StackPanel contient un indicateur de progression du RaceHorse et une photo d'un cheval. Un StackPanel a été utilisé pour contenir ces deux éléments afin que lorsque le Rectangle (c.-à-d. l'indicateur de progression) devient plus large, l'Image sera déplacée vers la droite avec lui.

Le TextBlock affiche le nom du RaceHorse. Comme le TextBlock est déclaré sous le StackPanel (lexicalement), il sera rendu au-dessus du StackPanel, à l'égard de l'ordre z. Cela veille à ce que le nom du cheval soit toujours visible.

VII-B. Définition des propriétés sur le vainqueur d'une course

Après que les visuels du modèle sont déclarés, il y a aussi quelques déclarations de déclencheurs. L'un d'eux est un DataTrigger. Un DataTrigger exécute son contenu lorsque le Binding fourni renvoi une valeur spécifique. Voici le code XAML pour ce déclencheur :

 
Sélectionnez
<!-- Set special values for the winner of the race. -->
<DataTrigger Binding="{Binding IsWinner}" Value="True">
    <Setter TargetName="progressIndicator"
            Property="Fill" Value="{StaticResource WinnerBrush}" />

    <Setter TargetName="horseName"
            Property="Foreground" Value="Black" />

    <Setter TargetName="horseName"
            Property="FontWeight" Value="Bold" />

    <Setter TargetName="horseName"
            Property="HorizontalAlignment" Value="Center" />
</DataTrigger>

Ce DataTrigger attend que la propriété IsWinner du RaceHorse modélisé passe de false à true. Lorsque cela se produit (c'est-à-dire que RaceHorse gagne une course) tous les Setters qu'il contient sont exécutés.

La classe Setter fournit un moyen d'attribuer une valeur à une propriété. Elle est particulièrement pratique lorsqu'elle est utilisée en XAML, mais peut être également utilisée dans le code-behind. Quand un RaceHorse gagne une course, les Setters modifient les visuels du modèle, de sorte que l'indicateur de progression est rendu avec un coup de pinceau doré et son nom se distingue visuellement.

Dans la capture d'écran ci-dessous, le RaceHorse appelé «Fresh Spice» a remporté la course :

image

Lorsque démarre la prochaine course et que la propriété IsWinner de Fresh Spice est remise à false, les propriétés affectées par ce déclencheur reviendront automatiquement à leurs valeurs précédentes.

Au cas où vous seriez curieux de savoir comment le DataTrigger sait quand la valeur de la propriété IsWinner change, la réponse réside dans le fait que RaceHorse implémente INotifyPropertyChanged. L'évènement PropertyChanged d'un objet RaceHorse est déclenché quand la valeur de sa propriété IsWinner change. La classe Binding est consciente de l'interface INotifyPropertyChanged et est informée quand l'événement PropertyChanged est déclenché pour la propriété IsWinner.

VII-C. Estomper les RaceHorses perdants

Quand un RaceHorse termine une course, mais ne termine pas premier, il devient rouge et puis s'estompe. Par « s'estomper », je veux dire que la propriété Opacity de l'élément Border du RaceHorse est animée à 60% sur trois quarts de seconde. Cet article ne couvre pas le support de WPF pour l'animation, mais il est important de savoir que les animations peuvent être démarrées en réponse à un déclencheur étant exécuté.

Pour changer la couleur et estomper un RaceHorse qui a perdu une course, un déclencheur doit connaître deux informations : si le RaceHorse a déjà terminé la course, et s'il a perdu la course. Ce type d'expression « AND » peut être implémenté avec un MultiDataTrigger, comme on le voit ci-dessous :

 
Sélectionnez
<!-- This MultiDataTrigger affects losers of the race. -->
<MultiDataTrigger>
  <MultiDataTrigger.Conditions>
    <Condition Binding="{Binding IsFinished}" Value="True" />
    <Condition Binding="{Binding IsWinner}" Value="False" />
  </MultiDataTrigger.Conditions>

  <!-- Apply the "finished the race" brush to
       the horse's progress indicator. -->
  <Setter TargetName="progressIndicator"
          Property="Fill" Value="{StaticResource FinishedBrush}" />

  <!-- Fade the race pit in and out if the horse lost the race. -->
  <MultiDataTrigger.EnterActions>
    <!-- Fade away the RaceHorse's Border element when it loses a race. -->
  </MultiDataTrigger.EnterActions>

  <MultiDataTrigger.ExitActions>
    <!-- Fade in the RaceHorse's Border element when a new race starts. -->
  </MultiDataTrigger.ExitActions>
</MultiDataTrigger>

VIII. Liens externes

Remerciements

Je tiens ici à remercier Josh Smith pour son aimable autorisation de traduire l'article, ainsi que Philippe Vialatte, tomlev et Djug pour leurs précieux conseils.
Je remercie également eusebe19 pour sa relecture.