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

Description de la série d'articles

Cette série d'articles suivra la logique suivante :

Cette section sera mise à jour en fonction de l'avancement de la série.

Les preuves de faisabilité (le code)

Chaque article sera fourni avec une preuve de faisabilité sous forme de code source et de démonstration.

Ces projets seront indépendants les uns des autres et développés en Visual Basic sous Visual Studio 2010. Ils seront uniquement des preuves de faisabilité et ne seront donc pas de qualité suffisante pour être utilisés tels quels en production.

Qualité insuffisante pour passer en production ! Pourquoi ?
Il ne s'agit pas ici de critère de qualité sur le code ; le code des preuves de faisabilité est fonctionnel et bien écrit (sauf erreur de ma part), mais ce code a été réalisé avec un seul objectif en tête : prouver que c'est possible... et utile.

Pour passer en production, d'autres critères interviennent :
  • tests unitaires et / ou contrats de code ;
  • architecture supportant l'évolution et la maintenance correctement (principe d'ouverture, fermeture etc.) ;
  • et plus globalement, tous les critères de qualité logicielle.
Concrètement, quels sont les problèmes de ces projets ?
  • Ces projets ne sont pas séparés en profil Client / Design, le code gérant le « Design Time » est directement intégré dans les projets. Ce qui nous force à modifier le profil des projets de « DotNet 4.0 Client Profile » à « DotNet 4.0 ». Normalement, en suivant ce qui se pratique en DotNet, le « Design Time » devrait être séparé du code fonctionnel et ainsi, devenir optionnel.
  • Lorsque je réalise des preuves de faisabilité, toute question architecturale qui ne fait pas partie du sujet étudié est automatiquement éludée. Donc, il peut y avoir des raccourcis dangereux au niveau de la structure des objets.
  • Pas de test unitaire, pas de contrat de code et juste des démonstrations minimalistes pour vérifier et démontrer le fonctionnement par l'exemple.
De plus, je ne suis pas partisan du code pollué par la documentation ou des commentaires inutiles.
  • Pour moi, un code se doit d'être lisible et accessible sans aucun commentaire. C'est pourquoi les preuves de faisabilité des articles ne contiendront aucun commentaire.

Finalité

Un projet regroupant toutes ces preuves de faisabilité en les améliorants pour les rendre utilisables en production sera fourni sur Developpez.Com.

Ce projet permettra l'utilisation de la liaison d'objets et proposera d'autres améliorations complémentaires à la liaison d'objets.

I. La liaison de données en Windows Forms

I-A. Constat

Comme tout développeur en Windows Forms, j'ai toujours été très déçu non pas du potentiel de la liaison de données, mais par sa sous-utilisation, un peu comme si Microsoft ne croyait pas trop en sa propre technologie.

Après plus de 10 ans d'expérience dans le développement d'application, j'ai pu constater que le code des interfaces graphiques est trop souvent du code de « plomberie » et encore plus souvent du code de liaison (telle méthode appelée sur tel évènement etc.).

Que l'on utilise un patron de conception, sa propre méthode ou autre chose, on se rend vite compte que 60 % du code des interfaces graphiques est du code de liaison et rien d'autre. Et encore, j'abaisse volontairement mon estimation pour ne pas choquer les esprits sensibles.

Or justement, lorsque l'on se décide enfin à passer le cap et à utiliser la liaison de données pour remplacer tout ce code pas inutile mais sans aucune valeur ajoutée et sans aucune intelligence, on se rend tout de suite compte que... ce n'est pas possible !

En effet la liaison de données mérite son nom, elle est destinée à relier des données à des composants graphiques, et toutes ses lacunes et ses faiblesses viennent de cet objectif fonctionnel.

Pourtant le moteur de liaison utilisé par la liaison de données est beaucoup plus puissant que ne le laisse apparaître la liaison de données. Un exemple : le moteur de liaison gère très bien les propriétés imbriquées (nested properties), seules les interfaces graphiques pour saisir ces propriétés imbriquées ne les supportent pas.

I-B. Lacunes et faiblesses

