• Facile

Ce cours est visible gratuitement en ligne.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Mis à jour le 29/04/2014

La manipulation des données (DataBinding & Converters)

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

S’il y a vraiment un chapitre où il faut être attentif, c’est bien celui-là. La liaison de données (ou databinding en anglais) est une notion indispensable et incontournable pour toute personne souhaitant réaliser des applications XAML sérieuses. Nous allons voir dans ce chapitre de quoi il s’agit et comment le mettre en place.

Principe du Databinding

Le databinding se traduit en français par « liaison de données ». Il s’agit de la possibilité de lier un contrôle à des données. Le principe consiste à indiquer à un contrôle où il peut trouver sa valeur et celui-ci se débrouille pour l’afficher. Nous l’avons entre-aperçu dans le chapitre précédent avec la ListBox, il est temps de creuser un peu son fonctionnement.
Techniquement, le moteur utilise un objet de type Binding qui associe une source de données à un élément de destination, d’où l’emploi du mot raccourci binding pour représenter la liaison de données.
Le binding permet de positionner automatiquement des valeurs aux propriétés des contrôles en fonction du contenu de la source de données. En effet, il est très fréquent de mettre des valeurs dans des TextBox, dans des TextBlock ou dans des ListBox, comme nous l’avons fait. Le binding est là pour faciliter tout ce qui peut être automatisable et risque d’erreurs. De plus, si la source de données change, il est possible de faire en sorte que le contrôle soit automatiquement mis à jour. Inversement, si des modifications sont faites depuis l’interface, alors on peut être notifié automatiquement des changements.

Le binding des données

Pour illustrer le fonctionnement le plus simple du binding, nous allons lier une zone de texte modifiable (TextBox) à une propriété d’une classe. Puisque le TextBox travaille avec du texte, il faut créer une propriété de type string sur une classe. Cette classe sera le contexte de données du contrôle. Créons donc la nouvelle classe suivante :

public class Contexte
{
    public string Valeur { get; set; }
}

Et une instance de cette classe dans notre code behind :

public partial class MainPage : PhoneApplicationPage
{
    private Contexte contexte;

    public MainPage()
    {
        InitializeComponent();

        contexte = new Contexte { Valeur = "Nicolas" };
        DataContext = contexte;
    }
}

Nous remarquons à la fin du constructeur que la propriété DataContext de la page est initialisée avec notre contexte de données, étape obligatoire permettant de lier la page au contexte de données.
Chaque objet FrameworkElement possède une propriété DataContext et chaque élément enfant d’un autre élément hérite de son contexte de données implicitement. C’est pour cela qu’ici on initialise notre contexte de données au niveau de la page afin que tous les éléments contenus dans la page héritent de ce contexte de données.

Il ne reste plus qu’à ajouter un contrôle TextBox qui sera lié à cette propriété :

<TextBox Text="{Binding Valeur}" Height="80"/>

Cela se fait grâce à l’expression de balisage {Binding}. Lorsque nous exécutons notre application, nous pouvons voir que la TextBox s’est correctement remplie avec la chaine de caractères Nicolas (voir la figure suivante).

La valeur du TextBox est liée à la propriété Valeur
La valeur du TextBox est liée à la propriété Valeur

Et tout ça automatiquement, sans avoir besoin de positionner la valeur de la propriété Text depuis le code behind.

Qu’est-ce qu’a fait le moteur de binding ? Il est allé voir dans son contexte (propriété DataContext) puis il est allé prendre le contenu de la propriété Valeur de ce contexte pour le mettre dans la propriété Text du TextBox, c’est-à-dire la chaine « Nicolas ».

Il faut faire attention car dans le XAML nous écrivons du texte, si nous orthographions mal Valeur, par exemple en oubliant le « u » :

<TextBox Text="{Binding Valer}" Height="80"/>

Alors la liaison de données n'aura pas lieu car la propriété est introuvable sur l’objet Contexte. Ce qui est vrai ! « Valer » n’existe pas. Il n’y a pas de vérification à la compilation, c’est donc au moment de l’exécution que nous remarquerons l’absence du binding.
La seule information que nous aurons, c’est dans la fenêtre de sortie du débogueur, où nous aurons :

System.Windows.Data Error: BindingExpression path error: 'Valer' property not found on 'DemoPartie2.Contexte' 'DemoPartie2.Contexte' 
(HashCode=54897010). BindingExpression: Path='Valer' DataItem='DemoPartie2.Contexte' (HashCode=54897010); target element is 'System.Windows.Controls.TextBox' 
(Name=''); target property is 'Text' (type 'System.String')..

indiquant que la propriété n’a pas été trouvée.

Il est également possible de définir un binding par code behind, pour cela enlevez l’expression de balisage dans le XAML et donnez un nom à votre contrôle :

