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 ControlClasse Control hérite de ou implémente ce qui suit :

On observe tout de suite une interface au nom explicite : IBindableComponentInterface 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.
- BindingContextPropriété BindingContext de type BindingContextClasse BindingContext ;
- DataBindingsPropriété DataBindings de type ControlBindingsCollectionClasse 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.
- 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 IExtenderProviderInterface IExtenderProvider et ProvidePropertyAttributeL'attribut ProvidePropertyAttribute.
MSDN nous dit à propos de IExtenderProviderInterface 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 objetVue d'ensemble du descripteur de type. utilisé par le moteur de liaison est exposé dans l'espace de nom System.ComponentModelEspace 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 memberInfoLa réflexion, mais par une vue sur ce modèle.
Vue qui est constituée principalement par les objets TypeDescriptorClasse TypeDescriptor et MemberDescriptorClasse MemberDescriptor.
- Souplesse et modularité grâce aux Design PatternsSouplesse et modularité grâce aux Design Patterns ;
- Comprendre le Design Pattern AdaptateurComprendre le Design Pattern Adaptateur ;
- Le forum de Développez.comLe forum de Développez.com.
Les définitions des patrons de conception ci-dessus sont tirées du livre : Design Patterns Pour JavaDesign Patterns Pour Java.
II-B-2. La réponse en image : 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.
- le Fournisseur de descripteur de Pont hérite de TypeDescriptionProviderClasse TypeDescriptionProvider ;
- le Descripteur de Pont hérite de CustomTypeDescriptorClasse CustomTypeDescriptor ;
- le Descripteur de propriété du Pont hérite de PropertyDescriptorClasse PropertyDescriptor ;
- le Convertisseur de type hérite de TypeConverterClasse 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é DataBindingsPropriété DataBindings aux composants existants.
Cette propriété doit fournir le même fonctionnement que la propriété DataBindingsPropriété DataBindings des contrôles.
Il correspond au fournisseur d'extensions du premier schéma.
<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- on déclare que l'on fournit la propriété DataBindingsPropriété DataBindings pour tous les objets IComponentInterface IComponent via l'attribut ProvidePropertyL'attribut ProvideProperty ;
- on implémente l'interface IExtenderProviderInterface IExtenderProvider en filtrant les types étendus via la méthode CanExtendMé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 IBindableComponentInterface 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 : MSDNComment implémenter un fournisseur d'extendeurs..
- 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 :Sélectionnez
ComponentBindings=NewBindableAdapter(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 :SélectionnezMe.DataBindingsProvider1.AddBinding(Me.ToolStripButton1,NewSystem.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 IBindableComponentInterface IBindableComponent au composant adapté.
Cet objet est nommé Adaptateur de liaison dans le premier schéma et Adaptateur dans le second.
<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 ClassLes éléments nécessaires au bon fonctionnement des liaisons sont l'implémentation de l'interface IBindableComponentInterface IBindableComponent et l'héritage de ComponentClasse Component.
L'implémentation de IBindableComponentInterface 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.
- la déclaration du Fournisseur de description du Pont via l'attribut TypeDescriptionProviderL'attribut TypeDescriptionProvider ;
- 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 ControlBindingsCollectionClasse ControlBindingsCollection est reliée via son constructeurConstructeur de la classe ControlBindingsCollection à 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 objetVue d'ensemble du descripteur de type 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 InterfaceImpossible 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 TypeDescriptionProviderAttributeL'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 TypeDescriptionProviderAttributeL'attribut TypeDescriptionProviderAttribute attend comme paramètre un TypeDescriptionProviderClasse TypeDescriptionProvider qui fournit les métadonnées supplémentaires au TypeDescriptorClasse TypeDescriptor.
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 ClassIII-C-3. Le descripteur de Pont : BridgeDescriptor▲
Un TypeDescriptionProviderClasse TypeDescriptionProvider doit retourner un ICustomTypeDescriptorInterface 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 ClassIII-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 ClassLe BridgePropertyDescriptor est un PropertyDescriptorClasse 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 ClassDeux 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.
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.

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.
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 SubPublic 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 SubPuis 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é BindingContextProprié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 liaisonsPartie 1: Présentation et amélioration de 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ènementsWindows Forms - de la liaison de données à la liaison d'objets - partie 3 : 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 JeanProfil de 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'articleForum de l'article.
Le code source de la preuve de faisabilité est disponible iciLes sources de la preuve de faisabilité.