Alors quelles sont ces lacunes et faiblesses de la liaison de données en Windows Forms ?

Tentative d'inventaire de ma part :
  • seuls les contrôles supportent par défaut la liaison de données, les composants comme les ToolStrips ne les supportent pas ;
  • l'éditeur de liaison n'est vraiment pas facile d'accès, si on conçoit ses interfaces graphiques centrées sur les liaisons, cela devient contre-productif. En plus, cet éditeur ne supporte pas les propriétés imbriquées ;
  • pas de liaison sur les évènements ;
  • pas de liaison sur les champs ;
  • pas de liaison entre les propriétés et le résultat d'une méthode ou la valeur d'un champ ;
  • pas de composant pour créer une vue sur un objet.

Dans cette série d'articles, je vais donc essayer de résoudre ces lacunes une par une et le tout sans réinventer la roue. J'entends par là que les résolutions se doivent le plus possible d'être compatibles avec l'existant.

En effet, à quoi bon faire tout ça, si l'on doit réécrire tous les contrôles des Windows Forms.
Absolument aucun intérêt, autant passer directement à WPF et / ou Silverlight.

II. Amélioration de l'ergonomie des liaisons

II-A. L'ergonomie existante

En Windows Forms on dispose de deux moyens pour saisir visuellement une liaison de données :

L'éditeur de liaison standard
l'éditeur de liaison de données ;
L'afficheur de source de données
et l'afficheur de sources de données.
Dans les deux images ci-dessus, la source de données utilisée est la même. Voici son code.
Sélectionnez

            Public Class PersonPropertyModel
        
                Private _Address As New AddressPropertyModel 
                Public Property FirstName As String 
                Public Property LastName As String
        
                Public ReadOnly Property Address As AddressPropertyModel
                    Get
                        Return Me._Address
                    End Get
                End Property
        
            End Class
            Public Class AddressPropertyModel
        
                Public Property Address1 As String
                Public Property Address2 As String
                Public Property Address3 As String
                Public Property PostalCode As Int32
                Public Property City As String
        
            End Class
        
Simple et plutôt classique, pourtant, entre l'éditeur et l'afficheur, les comportements sont très différents.
  • L'afficheur ne souffre d'aucun défaut et remplit très bien son rôle.
    Il supporte parfaitement les propriétés imbriquées, et permet même de faire un glisser déplacer de ces propriétés pour créer directement le label et le contrôle de saisie associés.
  • L'éditeur, par contre, est difficile d'accès et souffre d'un bogue extrêmement gênant sur les propriétés imbriquées puisqu'il ne les supporte pas.

Pourtant les deux utilisent la même liaison de données.

On vient de mettre le doigt sur ce que j'appelle « lacunes et faiblesses » de la liaison de données.

C'est aussi ce genre de chose qui me fait penser que Microsoft, lors de la création des Windows Forms a expérimenté la liaison de données, mais n'a pas poussé le sujet jusqu'au bout, en tout cas, en Windows Forms.

II-B. Utilisation de l'éditeur de liaison

Les étapes pour accéder à l'éditeur de liaison à partir du concepteur visuel des Windows Forms sont :
  1. Sélectionner un composant visuel ;
  2. Afficher la page de propriétés de ce composant visuel ;
  3. Sélectionner la propriété DataBindings ;
  4. Dérouler cette propriété ;
  5. Sélectionner la sous-propriété « Avancées » ;
  6. Cliquer sur le bouton qui ouvre l'éditeur.
Une fois dans l'éditeur de liaison, il faut saisir :
  • la propriété à lier ;
  • la source de la liaison utilisée ;
  • le mode de liaison de la source ;
  • le format ;
  • et la valeur nulle.

Et il faut répéter ces actions pour chaque liaison que l'on souhaite créer.

Pas très ergonomique tout ça.

II-C. La solution proposée

Sur la quantité d'informations à saisir pour configurer une liaison, on ne peut pas faire grand-chose. Hormis, peut-être, fournir un accès indépendant à chaque valeur pour faciliter l'accès à ces valeurs.