<TextBox x:Name="MonTextBox" Height="80"/>

puis utilisez le code behind suivant :

public partial class MainPage : PhoneApplicationPage
{
    private Contexte contexte;

    public MainPage()
    {
        InitializeComponent();

        MonTextBox.SetBinding(TextBox.TextProperty, new Binding("Valeur"));
        contexte = new Contexte { Valeur = "Nicolas" };
        DataContext = contexte;
    }
}

On en profite pour constater que le binding se fait bien avec une propriété de dépendance, ici TextBox.TextProperty.

Le binding est très pratique pour qu’un contrôle se remplisse avec la bonne valeur.

Il est possible de modifier la valeur affichée dans la zone de texte très facilement en modifiant la valeur du contexte depuis le code. Pour cela, changeons le XAML pour ajouter un bouton qui va nous permettre de déclencher ce changement de valeur :

<StackPanel>
    <TextBox Text="{Binding Valeur}" Height="80"/>
    <Button Content="Changer valeur" Tap="Button_Tap" />
</StackPanel>

Et dans l’événement de Tap, faisons :

private void button_Tap(object sender, System.Windows.Input.GestureEventArgs e)
{
    if (contexte.Valeur == "Nicolas")
        contexte.Valeur = "Jérémie";
    else
        contexte.Valeur = "Nicolas";
}

Par contre, il va manquer quelque chose. Un moyen de dire à la page « hé, j’ai modifié un truc, il faut que tu regardes si tu es impacté ». Ça, c’est le rôle de l’interface INotifyPropertyChanged. Notre classe de contexte doit implémenter cette interface et faire en sorte que quand on modifie la propriété, elle lève l’événement qui va permettre à l’interface de se mettre à jour. Notre classe de contexte va donc devenir :

public class Contexte : INotifyPropertyChanged
{
    private string valeur;
    public string Valeur
    {
        get
        {
            return valeur;
        }
        set
        {
            if (value == valeur)
                return;
            valeur = value;
            NotifyPropertyChanged("Valeur");
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    public void NotifyPropertyChanged(string nomPropriete)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(nomPropriete));
    }
}

Ici, lorsque nous affectons une valeur à la propriété, la méthode NotifyPropertyChanged est appelée en passant en paramètre le nom de la propriété de la classe qu’il faut rafraichir sur la page. Attention, c’est une erreur classique de ne pas avoir le bon nom de propriété en paramètres, faites-y attention.

Notez qu’avec la version 8.0 du SDK (en fait, grâce au framework 4.5), il est possible d’utiliser une autre solution pour implémenter INotifyPropertyChanged sans avoir l’inconvénient de devoir passer une chaine de caractère en paramètre. Il suffit d’utiliser l’attribut de méthode CallerMemberName, qui permet d’obtenir le nom de la propriété (ou méthode) qui a appelé notre méthode, en l’occurrence il s’agira justement du nom de la propriété qu’on aurait passé en paramètre :

public class Contexte : INotifyPropertyChanged
{
    private string valeur;
    public string Valeur
    {
        get { return valeur; }
        set { NotifyPropertyChanged(ref valeur, value); }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    public void NotifyPropertyChanged(string nomPropriete)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(nomPropriete));
    }

    private bool NotifyPropertyChanged<T>(ref T variable, T valeur, [CallerMemberName] string nomPropriete = null)
    {
        if (object.Equals(variable, valeur)) return false;

        variable = valeur;
        NotifyPropertyChanged(nomPropriete);
        return true;
    }
}

La syntaxe est plus élégante et il n’y a pas de risque de mal orthographier le nom de la propriété. Par contre, je suis désolé pour ceux qui suivent le cours avec la version 7.1 du SDK, il faudra continuer à utiliser la solution que j’ai présentée juste avant.

Note, il faudra inclure l’espace de nom :

using System.Runtime.CompilerServices;

Relançons l’application, nous pouvons voir que le clic sur le bouton entraîne bien le changement de valeur dans la TextBox.

Ok, c’est bien beau tout ça, mais n’est-ce pas un peu compliqué par rapport à ce qu’on a déjà fait, à savoir modifier directement la valeur de la propriété Text ?

Effectivement, dans ce cas-là, on pourrait juger que c’est sortir l’artillerie lourde pour pas grand-chose. Cependant c’est une bonne pratique dans la mesure où on automatise le processus de mise à jour de la propriété. Vous aurez remarqué que l’on ne manipule plus directement le contrôle mais une classe qui n’a rien à voir avec le TextBox. Et quand il y a plusieurs valeurs à mettre à jour d’un coup, c’est d’autant plus facile. De plus, nous pouvons faire encore mieux avec ce binding grâce à la bidirectionnalité de la liaison de données. Par exemple, modifions le XAML pour rajouter encore un bouton :

