Windows Forms - de la liaison de données à la liaison d'objets

Partie 2 : des liaisons sur les composants

Olivier Saint-Ouen

Lorsque l'on souhaite faire des interfaces utilisateurs centrées sur les liaisons (binding) en Windows Forms, une des frustrations auxquelles nous devons faire face est que seuls les contrôles supportent les liaisons.

Dans cet article, je vais vous montrer comment résoudre ce problème et le tout en étant pleinement compatible avec tous les composants existants.

Commentez

- Titre : Windows Forms - de la liaison de données à la liaison d'objets

- Auteur : Olivier Saint-Ouen

- Parution : 2 décembre 2010

- Mise à jour : 27 février 2020

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2010 Saint-Ouen Olivier. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Les contrôles et les liaisons

- II. Comment combler ce manque ?

- II-A. Nouveaux composants

- II-B. Composants existants

- II-B-1. Comment implémenter IBindableComponent sur des composants existants ?

- II-B-2. La réponse en image : le principe de fonctionnement

- II-B-3. Le Pont : mise en situation et intervenants

- III. Le tour de magie en pratique

- III-A. Les extensions : le DataBindingsProvider

- III-B. L'adaptation : le BindableAdapter

- III-C. La description : le Pont

- III-C-1. L'interface du Pont : IBridge

- III-C-2. Le fournisseur de descripteur de Pont : BridgeDescriptionProvider

- III-C-3. Le descripteur de Pont : BridgeDescriptor

- III-C-4. Le descripteur de propriété du Pont : BridgePropertyDescriptor

- IV. Démonstration

- V. Conclusion

- Remerciements

- Contact et code source

I - Les contrôles et les liaisons

Comme les contrôles supportent les liaisons, commençons par analyser comment ils implémentent cette fonctionnalité.

L'explorateur d'objets va nous renseigner sur ce point, il nous indique que la classe Control hérite de ou implémente ce qui suit :

On observe tout de suite une interface au nom explicite : IBindableComponent.
MSDN nous indique à son propos : « Permet à un composant qui n'est pas un contrôle d'émuler le comportement de liaison de données d'un contrôle Windows Forms. »

Exactement ce que l'on souhaite faire.

Cette interface expose deux propriétés particulièrement intéressantes en ce qui nous concerne :

- BindingContext de type BindingContext ;

- DataBindings de type ControlBindingsCollection.

DataBindings est justement la propriété que l'on déroule dans la page de propriétés pour obtenir l'éditeur de liaison.
On a trouvé ce qu'il manque à nos composants.

II - Comment combler ce manque ?

II-A - Nouveaux composants

Dans le cas d'un nouveau composant, aucune difficulté, il suffit d'implémenter cette interface et ce composant supportera les liaisons.

II-B - Composants existants

Dans notre problématique, on souhaite ajouter cette interface sur des composants existants.
En effet je me vois mal hériter de tous les composants des Windows Forms pour leur faire implémenter cette interface.

Ce serait inélégant et surtout le point de départ d'une vraie catastrophe. Imaginez un peu, une arborescence complète de composants à chaque nouvelle fonctionnalité !
Complètement ingérable comme solution.

II-B-1 - Comment implémenter IBindableComponent sur des composants existants ?

En réalité, ce n'est pas nécessaire.
Il suffit de faire croire aux liaisons de données que l'objet auquel elles sont reliées implémente cette interface.

Enfin, « suffit », c'est vite dit, c'est un peu plus complexe que d'implémenter l'interface en question.

Pour pouvoir réaliser ce petit tour de magie, plusieurs concepts et fonctionnalités interviennent :

- la capacité d'extension des Windows Forms pour ajouter la ou les propriétés manquantes aux composants existants ;

- le patron de conception Bridge ou Pont en français ;

- le patron de conception Adapter ou Adaptateur en français ;

- le modèle objet utilisé par le moteur de liaison afin d'implémenter les deux patrons de conception ci-dessus.

La capacité d'extension des Windows Forms est représentée par les objets IExtenderProvider et ProvidePropertyAttribute.
MSDN nous dit à propos de IExtenderProvider : "Définit l'interface pour étendre les propriétés à d'autres composants dans un conteneur."