Par contre, concernant l'accès à l'éditeur de liaison, là, on peut faire beaucoup mieux. Je vous propose d'ajouter un onglet dans la PropertyGrid qui sera dédié aux liaisons sur les propriétés.

Ce qui donnera :

Onglet des liaisons
dans cet onglet, au lieu d'éditer la valeur d'une propriété, on édite la liaison associée à la propriété ;
Onglet des liaisons avec une propriété déroulée.
en double-cliquant sur une propriété, ou en cliquant sur l'icône sur sa gauche, on déroule les valeurs correspondantes de la liaison, fournissant ainsi un accès séparé à ces valeurs et aux éditeurs associés ;
Editeurs de liaison
pour chaque propriété, on propose un éditeur visuel quand c'est nécessaire afin de faciliter la saisie et de limiter les erreurs ;
Editeur de liaison
enfin, pour ceux qui sont allergiques à la PropertyGrid et ses niveaux hiérarchiques, en cliquant sur le bouton d'édition à droite de la propriété, on affiche un éditeur classique.

Comme on n'a pas réinventé la roue, mais ajouté un troisième moyen d'édition des liaisons, les modes d'édition standards sont toujours présents et fonctionnent toujours de la même façon.

Les nouveaux éditeurs supportent les propriétés imbriquées et les liaisons sur autre chose que des sources de données.

II-D. Réalisation de la solution proposée

Dans la preuve de faisabilité de cet article, il n'y a pas beaucoup de code intéressant.
Il y a beaucoup d'éditeur visuel, qui sont de simples contrôles, beaucoup d'extensions pour faciliter l'écriture et la lecture du code et presque aucun code concernant les liaisons de données directement.

Les seuls sujets qui méritent une explication (à mon avis) sont :
  • comment ajouter un onglet à une PropertyGrid ;
  • comment créer un éditeur utilisable dans une PropertyGrid.

Si vous n'êtes intéressé que par les liaisons, rendez-vous à l'étape suivante : .

II-D-1. Le principe de fonctionnement

Principe de fonctionnement
  1. L'onglet des liaisons affiche les propriétés de la sélection courante en les associant avec leur liaison respective via un descripteur de propriétés spécifique.
  2. Le descripteur en question utilise un objet spécifique pour exposer la liaison de la propriété ; notamment pour des raisons de facilité et de persistance.
  3. Cet objet spécifique expose les différentes propriétés d'une liaison et fournit tous les éditeurs associés.

  • La persistance est gérée automatiquement par le concepteur visuel puisqu'on travaille directement sur des instances de liaisons standards.
  • Lorsqu'une liaison n'est plus valide lors d'une saisie, elle est automatiquement retirée de sa collection. Lorsqu'elle redevient valide, elle est automatiquement ajoutée à sa collection. Ce fonctionnement peut paraître bizarre, mais c'est la conséquence directe de la fonctionnalité recherchée, à savoir, permettre la saisie séparée des propriétés d'une liaison. Une liaison n'est valide que si elle dispose d'une source et d'un membre.

L'onglet des liaisons peut être fourni par n'importe quel objet, cela n'a aucune importance.

II-D-2. Comment ajouter un onglet à une PropertyGrid ?

Pour ajouter un onglet à une PropertyGrid, nous devons faire trois choses :
  1. Créer un onglet en héritant de PropertyTabPropertyTab ;
  2. Déclarer cet onglet sur un composant visuel via l'attribut PropertyTabAttributePropertyTabAttribute en précisant sa portée via l'énumération PropertyTabScopePropertyTabScope ;
  3. Utiliser le composant visuel de l'étape précédente dans le concepteur Windows Forms.
Hériter d'un PropertyTab est plutôt simple (enfin, en fonction de ce que va faire votre onglet bien évidemment) et peut se résumer comme suit :

L'attribut PropertyTabAttributeL'attribut PropertyTabAttribute ne peut pas être déclaré plusieurs fois sur un même objet. Par contre, on peut l'hériter et spécifier plusieurs onglets via cet héritage. Pour cela, l'héritier devra appeler la méthode InitializeArraysMéthode InitializeArrays de sa base.