<StackPanel>
    <TextBox Text="{Binding Valeur, Mode=TwoWay}" Height="80"/>
    <Button Content="Changer valeur" Tap="Button_Tap" />
    <Button Content="Afficher valeur" Tap="Button_Tap_1" />
</StackPanel>

La méthode associée à ce nouveau clic affichera la valeur du contexte :

private void button_Tap_1(object sender, System.Windows.Input.GestureEventArgs e)
{
    MessageBox.Show(contexte.Valeur);
}

On utilise pour cela la méthode MessageBox.Show qui affiche une petite boîte de dialogue minimaliste.
Les lecteurs attentifs auront remarqué que j’ai enrichi le binding sur la Valeur en rajoutant un Mode=TwoWay. Ceci permet d’indiquer que le binding s’effectue dans les deux sens. C’est-à-dire que si je modifie la propriété de la classe de contexte, alors l’interface est mise à jour. Inversement, si je modifie la valeur de la TextBox avec le clavier virtuel, alors la propriété de la classe est également mise à jour.
C’est à cela que va servir notre deuxième bouton. Démarrez l’application, modifiez la valeur du champ avec le clavier virtuel et cliquez sur le bouton permettant d’afficher la valeur (voir la figure suivante).

La valeur est affichée grâce à la liaison de données bidirectionnelle
La valeur est affichée grâce à la liaison de données bidirectionnelle

La valeur est bien récupérée. Vous pouvez faire le test en enlevant le mode TwoWay, vous verrez que vous ne récupérerez pas la bonne valeur.
Plutôt pas mal non ?

Maintenant que nous avons un peu mieux compris le principe du binding, il est temps de préciser un point important. Pour illustrer le fonctionnement du binding, j’ai créé une classe puis j’ai créé une variable à l’intérieur de cette classe contenant une instance de cette classe. Puis j’ai relié cette classe au contexte de données de la page. En général, on utilise ce découpage dans une application utilisant le patron de conception MVVM (Model-View-ViewModel). Je parlerai de ce design pattern dans le prochain chapitre.

Remarquez que l’on voit souvent la construction où c’est la classe de la page qui sert de contexte de données de la page.
Cela veut dire qu’on peut modifier l’exemple précédent pour que ça soit la classe MainPage qui implémente l’interface INotifyPropertyChanged, ce qui donne :

public partial class MainPage : PhoneApplicationPage, INotifyPropertyChanged
{
    private string valeur;
    public string Valeur
    {
        get { return valeur; }
        set { NotifyPropertyChanged(ref valeur, value); }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    public void NotifyPropertyChanged(string nomPropriete)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(nomPropriete));
    }

    private bool NotifyPropertyChanged<T>(ref T variable, T valeur, [CallerMemberName] string nomPropriete = null)
    {
        if (object.Equals(variable, valeur)) return false;

        variable = valeur;
        NotifyPropertyChanged(nomPropriete);
        return true;
    }

    public MainPage()
    {
        InitializeComponent();

        Valeur = "Nicolas";
        DataContext = this;
    }

    private void Button_Tap(object sender, System.Windows.Input.GestureEventArgs e)
    {
        if (Valeur == "Nicolas")
            Valeur = "Jérémie";
        else
            Valeur = "Nicolas";
    }
    private void Button_Tap_1(object sender, System.Windows.Input.GestureEventArgs e)
    {
        MessageBox.Show(Valeur);
    }
}

La classe Contexte n’a plus de raison d’être. Tout est porté par la classe représentant la page. On affecte donc l’objet this à la propriété DataContext de la page. Cette construction est peut-être un peu plus perturbante d’un point de vue architecture où on a tendance à mélanger les responsabilités dans la classe mais elle a l’avantage de simplifier pas mal le travail. Personnellement j’emploie très rarement ce genre de construction (j’utilise plutôt un dérivé de la première solution), mais je l’utiliserai de temps en temps dans ce cours pour simplifier le code.

Notre ListBox fonctionne également avec le binding. Il suffit d’utiliser l’expression de balisage avec la propriété ItemsSource :

<ListBox ItemsSource="{Binding Prenoms}" />

Nous aurons bien sûr défini la propriété Prenoms dans notre contexte :

public partial class MainPage : PhoneApplicationPage, INotifyPropertyChanged
{
    private List<string> prenoms;
    public List<string> Prenoms
    {
        get { return prenoms; }
        set { NotifyPropertyChanged(ref prenoms, value); }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    public void NotifyPropertyChanged(string nomPropriete)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(nomPropriete));
    }

    private bool NotifyPropertyChanged<T>(ref T variable, T valeur, [CallerMemberName] string nomPropriete = null)
    {
        if (object.Equals(variable, valeur)) return false;

        variable = valeur;
        NotifyPropertyChanged(nomPropriete);
        return true;
    }

    public MainPage()
    {
        InitializeComponent();

        Prenoms = new List<string> { "Nicolas", "Jérémie", "Delphine" };
        DataContext = this;
    }
}