Le but d'un Pont est de séparer l'aspect d'implémentation d'un objet de son aspect de représentation et d'interface.
Ainsi, d'une part l'implémentation peut être totalement encapsulée et d'autre part l'implémentation et la représentation peuvent évoluer indépendamment et sans que l'une exerce une contrainte sur l'autre.

Le but d'un Adaptateur est de convertir l'interface d'une classe existante en l'interface attendue par des clients également existants afin qu'ils puissent travailler ensemble.
Il s'agit de conférer à une classe existante une nouvelle interface pour répondre aux besoins des clients.

Le modèle objet utilisé par le moteur de liaison est exposé dans l'espace de nom System.ComponentModel.
Ce sujet est vaste, je me contente d'une explication succincte ici.
Le moteur de liaison n'accède pas au modèle objet directement par la réflexion et les objets type et memberInfo, mais par une vue sur ce modèle.
Vue qui est constituée principalement par les objets TypeDescriptor et MemberDescriptor.

Aller plus loin avec les patrons de conception :

- Souplesse et modularité grâce aux Design Patterns ;

- Comprendre le Design Pattern Adaptateur ;

- Le forum de Développez.com.

Les définitions des patrons de conception ci-dessus sont tirées du livre : Design Patterns Pour Java.

II-B-2 - La réponse en image : le principe de fonctionnement

Le principe de fonctionnement.

Ce schéma nous permet de mieux visualiser le processus d'adaptation.
Par contre, la fonction du Pont reste encore floue.

II-B-3 - Le Pont : mise en situation et intervenants

On constate que la fonction première du Pont est de dérouter le processus de description sur l'instance réelle exposée par le Pont.

L'interface du Pont prend ici tout son intérêt ; en effet, redéfinir toute la logique de description peut être un travail long et pénible.
En implémentant ce concept de façon autonome, on rend sa réutilisation et sa compréhension plus faciles.

Tous les éléments du Pont qui forment la colonne centrale du schéma spécialisent par héritage des objets du modèle de description :

- le Fournisseur de descripteur de Pont hérite de TypeDescriptionProvider ;

- le Descripteur de Pont hérite de CustomTypeDescriptor ;

- le Descripteur de propriété du Pont hérite de PropertyDescriptor ;

- le Convertisseur de type hérite de TypeConverter.

III - Le tour de magie en pratique

La solution envisagée peut paraître complexe à première vue.
Dans la pratique, comme vous allez pouvoir le constater, il n'en est rien : le code des objets est simple, voire extrêmement simple.
Le seul point vraiment indispensable à la compréhension du code, est l'astuce utilisée par l'Adaptateur.
Astuce qui est marquée par l'image adéquate dans l'explication du code de l'Adaptateur.

J'entends déjà des personnes s'écrier : « Mais il a la paternité aiguë celui-là ! »
Il est vrai que dans cet article j'utilise deux patrons de conception.
Il est vrai aussi que l'utilisation du Pont peut paraître superflue.

En ce qui me concerne, il est surtout vrai que sans le Pont, l'article et le code auraient été beaucoup plus compliqués à comprendre.
Concernant l'Adaptateur, c'est le meilleur cas d'utilisation que j'aie jamais vu en DotNet ; donc la question ne se pose même pas.
Je m'explique : nous allons ajouter une fonctionnalité du Framework à des objets du Framework, sans modifier pour autant ni l'un ni l'autre.
On se rapproche fortement du cas d'école ! Non ? Un cas d'école utile qui plus est.

III-A - Les extensions : le DataBindingsProvider

L'objectif de ce composant est d'ajouter la propriété DataBindings aux composants existants.
Cette propriété doit fournir le même fonctionnement que la propriété DataBindings des contrôles.
Il correspond au fournisseur d'extensions du premier schéma.

Voici le code de ce composant concernant la gestion de cette propriété :

<ProvideProperty("DataBindings", GetType(IComponent))>
Public Class DataBindingsProvider
Implements IExtenderProvider

Public Function CanExtend(ByVal extendee As Object) As Boolean Implements IExtenderProvider.CanExtend
If extendee Is Nothing Then Return False
If TypeOf extendee Is IBindableComponent Then Return False
Return TypeOf extendee Is IComponent
End Function