On peut accéder aux onglets d'une PropertyGrid via la propriété PropertyTabsPropriété PropertyTabs.

II-D-3. Le code de l'onglet des liaisons

 
Sélectionnez

<PermissionSetAttribute(SecurityAction.Demand, Name:="FullTrust")>
Public Class BindingsPropertyTab
    Inherits PropertyTab

    Private _ServiceProvider As IServiceProvider

    Private Sub New()
    End Sub
    Public Sub New(ByVal sp As IServiceProvider)
        MyBase.New()
        If sp Is Nothing Then Throw New ArgumentNullException("sp")
        Me._ServiceProvider = sp
    End Sub

    Public Overrides ReadOnly Property Bitmap As System.Drawing.Bitmap
        Get
            Return My.Resources.BindingsTab
        End Get
    End Property
    Public Overrides ReadOnly Property TabName As String
        Get
            Return My.Resources.BindingsTabName
        End Get
    End Property
    Public Overrides ReadOnly Property HelpKeyword As String
        Get
            Return My.Resources.BindingsHelpKeyWord
        End Get
    End Property

    Public Overrides Function CanExtend(ByVal extendee As Object) As Boolean
        Return Me.HasBindings(extendee)
    End Function

    Public Overrides Function GetDefaultProperty(ByVal component As Object) As PropertyDescriptor
        Dim Bindings = Me.GetBindings(component)
        If Bindings Is Nothing Then Return Nothing

        Dim DefaultBindingProperty = BindingHelper.GetDefaultBindingProperty(component)
        If DefaultBindingProperty IsNot Nothing Then
            Return New DesignableBindingPropertyDescriptor(component, 
                DefaultBindingProperty, Me._ServiceProvider, Bindings)
        End If

        Dim DefaultProperty = TypeDescriptor.GetDefaultProperty(component)
        If DefaultProperty Is Nothing Then Return Nothing
        Return New DesignableBindingPropertyDescriptor(component, 
            DefaultProperty, Me._ServiceProvider, Bindings)
    End Function

    Public Overloads Overrides Function GetProperties(ByVal component As Object, 
        ByVal attributes() As Attribute) As PropertyDescriptorCollection
        Dim ComponentBindings = Me.GetBindings(component)
        Return BindingHelper.GetBindingMembers(component, 
            ComponentBindings, attributes, Me._ServiceProvider)
    End Function
    Public Overrides Function GetProperties(ByVal component As Object) As PropertyDescriptorCollection
        Dim ComponentBindings = Me.GetBindings(component)
        Return BindingHelper.GetBindingMembers(component, 
            ComponentBindings, Nothing, Me._ServiceProvider)
    End Function
    Public Overrides Function GetProperties(ByVal context As ITypeDescriptorContext, 
        ByVal component As Object, 
        ByVal attributes() As Attribute) As PropertyDescriptorCollection
        If context.IsRootComponent(component) Then
            Return Me.GetProperties(component, attributes)
        Else
            Return TypeDescriptor.GetProperties(component, attributes)
        End If
    End Function

    Protected Overridable Function HasBindings(ByVal component As Object) As Boolean
        If component Is Nothing Then Return False
        Return TypeOf component Is IBindableComponent
    End Function
    Protected Overridable Function GetBindings(ByVal component As Object) As ControlBindingsCollection
        If component Is Nothing Then Return Nothing
        If Not TypeOf component Is IBindableComponent Then Return Nothing
        Return DirectCast(component, IBindableComponent).DataBindings
    End Function

End Class

Ce code est simple.

Les deux méthodes qui gèrent les liaisons sont HasBindings et GetBindings.
Ici, elles ne gèrent que les liaisons par défaut fournies par l'interface IBindableComponentInterface IBindableComponent.

II-D-4. Comment créer un éditeur visuel pour une propriété ?

Pour créer un éditeur visuel, il faut hériter de la classe UITypeEditorUITypeEditor.