Ce qui donne la figure suivante.

Liaison de données avec une ListBox
Liaison de données avec une ListBox

Le binding est encore plus puissant que ça, voyons encore un point intéressant. Il s’agit de la capacité de lier une propriété d’un contrôle à la propriété d’un autre contrôle.
Par exemple, mettons un TextBlock en plus de notre ListBox :

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>
    <ListBox x:Name="ListeBoxPrenoms" ItemsSource="{Binding Prenoms}" />
    <TextBlock Grid.Column="1" Text="{Binding ElementName=ListeBoxPrenoms, Path=SelectedItem}" Foreground="Red" />
</Grid>

Regardons l’expression de binding du TextBlock, nous indiquons que nous voulons lier la valeur du TextBlock à la propriété SelectedItem du contrôle nommé ListeBoxPrenoms. Ici cela voudra dire que lorsque nous sélectionnerons un élément dans la ListBox, alors celui-ci sera automatiquement affiché dans le TextBlock, sans avoir rien d’autre à faire :

Liaison de données entre propriétés de contrôles
Liaison de données entre propriétés de contrôles

Tout simplement. :D

Voilà pour cet aperçu du binding. Nous n’en avons pas vu toutes les subtilités mais ce que nous avons étudié ici vous sera grandement utile et bien souvent suffisant dans vos futures applications Windows Phone !

Binding et mode design

Vous vous rappelez notre ListBox quelques chapitres avant ? Nous avions créé une ListBox avec une liste de choses à faire. Cette liste de choses à faire était alimentée par la propriété ItemsSource dans le constructeur de la page. Le problème c’est que notre ListBox était vide en mode design. Du coup, pas facile pour faire du style, pour mettre d’autres contrôles, etc.
Le binding va nous permettre de résoudre ce problème. Prenons le code XAML suivant :

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
    <ListBox ItemsSource="{Binding ListeDesTaches}" >
        <ListBox.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal">
                    <Image Source="{Binding Image}" Width="30" Height="30" />
                    <TextBlock Text="{Binding Description}" Margin="20 0 0 0" />
                </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</Grid>

Avec dans le code behind :

public partial class MainPage : PhoneApplicationPage, INotifyPropertyChanged
{
    private IEnumerable<ElementAFaireBinding> listeDesTaches;
    public IEnumerable<ElementAFaireBinding> ListeDesTaches
    {
        get { return listeDesTaches; }
        set { NotifyPropertyChanged(ref listeDesTaches, value); }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    public void NotifyPropertyChanged(string nomPropriete)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(nomPropriete));
    }

    private bool NotifyPropertyChanged<T>(ref T variable, T valeur, [CallerMemberName] string nomPropriete = null)
    {
        if (object.Equals(variable, valeur)) return false;

        variable = valeur;
        NotifyPropertyChanged(nomPropriete);
        return true;
    }

    public MainPage()
    {
        InitializeComponent();

        List<ElementAFaire> chosesAFaire = new List<ElementAFaire>
        {
            new ElementAFaire { Priorite = 1, Description = "Arroser les plantes"},
            new ElementAFaire { Priorite = 2, Description = "Tondre le gazon"},
            new ElementAFaire { Priorite = 1, Description = "Planter les tomates"},
            new ElementAFaire { Priorite = 3, Description = "Laver la voiture"},
        };

        ListeDesTaches = chosesAFaire.OrderBy(e => e.Priorite).Select(e => new ElementAFaireBinding { Description = e.Description, Image = ObtientImage(e.Priorite) });

        DataContext = this;
    }

    private BitmapImage ObtientImage(int priorite)
    {
        if (priorite <= 1)
            return new BitmapImage(new Uri("/Assets/Images/vert.png", UriKind.Relative));
        return new BitmapImage(new Uri("/Assets/Images/rouge.png", UriKind.Relative));
    }
}

public class ElementAFaire
{
    public int Priorite { get; set; }
    public string Description { get; set; }
}
public class ElementAFaireBinding
{
    public BitmapImage Image { get; set; }
    public string Description { get; set; }
}

Pour l’instant, rien n’a changé, la ListBox est toujours vide en mode design. Sauf que nous avons également la possibilité de lier le mode design à un contexte de design. Créons donc une nouvelle classe :