<Category("Data")>
<RefreshProperties(RefreshProperties.All)>
<ParenthesizePropertyName(True)>
Public Function GetDataBindings(ByVal component As IComponent) As ControlBindingsCollection
End Function

Public Sub SetDataBindings(ByVal component As IComponent, ByVal bindings As ControlBindingsCollection)
End Sub

End Class

Dans l'ordre de lecture :

- on déclare que l'on fournit la propriété DataBindings pour tous les objets IComponent via l'attribut ProvideProperty ;

- on implémente l'interface IExtenderProvider en filtrant les types étendus via la méthode CanExtend (en plus de la validation du type attendu, on précise que l'on n'étend pas les objets qui implémentent déjà l'interface IBindableComponent) ;

- puis on fournit le code pour les accesseurs de la propriété.

C'est tout concernant l'ajout d'une propriété à un composant visuel par extension.
La logique de stockage des valeurs de propriétés n'a pas d'importance ici. Il s'agit d'un simple stockage par dictionnaire.
Pour aller plus loin sur ce sujet : MSDN.

La logique succincte de fonctionnement est la suivante :

- lors de l'extension d'un objet, le concepteur appelle la méthode GetDataBindings sur DataBindingsProvider ;

- la méthode GetDataBindings demande la valeur de la propriété à l'Adaptateur, si nécessaire, la stocke et renvoie cette valeur ;

Dans le code fourni, la ligne correspondant à l'utilisation de l'adaptateur est la suivante :

ComponentBindings = New BindableAdapter(component).DataBindings

- la logique de persistance des valeurs des propriétés pourrait être faite en utilisant la logique par défaut.
Dans la preuve de faisabilité, j'ai préféré utiliser un sérialiser spécifique qui permet d'obtenir le code suivant :

Me.DataBindingsProvider1.AddBinding(Me.ToolStripButton1, New System.Windows.Forms.Binding(
"Enabled", Me.BindingSource, "Command1Enabled", True,
System.Windows.Forms.DataSourceUpdateMode.OnPropertyChanged))

III-B - L'adaptation : le BindableAdapter

L'objectif ici est de fournir l'implémentation de l'interface IBindableComponent au composant adapté.

Cet objet est nommé Adaptateur de liaison dans le premier schéma et Adaptateur dans le second.

Voici son code complet :

<TypeDescriptionProvider(GetType(BridgeDescriptionProvider))>
Public Class BindableAdapter
Inherits Component
Implements IBindableComponent
Implements IBridge

Private _Bindings As ControlBindingsCollection
Private _Context As BindingContext
Private ReadOnly _RealInstance As Object

Private Sub New()
End Sub
Public Sub New(ByVal realInstance As Object)
If realInstance Is Nothing Then Throw New ArgumentNullException("realInstance")
Me._RealInstance = realInstance
End Sub

Public Property BindingContext As BindingContext Implements IBindableComponent.BindingContext
Get
If Me._Context Is Nothing Then Me._Context = New BindingContext
Return Me._Context
End Get
Set(ByVal value As BindingContext)
Me._Context = value
End Set
End Property

Public ReadOnly Property DataBindings As ControlBindingsCollection Implements IBindableComponent.DataBindings
Get
If Me._Bindings Is Nothing Then Me._Bindings = New ControlBindingsCollection(Me)
Return Me._Bindings
End Get
End Property

Public Function GetRealInstance() As Object Implements IBridge.GetRealInstance
Return Me._RealInstance
End Function

End Class

Les éléments nécessaires au bon fonctionnement des liaisons sont l'implémentation de l'interface IBindableComponent et l'héritage de Component.

L'implémentation de IBindableComponent est simple, comme on le voit dans le code, les valeurs sont créées et fournies à la demande.

Ensuite, l'Adaptateur utilise la logique du Pont afin de se faire passer pour le composant adapté au niveau du modèle de description et donc des liaisons.

Cette logique correspond à :

1. la déclaration du Fournisseur de description du Pont via l'attribut TypeDescriptionProvider ;

2. l'implémentation de l'interface IBridge, qui est la condition sine qua non pour l'utilisation du Pont tel qu'il est conçu ici.

