Le DataBinding est un moyen de lier une structure de données à des contrôles d’un formulaire sans avoir à se soucier des mises à jour effectuées par les 2 parties. Toutes les propriétés des contrôles peuvent être liées, mais traditionnellement on associe les propriétés Text ou Value.
Nous allons voir dans cet article les différents types de liaisons et les sources de données supportées par Windows Forms, puis nous continuerons avec un rapide aperçu des interfaces qui permettent au DataBinding de fonctionner. Pour finir, nous étudierons un cas pratique et mettrons en place un formulaire consommant une source de données personnalisée.
Il existe 2 types de DataBinding en Windows Forms :
Comme je l’ai souligné ci-dessus, Windows Forms est capable de lier de nombreux types de sources de données aux contrôles des formulaires.
Dans les cas les plus fréquents, il est possible d’utiliser la liaison simple en liant tout simplement la propriété publique de l’instance d’un objet à une propriété d’un contrôle. Tout cela s’effectue le plus simplement du monde en utilisant le type Binding.
Par exemple, il est possible de lier la propriété Name de l’instance d’une classe Person à une TextBox, aussi simplement que :
nameTextBox.DataBindings.Add("Text", person, "Name");
Il est aussi possible de lier une collection, une liste ou un tableau à un contrôle de type ListControl (ComboBox, ListBox…) ou DataGridView en utilisant le type BindingSource.
Le type BindingSource est le type de source de données le plus commun en Windows Forms. Il permet de transformer au moyen d’un proxy un type non supporté dans la liaison de données (comme IEnumerable) dans un type adapté (par exemple IList).
Nous pouvons ainsi associer une liste de Person à un ComboBox, en procédant comme ceci :
comboBox.DataSource = persons; comboBox.DisplayMember = "FullName"; comboBox.ValueMember = "Id";
A l’exécution, notre ComboBox sera automatiquement remplie, affichant en guise de libellé la propriété FullName et Id comme valeur.
Bien qu’il soit impossible de relier un contrôle avec un objet DbCommand ou DbDataReader, ADO .NET fourni un ensemble d’objets capablent de servir de source de données.
Ainsi la liaison d’un TextBox et d’une DataRow représente une liaison simple similaire à celle que nous avons vue ci-dessus. Une DataRow est la représentation d’une ligne d’une table d’une base de données avec ses colonnes et ses types.
Une DataTable peut-être liée à un contrôle de List comme une collection : elle représente après tout une collection de DataRow.
Par exemple :
dataGridView.DataSource = dataTable;
Dans ce cas, le DataGridView est lié à la DataTable, mais chaque ligne est liée à une DataRow.
Il est aussi possible d’utiliser une DataView, qui est en fait un sous-ensemble d’une DataTable pouvant être filtrée ou triée. Mais il faut savoir que vous créez une liaison avec une représentation figée à un instant t.
Enfin, vous avez la possibilité de créer une liaison simple ou complexe à un DataSet (collection de DataTable) mais cette fois il s’agit en réalité d’une liaison au DataViewManager par défaut. Cet objet est une vue personnalisée d’un DataSet tout entier, incluant les relations entre les DataTable et les DataView.
Le DataBinding est un mécanisme complexe interne à Windows Forms, il s’articule autour d’une série d’interfaces que nous allons décrire maintenant. Ces interfaces vont vous permettre de créer vos propres objets et d’interagir avec le gestionnaire de liaisons (CurrencyManager).
Il existe 2 types d’interfaces : les premières vont permettre aux auteurs de composants de consommer des données, les autres permettent aux développeurs de rendre leurs sources de données consommables.
Commençons tout d’abord par ce dernier groupe et voyons comment permettre à votre source de données d’être consommée par un composant.
Pour la création de composant, seules 2 interfaces sont disponibles :
Remarque :
Si votre composant hérite de Control, il n’est pas nécessaire d’implémenter l’interface IBindableComponent, et puisque la classe Control gère automatiquement ses liaisons à travers la propriété BindingContext, les cas dans lesquels vous devrez implémenter ICurrencyManagerProvider seront très rare.
Une fois le jeu de données lié à un composant BindingSource, il est très facile de naviguer dans celui-ci. En effet, BindingSource contient un mécanisme de navigation intégré et expose les méthodes MoveFirst, MovePrevious, MoveNext et MoveLast. Il est aussi aisé de connaitre la position courante avec la propriété Current ou bien de positionner le curseur avec Position. Enfin, vous pouvez rechercher un élément de votre source de données avec la méthode Find.
Maintenant que nous avons fait le tour des concepts liés au DataBinding avec Windows Forms, je vous propose d’étudier un cas pratique. Pour cela, nous allons commencer par construire une classe Person avec 4 propriétés : FirstName, LastName, Title et BirthDay.
public class Person
{
/// <summary>
/// Gets or sets the title.
/// </summary>
public Title Title { get; set; }
///<summary>
/// Gets or sets the last name.
///</summary>
/// The last name.
public string LastName { get; set; }
///<summary>
/// Gets or sets the first name.
///</summary>
public string FirstName { get; set; }
///<summary>
/// Gets or sets the birthday.
///</summary>
public DateTime Birthday { get; set; }
}
Cette classe très simple va nous permettre de stocker nos données tout au long de ce cas pratique.
Nous avons vu au paragraphe précédentqu’afin de notifier proprement les contrôles qui utilisent notre classe, nous devons implémenter l’interface INotifyPropertyChanged : en effet, cette interface oblige à exposer un évènement PropertyChanged qui va permettre à ces classes de savoir qu’une propriété a changé et surtout d’en connaître le nom.
Voici donc notre classe enrichie :
public class Person : INotifyPropertyChanged
{
private string _lastName;
private string _firstName;
private DateTime _birthDay;
private Title _title;
#region Properties
///<summary>
/// Gets or sets the title.
///</summary>
public Title Title
{
get { return _title; }
set
{
if (_title != value)
{
_title = value;
RaisePropertyChanged("Title");
}
}
}
///<summary>
/// Gets or sets the last name.
///</summary>
public string LastName
{
get { return _lastName; }
set
{
if (_lastName != value)
{
_lastName = value;
RaisePropertyChanged("LastName");
}
}
}
///<summary>
/// Gets or sets the first name.
///</summary>
public string FirstName
{
get { return _firstName; }
set
{
if (_firstName != value)
{
_firstName = value;
RaisePropertyChanged("FirstName");
}
}
}
///<summary>
/// Gets or sets the birth day.
///</summary>
public DateTime BirthDay
{
get { return _birthDay; }
set
{
if (_birthDay != value)
{
_birthDay = value;
RaisePropertyChanged("BirthDay");
}
}
}
#endregion
#region INotifyPropertyChanged
///<summary>
/// Occurs when a property value changes.
///</summary>
public event PropertyChangedEventHandler PropertyChanged;
///<summary>
/// Raises the property changed.
///</summary>
protected void RaisePropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
A présent, à chaque modification d’une propriété de notre classe, l’évènement PropertyChanged sera déclenché, cependant même s’il est possible de valider les données au moment de l’appel à la méthode RaisePropertyChanged, il n’est pas possible d’informer l’utilisateur que sa saisie n’est pas conforme ; pour cela, il existe l’interface IDataErrorInfo. Cette interface expose 2 propriétés : Error et Item, la propriété Item est une propriété spéciale permettant d’accéder à un élément particulier d’une collection, en C# elle est sous la forme d’un indexer (notée this[]). A chaque modification d’une propriété nous allons donc valider la valeur et en cas d’erreur ajouter le nom de celle-ci et le message dans un dictionnaire.
public class Person : INotifyPropertyChanged, IDataErrorInfo
{
private Dictionary _errorInfos;
private string _lastName;
private string _firstName;
private DateTime _birthDay;
private Title _title;
public Person()
{
_errorInfos = new Dictionary();
}
#region Properties
///<summary>
/// Gets or sets the last name.
///</summary>
public string LastName
{
get { return _lastName; }
set
{
if (_lastName != value)
{
_lastName = value;
RaisePropertyChanged("LastName", value);
}
}
}
[…]
#endregion
#region INotifyPropertyChanged
///<summary>
/// Occurs when a property value changes.
///</summary>
public event PropertyChangedEventHandler PropertyChanged;
///<summary>
/// Raises the property changed.
///</summary>
protected void RaisePropertyChanged(string propertyName, object value)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
ValidateProperty(propertyName, value);
}
#endregion
#region IDataErrorInfo
///<summary>
/// Gets an error message indicating what is wrong with this object.
/// An error message indicating what is wrong with this object. The default is an empty string ("").
///</summary>
[Browsable(false)]
public string Error
{
get
{
if (_errorInfos.Any())
{
return "Erreur lors de la saisie";
}
else
{
return string.Empty;
}
}
}
///<summary>
/// Gets the with the specified column name.
///</summary>
public string this[string columnName]
{
get
{
if (_errorInfos.ContainsKey(columnName))
{
return _errorInfos[columnName];
}
else
{
return string.Empty;
}
}
}
#endregion
#region Methods
///<summary>
/// Validates the property.
///</summary>
private void ValidateProperty(string propertyName, object value)
{
// on valide ici le contenu de la propriété que l'on vient de modifier !
switch (propertyName)
{
case "BirthDay":
var birthDay = (DateTime)value;
if (birthDay.Year < 1900 || birthDay.Year < DateTime.Now.Year)
{
SetError(propertyName, "Année de naissance incorrecte.");
}
break;
case "LastName":
case "FirstName":
var name = value as string;
if (string.IsNullOrWhiteSpace(name))
{
SetError(propertyName, "Ce champs ne peut-être vide.");
break;
}
if (Regex.IsMatch(name.Replace(" ", string.Empty), @"[\d\W]"))
{
SetError(propertyName, "Ce champs ne peut contenir que des lettres et des espaces.");
break;
}
break;
default:
SetError(propertyName, string.Empty);
break;
}
}
///
/// Sets the error.
///</summary>
private void SetError(string propertyName, string error)
{
_errorInfos.Remove(propertyName);
if (!string.IsNullOrWhiteSpace(error))
{
_errorInfos.Add(propertyName, error);
}
}
#endregion
}
A chaque appel de la méthode RaisePropertyChanged, la valeur est transmise à la méthode ValidateProperty afin d’être validée. Ici, nous vérifions que les noms ne contiennent aucun chiffre ou caractères non-alphabétiques ou bien que l’année de naissance soit supérieure à 1900 et inférieure ou égale à l’année courante. Dans le cas contraire un message est alors ajouté au dictionnaire errorInfos avec le nom de la propriété. Notez l’utilisation de l’attribut [Browsable(false)] qui permet de ne pas afficher la propriété Error lorsque l’objet est lié à un GridView.
Remarque :
Pour afficher facilement les erreurs dans un formulaire, il suffit d’ajouter le composant ErrorProvider et de lier à sa propriété DataSource la BindingSource courante.
Exemple :
this.errorProvider1.DataSource = this.personBindingSource;
Il est ainsi aisé d’obtenir ceci :
Une autre interface intéressante pour le DataBinding est IEditableObject, celle-ci fournit à la BindingSource 3 méthodes utiles aux 3 grandes étapes de la modification des données : BeginEdit, CancelEdit et EndEdit. Comme leur nom l’indique, elles seront appelées au début de la modification, à l’annulation et à la fin, vous permettant d’effectuer les actions adéquates.
Le DataBinding en Windows Forms est sûrement moins puissant qu’en WPF, Silverlight ou bien ASP.NET, mais il représente un véritable intérêt lorsque l’on souhaite construire des formulaires et en gérer simplement le contenu. Même si ça nécessite dans un premier temps un réel investissement dans la mise en œuvre, il est tout à fait possible de simplifier en vous créant une classe de base intégrant tous vos comportements par défaut et pourquoi pas l’ajout d’attributs.
Merci Sébastien pour ce très bon article !
Merci Stéphane
J’espère qu’on pourra se croiser prochainement !
Très bon article …. le data binding étant au coeur des avancés du framework 3.5 aussi bien pour les windows forms que pour les Web forms ! perso j’ai à cause de ton article j’ai voulu en savoir un peu plus.
je rajoute juste que çà aurait été cool d’avoir une suite de l’article qui montre une illustration du binding.par exemple étendre les exemples sur l’usage des interfaces de notification du style INotifyPropertyChanged pour montrer comment on peut s’en servir pour synchroniser les données du de la couche donnée avec l’affichage dans une formulaire par exemple
Merci de nouveau Guy pour ton commentaire.
Parmi les utilisations concrètes tu peux lire l’article sur l’utilisation de MVVM en Windows Forms.
Cependant, la synchronisation entre l’affichage et la couche de données sous-jacente est automatique avec le Binding, pas besoin d’ajouter de code.