[Windows Forms] – A la découverte du DataBinding
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.
Types de liaisons de données
Il existe 2 types de DataBinding en Windows Forms :
- La liaison de données simple : le contrôle est lié à un seul élément de données, tel qu’une valeur dans une colonne de table d’un groupe de données. C’est généralement le cas des contrôles TextBox et Label qui n’affichent qu’une seule valeur.
- La liaison de données complexe : le contrôle est lié à plusieurs enregistrements dans une base de données. Parmi les contrôles qui prennent en charge la liaison complexe, on trouve le DataGridView, ListBox et ComboBox.
Sources de données supportées
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.
Liaisons sur les objets simples
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");
Listes, tableaux et collections
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.
ADO.NET
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.
Mécanisme du DataBinding
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.
Interfaces pour l’implémentation de sources de données
- IList est l’interface la plus simple à utiliser : il s’agit de listes indexées d’Object. Typiquement les objets implémentant cette interface sont Array, ArrayList et CollectionBase, soit la grande majorité des interfaces non génériques du framework. Cependant, ces collections ne doivent contenir que des types homogènes : c’est-à-dire dérivant ou implémentant la même classe ou la même interface, car c’est le premier élément de la liste qui déterminera le type de la liaison.
- L’interface IBindingList offre en plus de IList des fonctionnalités de liaison de données comme le tri et la notification en cas de modification d’un élément distinct ou de la liste complète. Cette dernière fonctionnalité est très importante lorsque vous souhaitez mettre à jour un autre contrôle ou une autre classe suite à la modification d’un élément de votre liste.
- L’interface IBindingListView reprend les fonctionnalités de IBindingList en améliorant les filtres et les tris ; il est ainsi possible de trier sur plusieurs colonnes ou bien de filtrer par rapport à une chaine.
- L’interface IEnumerable est surtout utilisée avec ASP .NET mais peut être utilisée en Windows Forms avec le composant BindingSource.
- IEditableObject est une interface vous permettant de déterminer quand les modifications deviennent permanentes et offre 3 méthodes BeginEdit, EndEdit et CancelEdit dont les noms parlent d’eux-mêmes ! On pourra, par exemple, grâce à cette interface valider l’intégrité de chaque objet avant qu’il ne soit persisté.
- ICancelAddNew permet d’annuler le dernier ajout fait avec la méthode AddNew. Normalement tout objet implémentant IBindingList doit impérativement implémenter cette interface.
- ITypedList permet aux listes l’implémentant de contrôler quelles seront les propriétés des objets qui seront exposées et leur ordre.
- Les classes peuvent aussi implémenter l’interface ICustomTypeDescriptor pour fournir des informations dynamiques sur elles-mêmes. Contrairement à ITypedList qui s’applique à une liste, ICustomTypeDescriptor concerne les objets. Cette interface est utilisée par DataRowView pour déterminer le schéma des lignes sous-jacentes.
- Pour les objets n’implémentant pas IList et pour lesquels on souhaite activer la liaison basée sur les listes, il est possible d’utiliser l’interface IListSource. La méthode GetList permet de retourner une liste pouvant être liée. C’est l’interface utilisée par la classe DataSet.
- IRaiseItemChangedEvents s’applique aux listes qui implémentent également IBindingList pour déterminer si ces listes déclencheront l’évènement ListChanged de type ItemChanged via sa propriété RaisesItemChangedEvents.
- INotifyPropertyChanged permet aux classes l’implémentant de déclencher un évènement à chaque fois qu’une propriété est modifiée. On évite ainsi d’avoir à créer un évènement distinct pour chaque propriété, sous la forme propertyNameChanged. A noter qu’un objet utilisé dans une BindingList doit implémenter cette interface afin que la collection convertisse les évènements PropertyChanged en ListChanged de type ItemChanged.
- IDataErrorInfo n’est pas liée spécifiquement au DataBinding mais, de par sa nature informative, permet de personnaliser les messages d’erreurs liés aux propriétés.
Interfaces pour la création de composants consommateurs de données
Pour la création de composant, seules 2 interfaces sont disponibles :
- IBindableComponent permet de créer des composants non visuels qui prennent en charge la liaison de données. A travers les propriétés DataBindings et BindingContext, cette classe retourne les liaisons et le contexte du composant.
- ICurrencyManagerProvider permet aux composants de fournir leur propre CurrencyManager pour gérer ses liaisons de données. L’accès au CurrencyManager personnalisé se fait via la propriété CurrencyManager de l’objet.
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.
Navigation au sein des données
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.
Cas pratique
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.
Pour conclure
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.