public class MainPageDesign
{
    public IEnumerable<ElementAFaireBinding> ListeDesTaches
    {
        get
        {
            return new List<ElementAFaireBinding>
            {
                new ElementAFaireBinding { Image = new BitmapImage(new Uri("/Assets/Images/vert.png", UriKind.Relative)), Description = "Arroser les plantes"},
                new ElementAFaireBinding { Image = new BitmapImage(new Uri("/Assets/Images/rouge.png", UriKind.Relative)), Description = "Tondre le gazon"},
                new ElementAFaireBinding { Image = new BitmapImage(new Uri("/Assets/Images/rouge.png", UriKind.Relative)), Description = "Planter les tomates"},
                new ElementAFaireBinding { Image = new BitmapImage(new Uri("/Assets/Images/vert.png", UriKind.Relative)), Description = "Laver la voiture"},
            }; ;
        }
    }
}

Cette classe ne fait que renvoyer une propriété ListeDesTaches avec des valeurs de design. Compilez et rajoutez maintenant dans le XAML l’instruction suivante avant le conteneur de plus haut niveau :

<d:DesignProperties.DataContext>
    <design:MainPageDesign />
</d:DesignProperties.DataContext>

Ceci permet de dire que le contexte de design est à aller chercher dans la classe MainPageDesign.
Attention, la classe MainPageDesign n’est pas connue de la page ! Il faut lui indiquer où elle se trouve, en indiquant son espace de nom, un peu comme un using C#. Cette propriété se rajoute dans les propriétés de la page, <phone:PhoneApplicationPage> :

<phone:PhoneApplicationPage
    x:Class="DemoPartie2.MainPage"
    […plein de choses …]
    xmlns:design="clr-namespace:DemoPartie2"
    shell:SystemTray.IsVisible="True">

Avec cette écriture, je lui dis que le raccourci « design » correspond à l’espace de nom DemoPartie2.

Nous commençons à voir apparaître des choses dans le designer de Visual Studio (voir la figure suivante).

Le designer affiche les données de design grâce à la liaison de données
Le designer affiche les données de design grâce à la liaison de données

Avouez que c’est beaucoup plus pratique pour réaliser le design de sa page. :)

Avant de terminer, il faut savoir que Blend est également capable de nous générer des données de design sans que l'on ait forcément besoin de créer une classe spécifique. Pour illustrer ceci, repartez d’une nouvelle page vide. Puis ouvrez la page dans Expression Blend.

Cliquez sur l’onglet données, puis sur l’icône tout à droite créer des exemples de données (voir la figure suivante).

Création des exemples de données dans Blend
Création des exemples de données dans Blend

Cliquez sur nouvel exemple de données dans le menu proposé.
Indiquez un nom pour la source de données et choisissez de la définir dans ce document uniquement (voir la figure suivante).

Création de la source de données
Création de la source de données

Nous obtenons notre source de données, comme vous pouvez le voir à la figure suivante.

La source de données est visibles dans l'écran de données
La source de données est visibles dans l'écran de données

Elle est composée d’une collection d’objets contenant 2 propriétés. Property1 est de type chaine de caractères et Property2 est de type booléen (on peut le voir en cliquant sur les petits boutons à droite).
Renommez la première en Description et la seconde en Image, puis cliquez sur l’icône à droite pour changer le type de la propriété de la seconde, comme sur la figure suivante.

Modification du type de la propriété
Modification du type de la propriété

Et choisissez le type Image (voir la figure suivante).

Le type de la données est désormais Image
Le type de la données est désormais Image

Nous avons créé ici un jeu de données, stockées sous la forme d’un fichier XAML.
Il est possible de modifier les données en cliquant sur le bouton modifier les exemples de valeurs (voir la figure suivante).

Modification des exemples de valeurs
Modification des exemples de valeurs

Nous obtenons une fenêtre de ce style (voir la figure suivante).

Fenêtre de modification des exemples de valeurs
Fenêtre de modification des exemples de valeurs

On peut y mettre nos propres valeurs. Ici, j’ai changé le premier élément pour lui indiquer la valeur que je voulais et l’image que je souhaitais, mais il est également possible d’indiquer un répertoire pour sélectionner les images.
Maintenant, il est temps d’utiliser nos données. Sélectionnez la collection et faites-la glisser dans la fenêtre de design, comme indiqué sur la figure suivante.

La source de données est glissée dans le designer
La source de données est glissée dans le designer

Il vous crée automatiquement une ListBox avec les nouvelles données de la source de données (voir la figure suivante).

Une ListBox est automatiquement créée à partir de la source de données
Une ListBox est automatiquement créée à partir de la source de données

Utiliser l’ObservableCollection

Avant de terminer sur la liaison de données, reprenons un exemple simplifié de notre liste de taches. Avec le XAML suivant :

<Grid x:Name="LayoutRoot" Background="Transparent">
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
        <RowDefinition Height="100"/>
    </Grid.RowDefinitions>
    <ListBox ItemsSource="{Binding ListeDesTaches}" >
        <ListBox.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="{Binding Priorite}" Margin="20 0 0 0" />
                    <TextBlock Text="{Binding Description}" Margin="20 0 0 0" />
                </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
    <Button Content="Ajouter un élément" Tap="Button_Tap" Grid.Row="1" />
