Plan du site  
français  English
pixel
pixel

Articles - Étudiants SUPINFO

Introduction

Dans cette deuxième partie relative aux contrôles, nous allons découvrir les contrôles de liste, permettant de répéter un affichage autant de fois que nécessaire. On peut trouver ces contrôles pour afficher une liste d'éléments, par exemple des articles, des restaurants, des modèles, mais également pour afficher les différents catégories d'une navigation.

Ces contrôles justifieront au fur et à mesure de ce chapitre une liaison avec le code-behind, par exemple pour lier à ces contrôles une source de données externes. Nous verrons donc le concept de liaison entre données du code-behind et code XAML appelé "bindings", ainsi qu'un modèle de gestion de ces données pour faciliter la ré-utilisation et la séparation du code : le modèle MVVM.

Contrôles de Liste

ListView

Composé d'éléments ListViewItem, le contrôle ListView permet de rendre une liste d'éléments de manière verticale. Ce contrôle est utilisé pour rendre une liste indéterminée d'éléments rendus de manière identique.

<ListView>
  <ListViewItem>
    <TextBlock Text="Element A" />
  </ListViewItem>
  <ListViewItem>
    <TextBlock Text="Element B" />
  </ListViewItem>
</ListView>

Figure 1.1. Rendu d'un élément ListView

Rendu d'un élément ListView

Il est possible d'utiliser un modèle XAML de données pour une ListView. Ainsi, vous pouvez définir un modèle visuel appliqué à tous les éléments de la liste.

public MainPage()
{
  this.InitializeComponent();
  MonListView.ItemsSource = new string[] { 
    "Element A", "Element B", "Element C" 
  };
}
<ListView x:Name="MonListView">
  <ListView.ItemTemplate>
    <DataTemplate>
      <TextBlock Text="{Binding}" /> <!-- Expliqué au chapitre 3 -->
    </DataTemplate>
  </ListView.ItemTemplate>
</ListView>

Figure 1.2. Rendu d'un élément ListView avec DataTemplate

Rendu d'un élément ListView avec DataTemplate

Dans l'exemple précédent, nous utilisons une balise ListView.ItemTemplate pour définir le modèle partagé de chacun des éléments, défini dans la balise DataTemplate.

GridView

A l'inverse du contrôle ListView, le contrôle GridView permet le placement des éléments de manière horizontale. On utilise ce contrôle pour offrir une expérience d'exploration, et ainsi permettre une navigation au sein d'éléments bien plus visuels, tels qu'une galerie de photos ou une liste de catégories avec des images. Comme beaucoup d'éléments horizontaux, GridView place ses éléments horizontalement tant qu'il reste de la place, avant de créer une nouvelle ligne.

Figure 1.3. Exemple d'élément GridView

Exemple d'élément GridView

Tout comme les contrôles ListView possédant des éléments ListViewItem, le contrôle GridView possède des éléments GridViewItem.

public MainPage()
{
  this.InitializeComponent();
  MonGridView.ItemsSource = new string[] { 
    "Element A", "Element B", "Element C" 
  };
}
<Grid Background="White">
  <Grid.ColumnDefinitions>
    <!-- On utilise une colonne trop fine pour contenir
         les trois éléments présentés dans le code-behind -->
    <ColumnDefinition Width="200" />
    <ColumnDefinition Width="*" />
  </Grid.ColumnDefinitions>
  <GridView x:Name="MonGridView">
    <GridView.ItemTemplate>
      <DataTemplate>
        <TextBlock Text="{Binding}" />
      </DataTemplate>
    </GridView.ItemTemplate>
  </GridView>
</Grid>

Figure 1.4. Rendu d'un élément GridView

Rendu d'un élément GridView

Le modèle MVVM

Comme dans beaucoup de développements, les modèles de design (ou Design Patterns) sont des manières d'agencer le code afin de le rendre plus maintenable, plus lisible, plus simple, plus facile à évoluer. Ces modèles dépendent du langage ou de la plateforme utilisée. Ainsi, on utilisera plus facilement un modèle MVC (Model - View - Controller) en ASP.NEt, mais un modèle MVVM (Model - View - ViewModel) en UWP.