La documentation sur cette classe est abondante, je vais juste résumer sont utilisation classique :
Exemples de déclaration :
  • via les attributs ;
    Sélectionnez
    
    <Editor(GetType(DesignableBindingDataMemberSelectorEditor), GetType(UITypeEditor))>
    
  • via la méthode GetEditor.
    Sélectionnez
    
    Public Overrides Function GetEditor(ByVal editorBaseType As Type) As Object
        Return New DesignableBindingEditor
    End Function
    

II-D-5. Le code de l'éditeur utilisé pour les liaisons « éditables » 

 
Sélectionnez

Public Class DesignableBindingEditor
        Inherits UITypeEditor

        Public Overrides Function GetEditStyle(ByVal context As ITypeDescriptorContext) As UITypeEditorEditStyle
            Return UITypeEditorEditStyle.Modal
        End Function

        Public Overrides ReadOnly Property IsDropDownResizable As Boolean
            Get
                Return True
            End Get
        End Property

        Public Overrides Function EditValue(ByVal context As ITypeDescriptorContext, 
            ByVal provider As IServiceProvider, ByVal value As Object) As Object
            If provider Is Nothing Then Return value

            If value Is Nothing OrElse Not TypeOf value Is DesignableBinding Then Return value

            Dim UIService = provider.GetService(Of IWindowsFormsEditorService)()
            If UIService Is Nothing Then Return value

            Dim View = Me.CreateView(context, provider, DirectCast(value, DesignableBinding))
            UIService.ShowDialog(View)
            Return View.BindingBox.Binding

        End Function

        Private Function CreateView(ByVal context As ITypeDescriptorContext, 
            ByVal provider As IServiceProvider, 
            ByVal binding As DesignableBinding) As BindingEditorDialog
            Dim View As New BindingEditorDialog
            If TypeOf context.PropertyDescriptor Is DesignableBindingPropertyDescriptor Then
                Dim DBDescriptor = DirectCast(context.PropertyDescriptor, DesignableBindingPropertyDescriptor)
                View.BindingBox.BindedComponent = DBDescriptor.Component
                View.BindingBox.BindedMemberName = DBDescriptor.DisplayName
            End If
            View.BindingBox.Binding = binding
            View.BindingBox.ServiceProvider = provider
            View.BindingBox.Fill()
            Return View
        End Function
End Class
Cet éditeur fait le strict minimum :
  • il récupère un service d'affichageService d'affichage ;
  • il initialise le contrôle qui va permettre la saisie de la valeur (en fonction du contexte et de la valeur) ;
  • il affiche ce contrôle via le service d'affichage ;
  • il retourne la valeur, modifiée ou non.

II-D-6. L'architecture de services du concepteur Windows Forms

Toute la difficulté de l'utilisation des onglets de propriétés et des éditeurs visuels réside non pas dans les classes elles-mêmes, mais dans l'architecture de services du concepteur Windows FormsArchitecture de design.

Mieux on connaît cette architecture, plus facile est la gestion du « Design Time ».

II-D-6-1. Le fournisseur de services

La première chose à savoir, c'est que tous les services sont fournis par un objet du type IServiceProviderInterface IServiceProvider.

Dans le cas d'un UITypeEditorClasse UITypeEditor, ce fournisseur de services est passé en paramètre de la méthode EditValueMéthode EditValue.

On peut récupérer à tout instant ce fournisseur de services par d'autres moyens, notamment par le biais du SitePropriété Site attaché à chaque composant puisque l'interface ISiteInterface ISite hérite de l'interface IServiceProviderInterface IServiceProvider.

Un petit conseil ; lorsque vous utilisez ces services, n'hésitez pas à utiliser des extensions pour faciliter leur usage, sinon, une simple ligne, un simple appel peut vite devenir un enfer.

Exemple d'extension :
Sélectionnez

Namespace Extensions
    <Extension()> _
    Public Module ServiceExtensions

#Region "IServiceProvider"
        <Extension()>
        Public Function GetService(Of TService As Class)(
            ByVal SP As IServiceProvider) As TService
            If SP Is Nothing Then Return Nothing
            Return TryCast(SP.GetService(GetType(TService)), TService)
        End Function
#End Region

    End Module
End Namespace