</Grid>

Où nous affichons notre liste des tâches avec la valeur de la priorité et la description dans des TextBlock. Nous disposons également d’un bouton en bas pour rajouter un nouvel élément.
Le code behind sera :

public partial class MainPage : PhoneApplicationPage, INotifyPropertyChanged
{
    private List<ElementAFaire> listeDesTaches;
    public List<ElementAFaire> ListeDesTaches
    {
        get { return listeDesTaches; }
        set { NotifyPropertyChanged(ref listeDesTaches, value); }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    public void NotifyPropertyChanged(string nomPropriete)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(nomPropriete));
    }

    private bool NotifyPropertyChanged<T>(ref T variable, T valeur, [CallerMemberName] string nomPropriete = null)
    {
        if (object.Equals(variable, valeur)) return false;

        variable = valeur;
        NotifyPropertyChanged(nomPropriete);
        return true;
    }

    public MainPage()
    {
        InitializeComponent();

        List<ElementAFaire> chosesAFaire = new List<ElementAFaire>
        {
            new ElementAFaire { Priorite = 1, Description = "Arroser les plantes"},
            new ElementAFaire { Priorite = 2, Description = "Tondre le gazon"},
            new ElementAFaire { Priorite = 1, Description = "Planter les tomates"},
            new ElementAFaire { Priorite = 3, Description = "Laver la voiture"},
        };

        ListeDesTaches = chosesAFaire.OrderBy(e => e.Priorite).ToList();

        DataContext = this;
    }

    private void Button_Tap(object sender, System.Windows.Input.GestureEventArgs e)
    {
        ListeDesTaches.Add(new ElementAFaire { Priorite = 1, Description = "Faire marcher ce binding !" });
    }
}

public class ElementAFaire
{
    public int Priorite { get; set; }
    public string Description { get; set; }
}

La différence avec la version précédente est que nous utilisons une List<ElementAFaire> comme type d’objet lié à la source de données de la ListBox. Nous pouvons également voir que dans l’événement de clic sur le bouton, nous ajoutons un nouvel élément à la liste des taches, en utilisant la méthode Add() de la classe List<>.
Si nous exécutons notre application et que nous cliquons sur le bouton, un élément est rajouté à la liste, sauf que rien n’est visible dans notre ListBox. Problème !

Ah oui, c’est vrai, nous n’avons pas informé la page que la ListBox devait se mettre à jour. Pour ce faire, il faudrait modifier l’événement de clic sur le bouton de cette façon :

List<ElementAFaire> nouvelleListe = new List<ElementAFaire>(ListeDesTaches);
nouvelleListe.Add(new ElementAFaire { Priorite = 1, Description = "Faire marcher ce binding !" });
ListeDesTaches = nouvelleListe;

C’est-à-dire créer une copie de la liste, ajouter un nouvel élément et affecter cette nouvelle liste à la propriété ListDesTaches. Ce qui devient peu naturel …

C’est parce que la liste n’implémente pas INotifyCollectionChanged qui permet d’envoyer des évènements sur l’ajout ou la suppression d’un élément dans une liste. Heureusement il existe une autre classe dans le framework .NET qui implémente déjà ce comportement, il s’agit de la classe ObservableCollection. Il s’agit d’une liste évoluée prenant en charge les mécanismes de notification automatiquement lorsque nous faisons un ajout à la collection, lorsque nous supprimons un élément, etc. Changeons donc le type de notre propriété de liaison :

private ObservableCollection<ElementAFaire> listeDesTaches;
public ObservableCollection<ElementAFaire> ListeDesTaches
{
    get { return listeDesTaches; }
    set { NotifyPropertyChanged(ref listeDesTaches, value); }
}

Dans le constructeur, il faudra changer l’initialisation de la liste :

ListeDesTaches = new ObservableCollection<ElementAFaire>(chosesAFaire.OrderBy(e => e.Priorite));

Et désormais, lors du clic, il suffira de faire :

ListeDesTaches.Add(new ElementAFaire { Priorite = 1, Description = "Faire marcher ce binding !" });

Ce qui est quand même beaucoup plus simple.
Plutôt pratique cette ObservableCollection. Elle nous simplifie énormément la tâche lorsqu’il s’agit de faire des opérations sur une collection et qu’un contrôle doit être notifié de ce changement. C’est le complément idéal pour toute ListBox qui se respecte. De plus, avec l'ObservableCollection, notre ListBox ne s'est pas complètement rafraîchie, elle a simplement ajouté un élément. Avec la méthode précédente, c'est toute la liste qui se met à jour d'un coup, ce qui pénalise un peu les performances.