Ce modèle permet de séparer plus clairement la couche "business" (la logique principale de l'application) et la vue, ou séparer les fonctionnalités de l'application avec leur représentation visuelle. Ainsi, on rend le code plus maintenable, plus testable, plus facile à faire évoluer, et on obtient un code réutilisable à différents endroits de l'application, ou au sein d'un tout autre projet.

On séparera donc :

  • Les entités, ou Model, représentant nos structures de données (des articles, des personnes, ...)

  • L'interface utilisateur, ou View, le XAML représentant nos pages

  • La logique ou code "business", ou ViewModel, apportant des fonctionnalités et faisant le lien entre la couche Model et la couche View.

Le principe d'un ViewModel est multiple :

  • Comme dit précédemment, offrir des fonctionnalités. Notre objet ViewModel possèdera ainsi différentes fonctions pour récupérer ou agencer les données

  • Il offre également des méthodes permettant d'agir sur les données

  • Enfin, pour notifier les éléments de l'application utilisant ces données (majoritairement des contrôles), il offre des événements au changement de ses données.

Figure 1.5. Représentation du modèle MVVM

Représentation du modèle MVVM

Les différentes vues de l'application étant à l'écoute de la modification de ces données, elles peuvent se mettre à jour simplement et avec peu voire aucun code-behind spécifique. Comment lier les vues aux ViewModels ? Avec des liaisons, ou bindings !

Bindings

Les bindings permettent de lier efficacement une vue à un code-behind, et sont la base de tout modèle MVVM. Pour démontrer leur utilité, voici un exemple avec des modifications successives pour y ajouter des fonctionnalités. Commençons avec un état initial simple :

<Page> <!-- Attributs omis pour faciliter la lecture -->
  <StackPanel Orientation="Vertical">
    <TextBlock x:Name="TextBlockPrenom" Text="Jean" />
    <TextBlock x:Name="TextBlockNom" Text="Dupont" />
  </StackPanel>
</Page>

Dans cet exemple, on déclare deux éléments TextBlocks, "Prenom" et "Nom", avec chacun un texte. Gageons que ces valeurs viennent d'une source dynamique et non statique, et plaçons ces données dans le code-behind :

<Page> <!-- Attributs omis pour faciliter la lecture -->
  <StackPanel Orientation="Vertical">
    <TextBlock x:Name="TextBlockPrenom" />
    <TextBlock x:Name="TextBlockNom" />
  </StackPanel>
</Page>
public MainPage() {
  this.InitializeComponent();
  TextBlockPrenom.Text = "Jean";
  TextBlockNom.Text = "Dupont";
}

Ajoutons ensuite un bouton permettant de modifier ces valeurs au clic :

<Page> <!-- Attributs omis pour faciliter la lecture -->
  <StackPanel Orientation="Vertical">
    <TextBlock x:Name="TextBlockPrenom" />
    <TextBlock x:Name="TextBlockNom" />
    <Button Click="Button_Click">Changer les valeurs</Button>
  </StackPanel>
</Page>
public MainPage() {
  this.InitializeComponent();
  TextBlockPrenom.Text = "Jean";
  TextBlockNom.Text = "Dupont";
}

private void Button_Click(object s, RoutedEventArgs e) {
  TextBlockPrenom.Text = "David";
  TextBlockTwo.Text = "Durand";
}

On peut factoriser ce code pour améliorer sa lisibilité et fusionner les modifications de la page XAML à un seul endroit :

public string Prenom { get; set; }
public string Nom { get; set; }

public MainPage() {
  Prenom = "Jean"; 
  Nom = "Dupont";
  this.InitializeComponent();
  MiseAJour();
}

private void Button_Click(object s, RoutedEventArgs e) {
  Prenom = "David"; 
  Nom = "Durand";
  MiseAJour();
}

private void MiseAJour() {
  TextBlockPrenom.Text = Prenom;
  TextBlockNom.Text = Nom;
}

Le problème de cette structure est qu'elle est très difficile à maintenir :

  • Il y a deux propriétés dans le code-behind que l'on doit mettre à jour en appelant à chaque fois la méthode MiseAJour(). Cette méthode doit être appelée à chaque fois que les données pourraient changer.

  • L'accès aux éléments de la vue est direct, ce qui rend le code extrêmement dépendant de la vue.

  • Avec deux TextBlocks et deux propriétés, cela fait déjà beaucoup de code. Que faire avec une page plus complexe ?

  • Comment gérer ces données quand il faut afficher une liste ?

Les bindings (et par extension le modèle MVVM) permet d'éviter ces contraintes.

<Page ...
DataContext="{Binding RelativeSource={RelativeSource Self}}">
   <StackPanel Orientation="Vertical">
      <TextBlock Text="{Binding Prenom}" />
      <TextBlock Text="{Binding Nom}" />
   </StackPanel>
</Page>
public string Prenom { get; set; }
public string Nom { get; set; }

public MainPage() {
  Prenom = "Jean"; 
  Nom = "Dupont";
  this.InitializeComponent();
}

La balise Page possède l'attribut suivant :

DataContext="{Binding RelativeSource={RelativeSource Self}}"

Cet attribut permet de définir un contexte dans lequel les propriétés de la classe MainPage sont liées à la balise Page dans le XAML. Conséquemment, les propriétés "Prenom" et "Nom" de notre classe MainPage sont désormais accessibles dans toute balise incluse dans la balise Page.

Nos éléments TextBlocks ont pour contenu un binding vers leur propriété, Prenom ou Nom. Ainsi, au lancement de l'application, les valeurs sont bien insérées.

[Warning]

Cet exemple est encore loin d'être fonctionnel, car l'affichage ne sera pas mis à jour pendant le déroulement de l'application.

[Note]

Cet attribut DataContext se définit également dans le code-behind sous la forme :

DataContext = this;

On peut avec ce concept imaginer une structure plus complexe :

namespace HelloWorldApp {
  public class Utilisateur {
    public string Prenom { get; set; }
    public string Nom { get; set; }
  }

  public class MainPage : Page {
    public Utilisateur Utilisateur { get; set; }

    public MainPage() {
      Utilisateur = new Utilisateur()
      {
        Prenom = "Jean",
        Nom = "Dupont"
      };
      this.InitializeComponent();
    }
  }
}
<Page ...
DataContext="{Binding RelativeSource={RelativeSource Self}}">
   <StackPanel DataContext="{Binding Utilisateur}">
      <TextBlock Text="{Binding Prenom}" />
      <TextBlock Text="{Binding Nom}" />
   </StackPanel>
</Page>

Utilisation du modèle MVVM avec les Bindings

Les données ne sont pas mises à jour avec les exemples précédents, car si la vue sait quelle valeur a quelle propriété pour l'afficher, elle n'a aucun moyen de savoir quand les données ont été mises à jour. Ce modèle MVVM nous permettra donc d'ajouter cette fonctionnalité.

Comme vu précédemment, un ViewModel n'est qu'une simple classe. Elle possède différentes propriétés de votre choix, celles qui doivent être passées à la vue. Ainsi, si vous souhaitez afficher un texte et un nombre, votre ViewModel contiendra ces deux éléments.

Reprenons notre exemple affichant un prénom et un nom. Notre ViewModel possèdera donc une propriété Prénom, et une propriété Nom.

Présentation de la page XAML :

<StackPanel Orientation="Vertical" Margin="100">
  <TextBlock x:Name="TextBlockPrenom" Text="{Binding Prenom}" />
  <TextBlock x:Name="TextBlockNom" Text="{Binding Nom}" />
  <Button x:Name="ChangementTexte" Content="Changer le texte" Click="ChangementTexte_Click"/>
</StackPanel>

Présentation du ViewModel, appelé par convention avec le nom de la page concernée suivie de ViewModel :

public class MainPageViewModel
{
  private string _prenom;
  public string Prenom {
    get { return _prenom; }
    set { _prenom = value; }
  }

  public string _nom;
  public string Nom {
    get { return _nom; }
    set { _nom = value; }
  }
}

Présentation du code-behind :

public sealed partial class MainPage : Page
{
  // Création du ViewModel
  public MainPageViewModel MonViewModel = new MainPageViewModel();
  public MainPage()
  {
    // On définit par défaut des valeurs au ViewModel
    MonViewModel.Prenom = "Jean";
    MonViewModel.Nom = "Dupont";
    // On associe le ViewModel à la page courante.
    // Au lieu d'associer this et par là même toute l'instance de classe MainPage,
    // on communique uniquement à la vue l'objet ViewModel.
    DataContext = MonViewModel;
    this.InitializeComponent();
  }

  private void ChangementTexte_Click(object sender, RoutedEventArgs e)
  {
    // Au changement du texte, rien ne se passe, car la page XAML n'a
    // aucune information que le ViewModel a changé.
    MonViewModel.Prenom = "David";
    MonViewModel.Nom = "Durand";
  }
}

La partie centrale des bindings se situe dans la mise à jour des données. A cet effet, le framework .NET dispose de l'interface INotifyPropertyChanged et son événement PropertyChanged.

L'interface INotifyPropertyChanged appliqué à n'importe quel classe permet de disposer d'une propriété-événement appelée PropertyChanged. Cet interface est utilisé majoritairement dans le cadre qui nous intéresse, à savoir les bindings. Chaque binding, si la donnée liée implémente INotifyPropertyChanged, va être à l'écoute de l'événement PropertyChanged et changer son affichage en conséquence.

Cela implique deux concepts fondamentaux dans l'utilisation des bindings et plus généralement de la mise à jour de données :

  • Ne pas implémenter INotifyPropertyChanged empêche le reste de l'application d'être notifié quand des données sont mises à jour

  • Chaque modification de données doit être suivi par le déclenchement de l'événement PropertyChanged pour qu'il soit ensuite consommé par le reste de l'application.

Pour déclencher l'événement PropertyChanged, il faudra appeler la méthode Invoke de l'événement avec ces deux paramètres :

  • L'objet déclenchant l'événement (majoritairement la référence à l'instance courante, this)

  • La propriété modifiée par le biais d'une nouvelle instance de PropertyChangedEventArgs

Cet événement devra être invoqué à chaque mise à jour de la propriété, soit dans le mutateur (set) de celle-ci. Voici un exemple d'utilisation :

public class Exemple : INotifyPropertyChanged
{
  // Propriété-événement à déclencher à la modification des données
  public event PropertyChangedEventHandler PropertyChanged;

  private string _valeur;
  public string Valeur {
    get { return _valeur; }
    set {
      // Si la nouvelle valeur est différente de l'ancienne
      if(_valeur != value)
      {
        // On applique cette nouvelle valeur
        _valeur = value;
        // On déclenche l'événement PropertyChanged pour la propriété Valeur
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Valeur"));
      }
    }
  }
}

Ce fonctionnement est à répliquer pour chaque propriété nécessitant une mise à jour des données. Selon l'application, toutes les propriétés ne nécessitent pas ce traitement. On peut d'ailleurs factoriser ce déclenchement d'événement via une méthode chargée d'invoquer PropertyChanged, comme le montre cet exemple :

public class Exemple : INotifyPropertyChanged
{
  public event PropertyChangedEventHandler PropertyChanged;

  private string _valeurA;
  public string ValeurA {
    get { return _valeurA; }
    set {
      if(_valeurA != value)
      {
        _valeurA = value; OnPropertyChanged("ValeurA");
      }
    }
  }

  private string _valeurB;
  public string ValeurB {
    get { return _valeurB; }
    set {
      if(_valeurB != value)
      {
        _valeurB = value; OnPropertyChanged("ValeurB");
      }
    }
  }

  private void OnPropertyChanged(string nomProp)
  {
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nomProp));
  }
}

