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 :

Les bases d'un contrôle

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.

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

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 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.

Aller plus loin avec les patrons de conception :

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

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

Le patron de conception Pont en situation.

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 :

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 patternite 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.

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

<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 :

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..

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 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 = 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 :
     
    Sélectionnez
    
        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 IBindableComponentInterface 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 :
Sélectionnez

<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 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.

Cette logique correspond à :
  1. la déclaration du Fournisseur de description du Pont via l'attribut TypeDescriptionProviderL'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 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

 
Sélectionnez

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 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.

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

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 TypeDescriptionProviderClasse TypeDescriptionProvider doit retourner un ICustomTypeDescriptorInterface ICustomTypeDescriptor.

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

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é.

 
Sélectionnez

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 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 :

 
Sélectionnez

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 affiche un simple message.
Rien de bien transcendant.

Démonstration dans le concepteur Windows Forms
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.

Liaison des propriétés.
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 à 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 click de chaque ToolStripButton :
Sélectionnez

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 :
Sélectionnez

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 :

Démonstration à l'exécution.

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 LELOUPProfil de 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é.