Alors pourquoi je ne l’ai pas utilisé avant ? Parce que je considère qu’il est important de comprendre ce que l’on a fait. Le binding fonctionne avec tout ce qui est énumérable, comme la List<> ou n’importe quoi implémentant IEnumerable<>. C’est ce que j’ai illustré au début du chapitre. Lorsqu’on a besoin uniquement de remplir un contrôle et qu’il ne va pas se mettre à jour, ou pas directement, utiliser une liste ou un IEnumerable est le plus simple et le plus performant. Cela permet également de ne pas avoir besoin d’instancier une ObservableCollection.
Si bien sûr, il y a beaucoup d’opération sur la liste, suppression, mise à jour, ajout, … il sera beaucoup plus pertinent d’utiliser une ObservableCollection. Mais il faut faire attention à l’utiliser correctement…
Imaginons par exemple que je veuille mettre à jour toutes mes priorités… Comme je suis en avance, je rajoute un bouton me permettant d’augmenter la priorité de 1 pour chaque élément :

<Grid x:Name="LayoutRoot" Background="Transparent">
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
        <RowDefinition Height="150"/>
    </Grid.RowDefinitions>
    <ListBox ItemsSource="{Binding ListeDesTaches}" >
        <ListBox.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="{Binding Priorite}" Margin="20 0 0 0" />
                    <TextBlock Text="{Binding Description}" Margin="20 0 0 0" />
                </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
    <StackPanel Grid.Row="1">
        <Button Content="Ajouter un élément" Tap="Button_Tap" />
        <Button Content="Augmenter les priorités" Tap="Button_Tap_1" />
    </StackPanel>
</Grid>

Et dans la méthode du clic, je peux faire :

private void Button_Tap_1(object sender, RoutedEventArgs e)
{
    foreach (ElementAFaire element in ListeDesTaches)
    {
        element.Priorite++;
    }
}

Sauf qu’après un clic sur notre bouton, on se rend compte que l’ObservableCollection est mise à jour mais pas la ListBox… Aarrrgghhhh ! Alors que notre ObservableCollection était censée résoudre tous nos problèmes de notification …

C’est là où il est important d’avoir compris ce qu’on faisait réellement …
Ici, ce n’est pas la collection que l’on a modifiée (pas d’ajout, pas de suppression, …), mais bien l’objet contenu dans la collection. Il doit donc implémenter INotifyPropertyChanged, ce qui donne :

public class ElementAFaire : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public void NotifyPropertyChanged(string nomPropriete)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(nomPropriete));
    }

    private bool NotifyPropertyChanged<T>(ref T variable, T valeur, [CallerMemberName] string nomPropriete = null)
    {
        if (object.Equals(variable, valeur)) return false;

        variable = valeur;
        NotifyPropertyChanged(nomPropriete);
        return true;
    }

    private int priorite;
    public int Priorite
    {
        get { return priorite; }
        set { NotifyPropertyChanged(ref priorite, value); }
    }
    public string Description { get; set; }
}

Il faut également notifier du changement lors de l’accès à la propriété Priorite. En toute logique, il faudrait également le faire sur la propriété Description, mais vu que nous ne nous en servons pas ici, je vous fais grâce de ce changement (voir la figure suivante).

La collection est mise à jour grâce à l'implémentation de l'interface INotifyPropertyChanged
La collection est mise à jour grâce à l'implémentation de l'interface INotifyPropertyChanged

L’ObservableCollection est donc une classe puissante mais qui peut nous jouer quelques tours si son fonctionnement n’est pas bien maîtrisé.

Les converters

Parfois, lorsque nous faisons des liaisons de données, la source de données ne correspond pas exactement à ce que nous souhaitons afficher ; la preuve juste au-dessus. Nous voulions afficher une image dans une ListBox mais nous n’avions à notre disposition qu’un chiffre représentant une priorité. Pour y remédier, nous avions construit un objet spécial avec directement les bonnes valeurs via la classe ElementAFaireBinding :

List<ElementAFaire> chosesAFaire = new List<ElementAFaire>
{
    new ElementAFaire { Priorite = 1, Description = "Arroser les plantes"},
    new ElementAFaire { Priorite = 2, Description = "Tondre le gazon"},
    new ElementAFaire { Priorite = 1, Description = "Planter les tomates"},
    new ElementAFaire { Priorite = 3, Description = "Laver la voiture"},
};

listeDesTaches.ItemsSource = chosesAFaire.OrderBy(e => e.Priorite).Select(e => new ElementAFaireBinding { Description = e.Description, Image = ObtientImage(e.Priorite) });

C’est une bonne façon de faire mais il existe une autre solution qui consiste à appliquer un convertisseur lors de la liaison de données. Appelés « converters » en anglais, ils font en sorte de transformer une donnée en une autre, adaptée à ce que l’on souhaite lier.