L'astuce est la suivante :
La ControlBindingsCollection est reliée via son constructeur à l'Adaptateur et non à l'objet adapté.
Donc, quand le moteur de liaison cherche à résoudre les noms des propriétés, il utilise le descripteur de type fourni par l'Adaptateur qui est celui du pont, et qui va rediriger cette résolution sur l'instance réelle exposée par le Pont.
Instance qui correspond à l'adapté, c'est-à-dire, le composant étendu avec le support des liaisons.

III-C - La description : le Pont

Le Pont est la partie la plus complexe de cet article.
Il demande une bonne connaissance du modèle objet utilisé par les liaisons.

Son but est le suivant : permettre à un objet de se faire passer pour un autre au niveau du modèle objet ou, plus précisément, déporter ses descriptions sur l'instance réelle exposée par le Pont.

III-C-1 - L'interface du Pont : IBridge

Public Interface IBridge
Function GetRealInstance() As Object
End Interface

Impossible de faire plus simple.

III-C-2 - Le fournisseur de descripteur de Pont : BridgeDescriptionProvider

Pour que la magie du Pont rentre en jeu, comme on l'a vu précédemment via l'Adaptateur, il faut utiliser l'attribut TypeDescriptionProviderAttribute.
Celui-ci va nous permettre de spécifier nous-mêmes le descripteur utilisé pour l'objet qui déclare cet attribut.

L'attribut TypeDescriptionProviderAttribute attend comme paramètre un TypeDescriptionProvider qui fournit les métadonnées supplémentaires au TypeDescriptor.

Le Pont fournit une implémentation réutilisable de TypeDescriptionProvider. Voici son code :

Public Class BridgeDescriptionProvider
Inherits TypeDescriptionProvider

Public Overrides Function GetTypeDescriptor(ByVal objectType As Type,
ByVal instance As Object) As ICustomTypeDescriptor
Return New BridgeDescriptor(DirectCast(instance, IBridge))
End Function

End Class

III-C-3 - Le descripteur de Pont : BridgeDescriptor

Un TypeDescriptionProvider doit retourner un ICustomTypeDescriptor.

Le BridgeDescriptor hérite de CustomTypeDescriptor qui implémente ICustomTypeDescriptor.

Public Class BridgeDescriptor
Inherits CustomTypeDescriptor

Private ReadOnly _Bridge As IBridge

Public Sub New(ByVal realInstance As IBridge)
If realInstance Is Nothing Then Throw New ArgumentNullException("realInstance")
Me._Bridge = realInstance
End Sub

Private Function GetBridgePropertyDescriptorCollection(
ByVal realDescriptors As PropertyDescriptorCollection) As PropertyDescriptorCollection
Return New PropertyDescriptorCollection(Me.GetBridgePropertyDescriptors(realDescriptors))
End Function
Private Function GetBridgePropertyDescriptors(
ByVal realDescriptors As PropertyDescriptorCollection) As BridgePropertyDescriptor()
Dim Descriptors As New List(Of BridgePropertyDescriptor)
For Each RealDescriptor As PropertyDescriptor In realDescriptors
Descriptors.Add(New BridgePropertyDescriptor(Me._Bridge, RealDescriptor))
Next
Return Descriptors.ToArray
End Function

Public Overrides Function GetProperties() As PropertyDescriptorCollection
Return Me.GetBridgePropertyDescriptorCollection(
TypeDescriptor.GetProperties(Me._Bridge.GetRealInstance))
End Function
Public Overrides Function GetProperties(
ByVal attributes() As Attribute) As PropertyDescriptorCollection
Return Me.GetBridgePropertyDescriptorCollection(
TypeDescriptor.GetProperties(Me._Bridge.GetRealInstance, attributes))
End Function
End Class

III-C-4 - Le descripteur de propriété du Pont : BridgePropertyDescriptor

Pour décrire ses propriétés, le BridgeDescriptor utilise un BridgePropertyDescriptor par propriété.

Public Class BridgePropertyDescriptor
Inherits PropertyDescriptor

#Region "Fields"
Private _Bridge As IBridge
Private _RealProperty As PropertyDescriptor
#End Region

#Region "Constructors"
Public Sub New(ByVal bridge As IBridge, ByVal realProperty As PropertyDescriptor)
MyBase.New(realProperty)
If bridge Is Nothing Then Throw New ArgumentNullException("bridge")
If realProperty Is Nothing Then Throw New ArgumentNullException("realProperty")
Me._Bridge = bridge
Me._RealProperty = realProperty
End Sub
#End Region

