Ce tutoriel se propose de donner un aperçu du binding par « vue » en WPF.
Nous disposons d’instances d’une classe Equipe
C#
using System.Collections.ObjectModel; using System.ComponentModel; namespace TestBinding { class Equipe : INotifyPropertyChanged { #region INotifyPropertyChanged public event PropertyChangedEventHandler PropertyChanged; private void GenerePropertyChanged(string Propriete) { if (this.PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(Propriete)); } #endregion private Personne entraineur; /// <summary> /// Entraineur de l'équipe /// </summary> public Personne Entraineur { get { return entraineur; } set { if (entraineur != value) { entraineur = value; GenerePropertyChanged("Entraineur"); } } } private string nom; /// <summary> /// Nom de l'équipe /// </summary> public string Nom { get { return nom; } set { if (nom != value) { nom = value; GenerePropertyChanged("Nom"); } } } private string sport; /// <summary> /// Sport pratiqué /// </summary> public string Sport { get { return sport; } set { if (sport != value) { sport = value; GenerePropertyChanged("Sport"); } } } private string niveau; /// <summary> /// Niveau de l'équipe /// </summary> public string Niveau { get { return niveau; } set { if (niveau != value) { niveau = value; GenerePropertyChanged("Niveau"); } } } /// <summary> /// Liste des joueurs /// </summary> public ObservableCollection<Personne> Joueurs { get; set; } = new ObservableCollection<Personne>(); } } using System.ComponentModel; namespace TestBinding { class Personne : INotifyPropertyChanged { #region INotifyPropertyChanged public event PropertyChangedEventHandler PropertyChanged; private void GenerePropertyChanged(string Propriete) { if (this.PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(Propriete)); } #endregion private string nom; /// <summary> /// Le nom de la personne /// </summary> public string Nom { get { return nom; } set { if (nom != value) { nom = value; GenerePropertyChanged("Nom"); } } } private string prenom; /// <summary> /// Le prénom de la personne /// </summary> public string Prenom { get { return prenom; } set { if (prenom != value) { prenom = value; GenerePropertyChanged("Prenom"); } } } private int numero; /// <summary> /// Numéro de téléphone de la personne /// </summary> public int Numero { get { return numero; } set { if (numero != value) { numero = value; GenerePropertyChanged("Numero"); } } } public Personne(string LePrenom, string LeNom, int LeNumero) { Nom = LeNom; Prenom = LePrenom; Numero = LeNumero; } } }
VB.Net
Imports System.Collections.ObjectModel Imports System.ComponentModel Public Class Equipe Implements INotifyPropertyChanged #Region "INotifyPropertyChanged" Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged Private Sub GenerePropertyChanged(ByVal Propriete As String) RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(Propriete)) End Sub #End Region Private lEntraineur As Personne ''' <summary> ''' Entraineur de l'équipe ''' </summary> Public Property Entraineur() As Personne Get Return lEntraineur End Get Set(ByVal value As Personne) If lEntraineur IsNot value Then lEntraineur = value GenerePropertyChanged("Entraineur") End If End Set End Property Private leNom As String ''' <summary> ''' Nom de l'équipe ''' </summary> Public Property Nom() As String Get Return leNom End Get Set(ByVal value As String) If leNom <> value Then leNom = value GenerePropertyChanged("Nom") End If End Set End Property Private leSport As String ''' <summary> ''' Sport pratiqué ''' </summary> Public Property Sport() As String Get Return leSport End Get Set(ByVal value As String) If leSport <> value Then leSport = value GenerePropertyChanged("Sport") End If End Set End Property Private leNiveau As String ''' <summary> ''' Niveau de l'équipe ''' </summary> Public Property Niveau() As String Get Return leNiveau End Get Set(ByVal value As String) If leNiveau <> value Then leNiveau = value GenerePropertyChanged("Niveau") End If End Set End Property ''' <summary> ''' Liste des joueurs ''' </summary> Public Property Joueurs() As ObservableCollection(Of Personne) = New ObservableCollection(Of Personne)() End Class Imports System.ComponentModel Public Class Personne Implements INotifyPropertyChanged #Region "INotifyPropertyChanged" Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged Private Sub GenerePropertyChanged(ByVal Propriete As String) RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(Propriete)) End Sub #End Region Private leNom As String ''' <summary> ''' Le nom de la personne ''' </summary> Public Property Nom() As String Get Return leNom End Get Set(ByVal value As String) If leNom <> value Then leNom = value GenerePropertyChanged("Nom") End If End Set End Property Private lePrenom As String ''' <summary> ''' Le prénom de la personne ''' </summary> Public Property Prenom() As String Get Return lePrenom End Get Set(ByVal value As String) If lePrenom <> value Then lePrenom = value GenerePropertyChanged("Prenom") End If End Set End Property Private leNumero As Integer ''' <summary> ''' Numéro de téléphone de la personne ''' </summary> Public Property Numero() As Integer Get Return leNumero End Get Set(ByVal value As Integer) If leNumero <> value Then leNumero = value GenerePropertyChanged("Numero") End If End Set End Property Public Sub New(ByVal LePrenom As String, ByVal LeNom As String, ByVal LeNumero As Integer) Nom = LeNom Prenom = LePrenom Numero = LeNumero End Sub End Class
Et nous souhaitons afficher une instance dans cette fenêtre
<Window x:Class="TestBinding.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:TestBinding" mc:Ignorable="d" Title="FenetreEquipe" Height="265.546" Width="437.815"> <Grid> <GroupBox Header="Général" HorizontalAlignment="Left" Height="122" VerticalAlignment="Top" Width="198" Margin="10,10,0,0"> <Grid> <Label Content="Nom équipe" HorizontalAlignment="Left" Height="25" Margin="4,11,0,0" VerticalAlignment="Top" Width="80"/> <TextBox HorizontalAlignment="Left" Height="25" Margin="84,11,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="100"/> <Label Content="Sport" HorizontalAlignment="Left" Height="25" Margin="4,41,0,0" VerticalAlignment="Top" Width="80"/> <TextBox HorizontalAlignment="Left" Height="25" Margin="84,43,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="100"/> <Label Content="Niveau" HorizontalAlignment="Left" Height="25" Margin="4,73,0,0" VerticalAlignment="Top" Width="80"/> <TextBox HorizontalAlignment="Left" Height="25" Margin="84,73,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="100"/> </Grid> </GroupBox> <GroupBox Header="Entraineur" HorizontalAlignment="Left" Height="92" VerticalAlignment="Top" Width="198" Margin="10,137,0,0"> <Grid> <Label Content="Nom" HorizontalAlignment="Left" Height="25" Margin="4,11,0,0" VerticalAlignment="Top" Width="80"/> <TextBox HorizontalAlignment="Left" Height="25" Margin="84,11,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="100"/> <Label Content="Prénom" HorizontalAlignment="Left" Height="25" Margin="4,41,0,0" VerticalAlignment="Top" Width="80"/> <TextBox HorizontalAlignment="Left" Height="25" Margin="84,43,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="100"/> </Grid> </GroupBox> <GroupBox Header="Joueurs" HorizontalAlignment="Left" Height="219" Margin="213,10,0,0" VerticalAlignment="Top" Width="207"> <ListBox HorizontalAlignment="Left" Height="185" Margin="4,7,0,0" VerticalAlignment="Top" Width="187"/> </GroupBox> </Grid> </Window>
A noter, en WPF un GroupBox ne peut contenir qu’un seul contrôle, il faut donc y placer un conteneur (ici un Grid) qui sera chargé de disposer les contrôles enfants.
Nous pourrions donner un nom à chaque contrôle, puis affecter une à une les propriétés de l’équipe au contrôle adéquat.
Nous pourrions aussi affecter la liste de joueurs à l’ItemsSource de la ListBox.
Mais ce serait passer à coté d’un des points forts de WPF.
Le principe d’une vue est qu’une instance regroupe toutes les données nécessaires à l’affichage d’une fenêtre.
Cette vue est donnée « à manger » à la fenêtre en une seule fois, sans se soucier de conversion de type (au moins pour les types de base).
Pour l’exemple, il nous faut une instance d’Equipe, et binder cette instance à la fenêtre
C#
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); this.DataContext = InitDatas(); } /// <summary> /// Crée une équipe pour l'exemple /// </summary> /// <returns></returns> private Equipe InitDatas() { Equipe equipe = new Equipe(); equipe.Nom = "Les champions"; equipe.Niveau = "Top 14"; equipe.Sport = "Rugby"; equipe.Entraineur = new Personne("Gérard", "Manvu", 0101010101); equipe.Joueurs.Add(new Personne("Alain", "Di", 0202020202)); equipe.Joueurs.Add(new Personne("Jean","Sort", 0303030303)); equipe.Joueurs.Add(new Personne("Jérémie", "Lecouvert", 0404040404)); equipe.Joueurs.Add(new Personne("Paul", "Ochon", 0505050505)); equipe.Joueurs.Add(equipe.Entraineur); return equipe; } }
VB.Net
Partial Public Class MainWindow Inherits Window Public Sub New() InitializeComponent() Me.DataContext = InitDatas() End Sub ''' <summary> ''' Crée une équipe pour l'exemple ''' </summary> ''' <returns></returns> Private Function InitDatas() As Equipe Dim equipe As New Equipe() equipe.Nom = "Les champions" equipe.Niveau = "Top 14" equipe.Sport = "Rugby" equipe.Entraineur = New Personne("Gérard", "Manvu", 0101010101) equipe.Joueurs.Add(New Personne("Alain", "Di", 0202020202)) equipe.Joueurs.Add(New Personne("Jean","Sort", 0303030303)) equipe.Joueurs.Add(New Personne("Jérémie", "Lecouvert", 0404040404)) equipe.Joueurs.Add(New Personne("Paul", "Ochon", 0505050505)) equipe.Joueurs.Add(equipe.Entraineur) Return equipe End Function End Class
Et c’est tout.
Il faut maintenant décrire dans le xaml comment afficher tout cela.
Tout d’abord, puisque l’Equipe est bindée sur le DataContext de la fenêtre, il faut que celui-ci soit averti que ses données sont issues du binding
<Window x:Class="TestBinding.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:TestBinding" mc:Ignorable="d" Title="MainWindow" Height="265" Width="437" DataContext="{Binding}"><!--C'est ici-->
Ensuite, il faut que les contrôles enfants aient accès au même contexte que leurs parents (le premier Grid, le GroupBox contenant les informations générales etc…)
<Grid DataContext="{Binding .}"><!--C'est ici--> <GroupBox Header="Général" HorizontalAlignment="Left" Height="122" VerticalAlignment="Top" Width="198" Margin="10,10,0,0" DataContext="{Binding .}"><!--Là--> <Grid DataContext="{Binding .}"><!--et là--> <Label Content="Nom équipe" HorizontalAlignment="Left" Height="25" Margin="4,11,0,0" VerticalAlignment="Top" Width="80"/> <TextBox HorizontalAlignment="Left" Height="25" Margin="84,11,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="100" Text="{Binding Nom}"/> <Label Content="Sport" HorizontalAlignment="Left" Height="25" Margin="4,41,0,0" VerticalAlignment="Top" Width="80"/> <TextBox HorizontalAlignment="Left" Height="25" Margin="84,43,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="100" Text="{Binding Sport}"/> <Label Content="Niveau" HorizontalAlignment="Left" Height="25" Margin="4,73,0,0" VerticalAlignment="Top" Width="80"/> <TextBox HorizontalAlignment="Left" Height="25" Margin="84,73,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="100" Text="{Binding Niveau}"/> </Grid>
<Grid DataContext="{Binding .}">
ça veut dire : « Comme au-dessus »
Le GroupBox entraineur, et surtout ses contrôles enfants, ont besoin d’avoir accès à la propriété Entraineur
<GroupBox Header="Entraineur" HorizontalAlignment="Left" Height="92" VerticalAlignment="Top" Width="198" Margin="10,137,0,0" DataContext="{Binding Entraineur}"><!--C'est plus un point--> <Grid> <Label Content="Nom" HorizontalAlignment="Left" Height="25" Margin="4,11,0,0" VerticalAlignment="Top" Width="80"/> <TextBox HorizontalAlignment="Left" Height="25" Margin="84,11,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="100" Text="{Binding Nom}"/> <Label Content="Prénom" HorizontalAlignment="Left" Height="25" Margin="4,41,0,0" VerticalAlignment="Top" Width="80"/> <TextBox HorizontalAlignment="Left" Height="25" Margin="84,43,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="100" Text="{Binding Prenom}"/> </Grid> </GroupBox>
Pour la ListBox, ça se complique un peu. Nous voulons sur une ligne voir le prénom et le nom d’un joueur.
Il faut définir cet affichage. Il peut être décrit au niveau de la fenêtre, dans un autre fichier ou directement au niveau de la ListBox
<GroupBox Header="Joueurs" HorizontalAlignment="Left" Height="219" Margin="213,10,0,0" VerticalAlignment="Top" Width="207" DataContext="{Binding .}"><!--ici c'est un point--> <ListBox HorizontalAlignment="Left" Height="185" Margin="4,7,0,0" VerticalAlignment="Top" Width="187" ItemsSource="{Binding Joueurs}"><!--là on binde l'itemsSource--> <ListBox.ItemTemplate><!--Et là on décrit le modèle d'affichage d'un joueur--> <DataTemplate> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Prenom}" Margin="2"/> <TextBlock Text="{Binding Nom}" Margin="2"/> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </GroupBox>
Jusqu’ici aucun contrôle n’a de nom.
Et pourtant, tout s’affiche bien.
Maintenant, on souhaite afficher le numéro de téléphone du joueur sélectionné.
Une petite retouche du xaml, il faut donner un nom à la ListBox, pour pouvoir la référencer auprès de l’affichage du numéro.
<GroupBox Header="Joueurs" HorizontalAlignment="Left" Height="214" Margin="213,10,0,0" VerticalAlignment="Top" Width="207" DataContext="{Binding .}"><!--ici c'est un point--> <Grid> <ListBox Name="lstJoueurs" HorizontalAlignment="Left" Height="141" Margin="4,7,0,0" VerticalAlignment="Top" Width="187" ItemsSource="{Binding Joueurs}"><!--là on binde l'itemsSource--> <ListBox.ItemTemplate><!--Et là on décrit le modèle d'affichage d'un joueur--> <DataTemplate> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Prenom}" Margin="2"/> <TextBlock Text="{Binding Nom}" Margin="2"/> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> <Label Content="Téléphone" HorizontalAlignment="Left" Height="29" Margin="10,153,0,0" VerticalAlignment="Top" Width="64"/> <TextBox HorizontalAlignment="Left" Height="29" Margin="79,153,0,0" TextWrapping="Wrap" DataContext="{Binding ElementName=lstJoueurs, Path=SelectedItem}" Text="{Binding Numero}" VerticalAlignment="Top" Width="106"/><!--le dataconext est bindé sur la listBox--> </Grid> </GroupBox>
Mais le zéro de devant a disparu!
Et oui, car la propriété est un int, les zéro à gauche sont considérés comme« inutiles ».
Vous le vouliez ce zéro? il suffit de formater
Text="{Binding Numero, StringFormat={}{0:D10}}"
comme on ferait avec string.Format.
Et que diriez-vous d’une infobulle qui affiche la date de naissance du joueur? Cela nécessite d’ajouter une propriété DateTime à la classe et de la renseigner (je vous passe le code…)
<ListBox.ItemTemplate><!--Et là on décrit le modèle d'affichage d'un joueur--> <DataTemplate> <StackPanel Orientation="Horizontal" ToolTip="{Binding Naissance}"><!--rien de plus, pas de conversion--> <TextBlock Text="{Binding Prenom}" Margin="2"/> <TextBlock Text="{Binding Nom}" Margin="2"/> </StackPanel> </DataTemplate> </ListBox.ItemTemplate>
Le format par défaut est à l’américaine, mais vous avez vu plus haut qu’il est facile de choisir son format d’affichage.
Maintenant, nous souhaitons que les prénoms des joueurs soient affichés en différentes couleurs selon leur âge:
Nous ajoutons une propriété Age à la classe Personne (là aussi, je vous passe le code).
A noter, pour les besoins de l’article, le projet utilise l’année 2018 pour le calcul de l’âge
Il faut ajouter un trigger de données à la TextBox correspondante
<TextBlock Text="{Binding Prenom}" Margin="2"> <TextBlock.Style> <Style TargetType="TextBlock"> <Style.Triggers> <DataTrigger Binding="{Binding Age}" Value="8"> <Setter Property="Foreground" Value="Blue"/> </DataTrigger> <DataTrigger Binding="{Binding Age}" Value="10"> <Setter Property="Foreground" Value="Green"/> </DataTrigger> <DataTrigger Binding="{Binding Age}" Value="28"> <Setter Property="Foreground" Value="Red"/> </DataTrigger> </Style.Triggers> </Style> </TextBlock.Style> </TextBlock>
On peut trigger à partir
Les exemples sont légions sur le Net.
Par contre, on ne peut utiliser qu’une valeur fixe: on ne peut pas écrire
Value < "9"
par exemple.
Il faut utiliser un convertisseur personnalisé.
Imaginons maintenant que la GroupBox des joueurs doit avoir un fond à la couleur de l’équipe et que cette couleur est saisie en string dans la classe.
Il nous faut donc un convertisseur
C#
public class ConvertisseurStringVersCouleur : IValueConverter { /// <summary> /// Décrit la conversion entre la donnée et le type attendu par la propriété /// Ici un texte donne la couleur, et le controle attend un SolidColorBrush /// </summary> /// <param name="value">Valeur de la propriété</param> /// <param name="targetType">Type attendu</param> /// <param name="parameter">Paramètre (s'il y en a)</param> /// <param name="culture">Culture à utliser</param> /// <remarks>dans notre cas seul value est utilisée, mais souvent tous les paramètres sont utiles</remarks> /// <returns></returns> public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { string couleurStringEnMinuscule = ((string)value).ToLower(); switch (couleurStringEnMinuscule) { case "bleu": return new SolidColorBrush(Colors.LightBlue); case "rouge": return new SolidColorBrush(Colors.Red); default: return new SolidColorBrush(Colors.Transparent); } } /// <summary> /// Décrit si besoin la conversion inverse. /// </summary> /// <param name="value"></param> /// <param name="targetType"></param> /// <param name="parameter"></param> /// <param name="culture"></param> /// <returns></returns> public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } }
VB.Net
Imports System.Globalization Public Class ConvertisseurStringVersCouleur Implements IValueConverter ''' <summary> ''' Décrit la conversion entre la donnée et le type attendu par la propriété ''' Ici un texte donne la couleur, et le controle attend un SolidColorBrush ''' </summary> ''' <param name="value">Valeur de la propriété</param> ''' <param name="targetType">Type attendu</param> ''' <param name="parameter">Paramètre (s'il y en a)</param> ''' <param name="culture">Culture à utliser</param> ''' <remarks>dans notre cas seul value est utilisée, mais souvent tous les paramètres sont utiles</remarks> ''' <returns></returns> Public Function Convert(ByVal value As Object, ByVal targetType As Type, ByVal parameter As Object, ByVal culture As CultureInfo) As Object Implements IValueConverter.Convert Dim couleurStringEnMinuscule As String = (CStr(value)).ToLower() Select Case couleurStringEnMinuscule Case "bleu" Return New SolidColorBrush(Colors.LightBlue) Case "rouge" Return New SolidColorBrush(Colors.Red) Case Else Return New SolidColorBrush(Colors.Transparent) End Select End Function Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack Throw New NotImplementedException() End Function End Class
A noter, ce convertisseur traduit de string vers SolidColorBrush mais pas dans l’autre sens.
Et il doit être référencé et utilisé dans le xaml.
<Window x:Class="TestBinding.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:TestBinding"> <!--si le namespace local n'existe pas on l'ajoute-->
Puis on référence une ressource à la fenêtre
<Window x:Class="TestBinding.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:TestBinding" mc:Ignorable="d" Title="MainWindow" Height="265" Width="437" DataContext="{Binding}"><!--C'est ici--> <Window.Resources> <local:ConvertisseurStringVersCouleur x:Key="monConvertisseur"/> </Window.Resources>
Et enfin, on affecte la couleur au GroupBox
<GroupBox Header="Joueurs" HorizontalAlignment="Left" Height="214" Margin="213,10,0,0" VerticalAlignment="Top" Width="207" DataContext="{Binding .}" Background="{Binding Couleur, Converter={StaticResource monConvertisseur}}"><!--Le BackGround fait appel à notre convertisseur-->
Un seul contrôle a dû être nommé.
Pas besoin de gérer les conversions de int et de DateTime, cependant on peut personnaliser le format.
Tant que l’interface est simple, tout se gère au niveau du xaml, ce qui implique qu’un designer n’a pas forcément besoin d’apprendre le C# ou VB.Net. Bien sûr du code peut être nécessaire si l’on doit écrire un convertisseur de type personnalisé, mais cela peut être réalisé par le codeur behind.
A l’inverse, il existe des outils permettant de créer facilement une interface XAML: Blend, Kaxml, etc
Vous pouvez télécharger les projets C# et VB.Net ici
Merci à VB95 et JMO pour leurs corrections et commentaires.