Un exemple sera plus clair qu’un long discours. Prenons par exemple le cas où l’on souhaite masquer une zone de l’écran en fonction d’une valeur. L’affichage / masquage d’un contrôle, c’est le rôle de la propriété Visibility qui a la valeur Visible par défaut ; pour être invisible, il faut que la valeur soit à Collapsed :

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
    <StackPanel>
        <TextBlock Text="Je suis visible" Visibility="Collapsed" />
        <Button Content="Masquer/Afficher" Tap="Button_Tap" />
    </StackPanel>
</Grid>

Ces deux valeurs font partie de l'énumération Visibility.
Sauf que pour nous, il est beaucoup plus logique de travailler avec un booléen : s’il est vrai, le contrôle est visible, sinon il est invisible.

C’est là que va servir le converter, il va permettre de transformer true en Visible et false en Collapsed. Pour créer un tel converter, nous allons ajouter une nouvelle classe : VisibilityConverter. Cette classe doit implémenter l’interface IValueConverter qui force à implémenter une méthode de conversion de bool vers Visibility et inversement une méthode qui transforme Visibility en bool. Voici donc une telle classe :

public class VisibilityConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return (bool)value ? Visibility.Visible : Visibility.Collapsed;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        Visibility visibility = (Visibility)value;
        return (visibility == Visibility.Visible);
    }
}

Le code n’est pas très compliqué. La seule chose peut-être nouvelle pour certains est l’utilisation de l’opérateur ternaire « ? ». L’écriture suivante :

Visibility v = visibility ? Visibility.Visible : Visibility.Collapsed;

permet de remplacer :

Visibility v;
if (visibility)
    v = Visibility.Visible;
else
    v = Visibility.Collapsed;

Il évalue le booléen (ou la condition) à gauche du point d’interrogation. Si le résultat est vrai, alors il prend le premier opérande, sinon il prend le second, situé après les deux points.

Bref, une fois ceci fait, il va falloir déclarer le converter dans notre page XAML. Cela se fait dans les ressources et ici, je le mets dans les ressources de la page :

<phone:PhoneApplicationPage.Resources>
    <converter:VisibilityConverter x:Key="VisibilityConverter" />
</phone:PhoneApplicationPage.Resources>

Ce code doit être présent au même niveau que le conteneur de base de la page, c’est-à-dire à l’intérieur de la balise <phone:PhoneApplicationPage>.

Attention, la classe converter n’est pas connue de la page, il faut ajouter un espace de nom dans les propriétés de la page : xmlns:converter="clr-namespace:DemoPartie2"

Il ne reste plus qu’à créer une propriété pour le binding dans notre classe :

private bool textBlockVisible;
public bool TextBlockVisible
{
    get { return textBlockVisible; }
    set { NotifyPropertyChanged(ref textBlockVisible, value); }
}

Et à modifier sa valeur dans l’événement de clic sur le bouton :

private void Button_Tap(object sender, RoutedEventArgs e)
{
    TextBlockVisible = !TextBlockVisible;
}

Maintenant, nous devons indiquer le binding dans le XAML et que nous souhaitons utiliser un converter, cela se fait avec :

<TextBlock Text="Je suis visible" Visibility="{Binding TextBlockVisible,Converter={StaticResource VisibilityConverter}}" />

Nous lui indiquons ici que le converter est accessible en ressources par son nom : VisibilityConverter.
N’oublions pas de donner la valeur initiale du booléen, par exemple dans le constructeur, ainsi que d’alimenter le DataContext :

public MainPage()
{
    InitializeComponent();
    TextBlockVisible = false;
    DataContext = this;
}

Et voilà, le fait de changer la valeur du booléen influe bien sur la visibilité du contrôle, comme vous pouvez le constater à la figure suivante.

Le converter permet de modifier la visibilité du contrôle grâce à une liaison de données
Le converter permet de modifier la visibilité du contrôle grâce à une liaison de données

Il est assez courant d’écrire des converters pour se simplifier la tâche, par contre cela rajoute un temps de traitement qui en fonction des cas peut être important. Si c’est possible, préférez les cas où la donnée est correctement préparée dès le début, pensez que les smartphones n’ont pas autant de puissance que votre machine de développement…

  • La liaison de données, binding en anglais, est un élément fondamental des applications XAML et permet d’associer une source de données à un contrôle.

  • On utilise l’extention de balisage {Binding} pour déclarer une liaison de données depuis une propriété d’un contrôle dans le XAML.

  • Il est possible d’améliorer le mode design de ses pages grâce au binding.

  • L’ObservableCollection est une liste évoluée qui gère automatiquement les notifications en cas de changement dans la liste.

  • Les converters sont un mécanisme qui permet de transformer une donnée en une autre au moment du binding.

Exemple de certificat de réussite
Exemple de certificat de réussite