#Region "Overrided Properties"
Public Overrides ReadOnly Property ComponentType As Type
Get
Return Me._RealProperty.ComponentType
End Get
End Property
Public Overrides ReadOnly Property IsReadOnly As Boolean
Get
Return Me._RealProperty.IsReadOnly
End Get
End Property
Public Overrides ReadOnly Property PropertyType As Type
Get
Return Me._RealProperty.PropertyType
End Get
End Property
Public Overrides ReadOnly Property SupportsChangeEvents As Boolean
Get
Return Me._RealProperty.SupportsChangeEvents
End Get
End Property
#End Region

#Region "Overrided Methods"
Public Overrides Function GetValue(ByVal component As Object) As Object
Return Me._RealProperty.GetValue(Me.GetRealComponent(component))
End Function
Public Overrides Sub SetValue(ByVal component As Object, ByVal value As Object)
Me._RealProperty.SetValue(Me.GetRealComponent(component), value)
End Sub

Public Overrides Function ShouldSerializeValue(ByVal component As Object) As Boolean
Return Me._RealProperty.ShouldSerializeValue(Me.GetRealComponent(component))
End Function

Public Overrides Function CanResetValue(ByVal component As Object) As Boolean
Return Me._RealProperty.CanResetValue(Me.GetRealComponent(component))
End Function
Public Overrides Sub ResetValue(ByVal component As Object)
Me._RealProperty.ResetValue(Me.GetRealComponent(component))
End Sub

Public Overrides Sub AddValueChanged(ByVal component As Object, ByVal handler As EventHandler)
Me._RealProperty.AddValueChanged(Me.GetRealComponent(component), handler)
End Sub
Protected Overrides Sub OnValueChanged(ByVal component As Object, ByVal e As System.EventArgs)
MyBase.OnValueChanged(Me.GetRealComponent(component), e)
End Sub
Public Overrides Sub RemoveValueChanged(ByVal component As Object, ByVal handler As System.EventHandler)
Me._RealProperty.RemoveValueChanged(Me.GetRealComponent(component), handler)
End Sub

Protected Overrides Function GetInvocationTarget(ByVal type As Type, ByVal instance As Object) As Object
If type Is GetType(IBridge) Then Return Me._Bridge
If type Is Me._Bridge.GetRealInstance.GetType Then Return Me._Bridge.GetRealInstance
Return MyBase.GetInvocationTarget(type, instance)
End Function
#End Region

#Region "Methods"
Private Function GetRealComponent(ByVal candidate As Object) As Object
If candidate Is Me._Bridge Then
Return Me._Bridge.GetRealInstance
Else
Return candidate
End If
End Function
#End Region
End Class

Le BridgePropertyDescriptor est un PropertyDescriptor qui redirige les demandes de description sur l'instance réelle exposée par le Pont au lieu de l'instance du Pont.

Le convertisseur de type n'est pas présenté dans cet article.
Il n'apporte rien au sujet et n'est pas utilisé par le code de cet article.
Toutefois, pour les curieux, il est implémenté dans le code livré avec cet article.

IV - Démonstration

Pour démontrer le fonctionnement des liaisons sur les composants, nous allons utiliser le modèle suivant :

Public Class SampleDataSource
Implements INotifyPropertyChanged

Public Event PropertyChanged(ByVal sender As Object, ByVal e As PropertyChangedEventArgs)
Implements INotifyPropertyChanged.PropertyChanged

Private _Command1Enabled As Boolean = True
Private _Command2Enabled As Boolean = True

<DefaultValue(True)>
Public Property Command1Enabled As Boolean
Get
Return Me._Command1Enabled
End Get
Set(ByVal value As Boolean)
Me._Command1Enabled = value
Me.NotifyPropertyChanged("Command1Enabled")
End Set
End Property
<DefaultValue(True)>
Public Property Command2Enabled As Boolean
Get
Return Me._Command2Enabled
End Get
Set(ByVal value As Boolean)
Me._Command2Enabled = value
Me.NotifyPropertyChanged("Command2Enabled")
End Set
End Property