Pour implémenter INotifyPropertyChanged dans notre structure, nous allons donc mettre à jour notre ViewModel comme suit :

// On implémente l'interface INotifyPropertyChanged
public class MainPageViewModel : INotifyPropertyChanged
{
  // Cet interface offre une propriété-événement appelée PropertyChanged.
  // Cette propriété va stocker tous les éléments de code s'intéressant
  // aux modifications du ViewModel, pour les prévenir dès qu'une modification
  // survient. On peut grossièrement comparer PropertyChanged à une liste
  // d'éléments à contacter en cas de changement.
  public event PropertyChangedEventHandler PropertyChanged;

  private string _prenom;
  public string Prenom {
    get { return _prenom; }
    set {
      // Si la valeur n'a pas changé, n'allons pas plus loin.
      if (_prenom == value) return;
      _prenom = value;
      // La valeur est différente, je notifie donc tous les éléments de code
      // écoutant les modifications de la propriété changée (ici Prenom)
      // Le caractère "?" permet d'éviter une exception si aucun code n'écoute
      // les modifications. Dans ce cas, l'événement n'est pas lancé.
      PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Prenom"));
    }
  }
  private string _nom;
  public string Nom {
    get { return _nom; }
      set {
        // Même comportement que la propriété Prenom, voir ci-dessus
        if (_nom == value) return;
        _nom = value;
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Nom"));
    }
  }
}