II-D-6-2. Les principaux services

Et ce ne sont que les plus utilisés.
La plupart du temps, ils se trouvent dans les espaces de nom qui se terminent par Design.

Je vous laisse consulter les liens MSDN si le sujet vous intéresse.

III. Démonstration

Pour démontrer l'utilité de la solution proposée dans cet article, nous allons utiliser un modèle de données des plus classiques qui soit et le lier à une représentation visuelle.

En plus de tout ça, nous allons ajouter un contrôle qui va se lier à un autre contrôle (sans passer par une source de donnéesComposant BindingSource).

III-A. Les modèles de données

 
Sélectionnez

<PropertyTab(GetType(BetterAccessibility.BindingsPropertyTab), PropertyTabScope.Document)>
Public Class PersonModel
    Inherits Component

    Private _Address As New AddressModel

    <DisplayName("Prénom")>
    Public Property FirstName As String
    <DisplayName("Nom")>
    Public Property LastName As String

    <DisplayName("Adresse")>
    <DesignerSerializationVisibility(DesignerSerializationVisibility.Content)>
    Public ReadOnly Property Address As AddressModel
        Get
            Return Me._Address
        End Get
    End Property

End Class

<TypeConverter(GetType(ExpandableObjectConverter))>
Public Class AddressModel

    <DisplayName("Adresse")>
    Public Property Address As String
    <DisplayName("Complément d'adresse")>
    Public Property Complement As String
    <DisplayName("Code postal")>
    Public Property PostalCode As Int32
    <DisplayName("Ville")>
    Public Property City As String

End Class

Ce modèle est une adaptation du premier modèle utilisé dans cet article.
Le modèle racine est un composant afin de pouvoir l'inclure dans la vue.
Il active aussi l'onglet de liaison. N'importe quel autre objet aurait pu remplir ce rôle.

III-B. Le formulaire de démonstration

Formulaire de démonstration dans le concepteur Windows Forms
Les TextBox en lecture / écriture sont reliées aux propriétés correspondantes sur les modèles.
PersonModel1 est affecté au BindingSource.
Sélectionnez

Public Sub New()

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

    ' Ajoutez une initialisation quelconque après l'appel InitializeComponent().
    Me.PersonModelBindingSource.DataSource = Me.PersonModel1
End Sub
Le modèle dans le concepteur Windows Forms
Le modèle de données est initialisé comme indiqué ci-dessus.
Propriété du clone.
Enfin la TextBox servant de clone est reliée, comme le montre l'image, à la TextBox du nom.
Démonstration à l'exécution
Résultat quand on exécute la démonstration.

Le code utilisateur complet du formulaire se résume au code du constructeur ci-dessus.
Tout le code des liaisons se situe dans le code généré par Visual Studio.

Les différentes liaisons fonctionnent parfaitement.
Lorsque l'on modifie le nom, son clone est automatique modifié.
La liaison du clone fonctionne sans passer par une source de données.

III-C. Conclusion

La démonstration de cet article est loin d'être parfaite, elle peut être améliorée de diverses manières.

On pourrait par exemple :

Néanmoins, la preuve est faite, il est possible d'améliorer l'ergonomie des liaisons et ce, sans toucher aux liaisons en question.

D'ailleurs, en ce faisant, nous avons pu observer et corriger une autre lacune / manque des éditeurs standards : ceux-ci ne permettent pas d'éditer des liaisons en dehors de celles proposées pas les sources de données, or, le moteur de liaison le supporte parfaitement.

Ceci conclut cet article, mais pas cette série ; prochaine lacune : « des liaisons sur les composantsWindows Forms - de la liaison de données à la liaison d'objets - partie 2 : des liaisons sur les composants ». On pourra y observer la magie de certains patrons de conception.

Je n'en dis pas plus, vous pourrez le constater par vous-même.

Merci à vous et à bientôt.

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 à Jacques JeanProfil de Jacques Jean pour ses corrections et sa relecture attentive ainsi qu'à Philippe VialatteProfil de Philippe Vialatte pour son accueil dans l'équipe et ses conseils.

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