Private Sub NotifyPropertyChanged(ByVal propertyName As String)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
Public Sub ExecuteCommand1(ByVal owner As IWin32Window)
Me.Command1Enabled = Not Me._Command1Enabled
MessageBox.Show(owner, "Command 1 State was toggled.",
"Command 1 Executed", MessageBoxButtons.OK, MessageBoxIcon.Information)
End Sub
Public Sub ExecuteCommand2(ByVal owner As IWin32Window)
Me.Command2Enabled = Not Me._Command2Enabled
MessageBox.Show(owner, "Command 2 State was toggled.",
"Command 2 Executed", MessageBoxButtons.OK, MessageBoxIcon.Information)
End Sub
End Class

Deux propriétés qui indiquent le statut actif / inactif de deux méthodes.
Deux méthodes qui inversent leur propre état à chaque utilisation et affichent un simple message.
Rien de bien transcendant.

Dans le concepteur de Visual Studio, nous créons un formulaire comme celui-ci.

Un ToolstripButton pour chaque commande.
Un PropertyGrid pour afficher le modèle de test.
Un BindingSource relié au modèle ci-dessus.
Un DataBindingsProvider pour fournir les liaisons aux composants.

Puis nous relions la propriété Enabled de chaque toolstrip button à la propriété correspondante dans le modèle.

Bonne nouvelle, le support des liaisons a bien été ajouté aux composants.
En plus, le comportement est strictement le même que pour les contrôles.

On appelle les bonnes méthodes sur le clic de chaque ToolStripButton :

Private Sub ToolStripButton1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs)
Handles ToolStripButton1.Click
DirectCast(Me.BindingSource.DataSource, SampleDataSource).ExecuteCommand1(Me)
Me.PropertyGrid.Refresh()
End Sub

Private Sub ToolStripButton2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs)
Handles ToolStripButton2.Click
DirectCast(Me.BindingSource.DataSource, SampleDataSource).ExecuteCommand2(Me)
Me.PropertyGrid.Refresh()
End Sub

On initialise la source de données sur le constructeur du formulaire :

Public Sub New()

' Cet appel est requis par le concepteur.
InitializeComponent()

' Ajoutez une initialisation quelconque après l'appel InitializeComponent().
Me.BindingSource.DataSource = New SampleDataSource
Me.PropertyGrid.SelectedObject = Me.BindingSource.DataSource
End Sub

Puis on lance la démonstration :

Quand on clique sur un des boutons, la commande s'exécute, et l'état de cette commande est modifié.
Quand on modifie l'état d'une commande via la PropertyGrid, ce changement se répercute sur le ToolStripButton relié à la propriété.

V - Conclusion

Le code de cet article fonctionne et prouve l'idée générale. Par contre, il lui manque certaines choses :
- le pont ne gère que les propriétés ;
- la propriété BindingContext n'est pas exposée par les extensions ;
- les sélections multiples des propriétés ne sont pas gérées.

En terminant cet article, nous clôturons la partie « Correction des lacunes et faiblesses de la liaison de données ».
Nous avons amélioré l'ergonomie des liaisons et ajouté leur support à n'importe quel composant.

En ce faisant, une jolie transition s'est offerte à nous pendant la démonstration.
Transition qui nous emmène dans la partie « Extensions des fonctionnalités de la liaison de données » de cette série d'articles et qui introduit parfaitement l'article suivant.

En effet, dans cette démonstration, nous avons utilisé du code sans aucun intérêt pour déclencher les commandes sur les évènements des boutons.
Et si on pouvait se passer de ce genre de code ?
Et si nous pouvions faire des liaisons sur les évènements ?

La réponse dans le prochain article de cette série.
Merci à vous et à bientôt pour un article intitulé : « Des liaisons sur les évènements ».

Remerciements

J'adresse ici tous mes remerciements à tous les membres de l'équipe de rédaction de "developpez.com" pour le temps qu'ils ont bien voulu passer à la correction et à l'amélioration de cet article.

Merci notamment à Claude LELOUP et à Jacques Jean pour leurs corrections et leurs relectures attentives.

Contact et code source

Si vous constatez une erreur dans l'article, dans les sources ou pour toute autre information, n'hésitez pas à me contacter via le forum de l'article.

Le code source de la preuve de faisabilité est disponible ici.