Les bindings en XAML étant basés sur cet interface INotifyPropertyChanged, aucune autre configuration n'est nécessaire et notre vue est bien mise à jour au clic du bouton.

Figure 1.6. Mise à jour de la vue avec MVVM

Mise à jour de la vue avec MVVM

On peut également utiliser ce modèle pour une structure plus complexe, avec un objet Utilisateur. Notre code-behind s'adapte ainsi très facilement :

public sealed partial class MainPage : Page
{
    public MainPageViewModel MonViewModel = new MainPageViewModel();
    public MainPage()
    {
        MonViewModel.Utilisateur = new Utilisateur()
        {
            Prenom = "Jean",
            Nom = "Dupont"
        };
        DataContext = MonViewModel;
        this.InitializeComponent();
    }

    private void ChangementTexte_Click(object sender, RoutedEventArgs e)
    {
        MonViewModel.Utilisateur.Prenom = "David";
        MonViewModel.Utilisateur.Nom = "Durand";
    }
}

Il est important de comprendre que l'interface INotifyPropertyChanged notifie, comme son nom l'indique, des propriétés modifiées. Or, si nous stockons un objet Utilisateur dans notre ViewModel, ce sont les propriétés de l'objet Utilisateur qui seront modifiées, et non la propriété Utilisateur elle-même. C'est donc notre modèle qui implémentera INotifyPropertyChanged, permettant par là même de factoriser ce fonctionnement pour les multiples ViewModels qu'on peut trouver dans une application complexe.

Présentation du modèle Utilisateur :

public class Utilisateur : INotifyPropertyChanged
{
  private string _prenom;

  public event PropertyChangedEventHandler PropertyChanged;

  public string Prenom
  {
    get { return _prenom; }
    set
    {
      if (_prenom == value) return;
      _prenom = value;
      PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Prenom"));
    }
  }
  private string _nom;
  public string Nom
  {
    get { return _nom; }
    set
    {
      if (_nom == value) return;
      _nom = value;
      PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Nom"));
    }
  }
}

Après avoir fait cette structure, le code est grandement simplifié dans les autres parties de l'application :

Présentation du ViewModel :

public class MainPageViewModel
{
  public Utilisateur Utilisateur { get; set; }
}

Présentation du code-behind :

public sealed partial class MainPage : Page
{
  public MainPageViewModel MonViewModel = new MainPageViewModel();

  public MainPage()
  {
    MonViewModel.Utilisateur = new Utilisateur()
    {
      Prenom = "Jean",
      Nom = "Dupont"
    };
    DataContext = MonViewModel;
    this.InitializeComponent();
  }

  private void ChangementTexte_Click(object sender, RoutedEventArgs e)
  {
    MonViewModel.Utilisateur.Prenom = "David";
    MonViewModel.Utilisateur.Nom = "Durand";
  }
}

Il ne nous reste plus qu'à changer légèrement notre StackPanel, pour définir son contexte de données (DataContext) comme la propriété "Utilisateur" de notre ViewModel.

Présentation du code XAML :

<StackPanel Orientation="Vertical" DataContext="{Binding Utilisateur}" Margin="100">
  <TextBlock x:Name="TextBlockPrenom" Text="{Binding Prenom}" />
  <TextBlock x:Name="TextBlockNom" Text="{Binding Nom}" />
  <Button x:Name="ChangementTexte" Content="Changer le texte" Click="ChangementTexte_Click"/>
</StackPanel>

Pour finir, que faire avec une liste d'utilisateurs ? Le modèle reste strictement le même. Attention cependant, nous n'allons pas utiliser la structure List mais ObservableCollection, qui implémente automatiquement toutes les mécaniques de mises à jour. Ainsi, si notre liste est modifiée, la vue sera notifiée et prendra en compte les notifications.

Modifions notre ViewModel comme suit :

public class MainPageViewModel
{
  public ObservableCollection<Utilisateur> Utilisateurs { get; set; }
}

Notre vue en XAML comme suit :

<ListView ItemsSource="{Binding Utilisateurs}">
  <ListView.ItemTemplate>
    <DataTemplate>
      <StackPanel Orientation="Horizontal">
        <TextBlock Text="{Binding Prenom}" />
        <TextBlock Text="{Binding Nom}" />
      </StackPanel>
    </DataTemplate>
  </ListView.ItemTemplate>
</ListView>

Et notre code-behind comme suit. Aucune réelle modification ici à part initialiser cette collection d'éléments :

public MainPage()
{
  MonViewModel.Utilisateurs = new ObservableCollection<Utilisateur>()
  {
    new Utilisateur()
    {
      Prenom = "Jean",
      Nom = "Dupont"
    },
    new Utilisateur()
    {
      Prenom = "David",
      Nom = "Durand"
    }
  };
  DataContext = MonViewModel;
  this.InitializeComponent();
}

La liste est bien initialisée et sera mise à jour dans la vue à chaque modification. On peut ainsi imaginer pour terminer notre panorama des bindings un bouton ajoutant un élémént dans notre collection d'utilisateurs tout en changeant également le premier élément.

private void ChangementListe_Click(object sender, RoutedEventArgs e)
{
  MonViewModel.Utilisateurs.Add(new Utilisateur()
  {
    Prenom = "Marie",
    Nom = "Richard"
  });
  MonViewModel.Utilisateurs.ElementAt(0).Prenom = "Pauline";
}

Figure 1.7. Mise à jour de la vue avec MVVM pour une liste

Mise à jour de la vue avec MVVM pour une liste

Pour plus d'informations sur les bindings, les interfaces INotifyPropertyChanged et les nouveautés de Visual Studio 2015 (intégrant notamment le binding résolu à la compilation), n'hésitez pas à visionner cette vidéo de Channel9 :

Figure 1.8. Explications complémentaires à propos des bindings (vidéo en Anglais)


About SUPINFO | Contacts & addresses | Teachers | Press | INVESTOR | Conditions of Use & Copyright | Respect of Privacy
Logo de la société Cisco Logo de la société IBM Logo de la société Sun-Oracle Logo de la société Apple Logo de la société Sybase Logo de la société Novell Logo de la société Intel Logo de la société Accenture Logo de la société SAP Logo de la société Prometric Logo du IT Academy Program par Microsoft

SUPINFO International University is globally operated by EDUCINVEST Belgium - Avenue Louise, 534 - 1050 Brussels
and is accredited in France by Association Ecole Supérieure d'Informatique de Paris (ESI SUPINFO)