Les composants extenders ou providers de WinForm

Les extenders ou providers

Avant Propos

Les extenders ou providers sont des composants que l'on peut drag-dropper sur le designer Winform et qui permettent de données une "expérience utilisateur supplémentaire". Il s'agit par exemple des composants Tooltip, ErrorProvider et HelpProvider. Techniquement, il s'agit de composant qui expose des propriétés supplémentaire sur les autres composants/contrôles de la Form, comme par exemple une propriété Tooltip sur tous les contrôles n'en possédant pas une "builtin".

Introduction

Si vous avez un jour peaufiné votre interface graphique, vous avez sûrement utilisé des composants spéciaux tels que ErrorProvider, HelpProvider ou encore ToolTip.

Voici rapidement à quoi chacun des composants cités ci-dessus peuvent servir :

  • ErrorProvider : permet d'afficher une icône à côté d'un autre contrôle pour indiquer une erreur de saisie, par exemple.
  • HelpProvider : permet de fournir un contexte d'aide sur chacun des contrôles.
  • ToolTip : permet de définir une propriété Tooltiptext sur chacun des contrôles (qui n'en fournissent pas déjà un).

Et vous aurez sûrement remarqué que, sur tous les autres contrôles, il y a une ou plusieurs propriétés dont le nom est « propriété sur nom_composant » / « property on component_name », où propriété est un nom de propriété et nom_composant est le (Name) du composant Provider (pas le nom du contrôle).

Ces composants, appelés « Extender » ou « Provider » ont leurs propres propriétés, mais aussi, fournissent des propriétés sur les autres contrôles, appelées « propriétés étendues ».

Il y aurait bien un autre moyen que les « Extenders » pour modifier le comportement d'un type de contrôle: dériver de chaque type de contrôles pour rajouter les propriétés nécessaires. Cependant, si vous voulez, par exemple, ajouter des propriétés à tous les types de boutons (type ButtonBase), vous ne pouvez pas employer la dérivation (puisque dériver de ButtonBase ne changerait pas la classe Textbox). De plus, pour des raisons de maintenance, il n'est pas raisonnable de faire des dérivées de plusieurs contrôles pour ajouter les mêmes propriétés avec les mêmes traitements (le copier-coller n'étant pas une technique sûre en matière de développement).

Si vous n'avez jamais regardé le code généré dans la méthode InitializeComponent de la Form parente, voici ce que cela peut donner (et notamment la ligne en gras) :

/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
    this.components = new System.ComponentModel.Container();
    this.textBox1 = new System.Windows.Forms.TextBox();
    this.extenderComponent1 = new
    LesDesignersLib.ExtenderComponent(
    this.components);
    this.SuspendLayout();
    //
    // textBox1
    //
    this.textBox1.Location = new System.Drawing.Point(69, 40);
    this.extenderComponent1.SetMyDrawnText(this.textBox1, "test");
    this.textBox1.Name = "textBox1";
    this.textBox1.Size = new System.Drawing.Size(100, 20);
    this.textBox1.TabIndex = 0;
    //
    // Form1
    //
    this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
    this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
    this.ClientSize = new System.Drawing.Size(292, 266);
    this.Controls.Add(this.textBox1);
    this.Name = "Form1";
    this.Text = "Form1";
    this.ResumeLayout(false);
    this.PerformLayout();
}

Développer un Extender

L'interface IExtenderProvider

Techniquement, un extender est généralement un Component (mais peut aussi être un UserControl ou un Control Owner Drawn) qui implémente l'interface IExtenderProvider et qui possède certains attributs.

L'interface IExtenderProvider possède une seule et unique méthode CanExtend qui renvoie un booléen indiquant si les propriétés étendues que fournit cet extender, doivent être affichées pour l'Object passé en paramètre. On notera que ce n'est pas cette interface qui permet de donner la liste des propriétés étendues que cet extender fournit. En fait, il faut ajouter un attribut ProvideProperty par propriété étendue sur la classe de notre composant (qui implémente IExtenderProvider) afin de signaler les propriétés étendues au Designer. Si la méthode CanExtend renvoie true, le designer utilise la réflexion pour obtenir la liste des propriétés étendues et les applique à l'object (contrôle ou composant) qu'il a passé à cette méthode.

Les attributs ProvideProperty

L'attribut ProvideProperty spécifie le nom de la propriété étendue et le type des objets auxquels s'applique la propriété étendue. En fait, le designer teste si le contrôle sélectionné est un type dérivé du type passé à l'attribut. Donc si vous voulez appliquer une propriété étendue à des contrôles divers, il faut que vous spécifiiez comme type, un type de base commun. Par exemple, pour fournir une propriété étendue à un Checkbox, un RadioButton et un Button alors vous devez spécifier ButtonBase comme type à l'attribut ProvideProperty.

[ProvideProperty("MyDrawnText",typeof(TextBox))]

Implémentation de CanExtend ou « de l'art du filtrage des composants admissibles »;

Vous me direz « Quelle est la différence entre CanExtend et le type que l'on passe dans l'attribut ProvideProperty ». Eh bien, à cela, je répondrai que CanExtend est un filtre d'instances et que ProvideProperty fournit un filtre de types.

ProvideProperty indique au designer quels types de contrôles ou composants peuvent posséder une propriété étendue fournie par notre extender. CanExtend, quant à elle, permet de dire si une instance particulière d'un composant doit posséder la propriété. On peut, par exemple, filtrer suivant les propriétés du contrôle passé en paramètre, ou encore suivant son type ou enfin, empêcher d'appliquer les propriétés étendues sur l'extender lui-même.

Un problème survient toutefois : si l'on a deux propriétés étendues qui s'appliquent sur des types de contrôles différents, certes, ProvideProperty dira sur quel contrôle on devrait appliquer chacune des propriétés, mais CanExtend ne saura pas pour laquelle des propriétés elle est appelée (car elle sera appelée indistinctement pour les deux). Il faudra alors ne faire des tests que sur les instances en fonction de leur type (deux types différents dans le cas présent). En général, on préfèrera faire deux composants séparés.

Il existe toutefois un cas où l'on sera obligé de séparer en plusieurs composants : une propriété étendue pour Button et Checkbox et une autre pour RadioButton. Pour la première, il sera nécessaire de mettre ButtonBase comme type dans ProvideProperty puisque ButtonBase est la base directe de Checkbox et Button. Le problème est que la seconde propriété qui devrait être réservée au Button, ne pourra pas l'être. En effet, Button est aussi un ButtonBase et comme CanExtend ne sait pas pour quelle propriété elle travaille, elle ne pourra pas renvoyer false pour les Button sur la première propriété. Il en résultera que les Button auront les deux propriétés.

Par exemple :

public bool CanExtend(object extendee)
{
    //uniquement TextBox
    if (extendee != null && extendee is TextBox)
        //TextBox n'ayant pas de contenu
        return string.IsNullOrEmpty(((TextBox)extendee).Text);
    else
        return false;
}

En résumé : le designer filtre d'abord les contrôles par type, suivant ProvideProperty, puis demande confirmation à CanExtend pour savoir si un contrôle particulier doit avoir les propriétés.

Les méthodes permettant d'accéder aux propriétés étendues

Ensuite, il faut que vous définissiez deux méthodes GetPropertyName et SetPropertyNamePropertyName est le nom de la propriété étendue. Ces méthodes permettent de définir et de récupérer la valeur de la propriété pour un contrôle donné.

La méthode GetPropertyName prend un paramètre du type qui a été fourni à l'attribut ProvideProperty pour la propriété étendue. Elle renvoie un objet du type de la propriété étendue.

La méthode SetPropertyName prend un paramètre du type qui a été fourni à l'attribut ProvideProperty pour la propriété étendue et un paramètre du type de la propriété étendue. Elle ne renvoie pas de valeur.

Le designer utilise ces deux méthodes afin de définir les propriétés étendues au moment du Design (donc sérialisées dans InitializeComponent). Le programmeur peut aussi utiliser celles-ci afin de définir au runtime une propriété étendue (par exemple, définir l'erreur à afficher par l'ErrorProvider).

Par exemple :

public string GetMyDrawnText(Control c)
public void SetMyDrawnText(Control c, string value)

Implémentations des méthodes d'accès aux propriétés étendues

Pour implémenter ces méthodes et parce que les propriétés étendues sont appliquées sur tous les contrôles d'un certain type (avec filtrage par appel de CanExtend), il est nécessaire de posséder une Hashtable ou un Dictionary des contrôles pour lesquels l'extender fournit des propriétés étendues. En effet, la propriété étendue ne fait pas partie du contrôle mais uniquement de l'extender qui manipule le contrôle.

En tout cas, il est souvent nécessaire de garder une liste des contrôles sur lesquels on a défini une propriété étendue. Il conviendra donc d'ajouter à la liste tout nouveau contrôle passé à la méthode SetPropertyName.

Par exemple :

/// <summary>
/// Liste des instances de contrôles pour lesquels on fournit une propriété
/// </summary>
private Dictionary<Control, ControlTextDrawer> ctrlTexts = new Dictionary<Control, ControlTextDrawer>();

Appliquer les attributs des propriétés classiques aux propriétés étenduess

Vous pouvez tout à fait appliquer les attributs de Design habituel (comme Description, Category, DefaultValue...) sur les propriétés étendues. Pour cela, il suffit simplement de mettre ces attributs sur la méthode GetPropertyName.

Cependant, il peut arriver que certains éditeurs (vous savez, les éditeurs : le DropDown qui se met sous la case dans la fenêtre de propriétés ou encore les dialogues que l'on obtient en cliquant sur le bouton « ... » qui se met dans cette même case) ne fonctionnent pas pour les propriétés étendues (par exemple, celui des icônes dans les ImageList). Mais rien ne vous empêche de refaire un UITypeEditor personnel pour une propriété étendue.

Par exemple :

[Category("Appearance") * 
[Description("Texte à afficher au milieu") * 
public string GetMyDrawnText(Control c)

Et maintenant, qu'est-ce que je fais avec les contrôles à qui j'ai fourni les propriétés étendues

Eh, bien, c'est à vous de voir ! Vous pouvez faire ce que vous voulez et surtout associer des handlers d'évènements afin de modifier le comportement des contrôles auxquels vous avez fourni des propriétés. Par exemple, vous pouvez capter les évènements Enter et Leave afin de changer la couleur de fond quand la souris passe sur chaque contrôle « connu », la couleur étant la valeur d'une propriété étendue affectée à celui-ci. En fait, capter les évènements du contrôle est le premier des deux moyens de modifier dynamiquement son comportement suivant les interactions de l'utilisateur.

Le second moyen est de sous-classer chaque contrôle connu. En effet, cela peut être nécessaire pour dessiner sur un contrôle, car la plupart des contrôles .Net standards sont, en fait, des wrappers des contrôles Windows du même nom et l'arrivée du message WM_PAINT dans la boucle de messages, ne déclenche pas l'évènement Paint puisque le contrôle se dessine sans l'aide de .Net. Par contre, la méthode WndProc d'un contrôle est toujours appelée pour tous les messages. La solution est donc de filtrer les messages pour dessiner au moment de WM_PAINT, dans notre exemple. Cela se réalise en créant une classe dérivant de NativeWindow et en associant chaque contrôle connu de l'extender à une instance de cette classe. Voir l'exemple dans la section suivante.

Vous pouvez aussi modifier les propriétés des contrôles au moment de l'appel de SetPropertyName, mais cela n'est possible qu'une seule fois sans l'intervention de l'utilisateur.

Il y a plusieurs moments où l'on peut modifier un contrôle ou associer un handler d'évènement :

  • Quand on définit la propriété étendue avec la méthode SetPropertyName (je vous rappelle qu'une instance du contrôle vous est passée avec la valeur de la propriété). Il faudra toutefois vérifier que l'instance que l'on vous passe n'est pas « null » / « Nothing ».
  • Si vous implémentez ISupportInitialize, alors vous pouvez modifier les contrôles que vous avez stockés dans votre liste interne (construite dans SetPropertyName) dans la méthode EndInit qui est appelée à la fin de la méthode InitializeComponent.
  • Par le biais de « sender » dans les handlers que vous avez enregistrés dans l'un des deux cas précédents. En effet, le paramètre « sender » d'un handler reçoit toujours l'instance du contrôle qui a déclenché l'évènement.

Exemple

using System;
using System.ComponentModel;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Windows.Forms;
using System.Drawing;

namespace LesDesignersLib
{
/// <summary>
/// Démontre l'utilisation d'un extender/provider
/// (ProvideProperty) permet de préciser sur quels types de composant
/// on fournit une propriété dont on passe aussi le nom
/// pour fournir la propriété on créera
/// une méthode GetNomPropriété/SetNomPropriété
/// </summary>
[ProvideProperty("MyDrawnText",typeof(TextBox))]
public partial class ExtenderComponent : Component,IExtenderProvider
{
    /// <summary>
    /// Liste des instances de contrôles pour lesquels on fournit une propriété
    /// </summary>
    private Dictionary<Control, ControlTextDrawer> ctrlTexts =
    new Dictionary<Control, ControlTextDrawer>();

    public ExtenderComponent()
    {
        InitializeComponent();
    }

    public ExtenderComponent(IContainer container)
    {
        container.Add(this);

        InitializeComponent();
    }

    /// <summary>
    /// Définit le getter de la propriété fournie
    /// Pour appliquer des attributs à la propriété fournie,
    /// on doit les appliquer à cette méthode
    /// </summary>
    /// <param name="c">Contrôle (ou composant) pour lequel
    /// on va renvoyer la valeur de la propriété</param>
    /// <returns>renvoie la valeur de la propriété
    /// fournie pour le contrôle donné</returns>
    [Category("Appearance")]
    [Description("Texte à afficher au milieu")]
    public string GetMyDrawnText(Control c)
    {
        //récupère la valeur dans la collection de nos contrôles
        ControlTextDrawer ret = null;
        ctrlTexts.TryGetValue(c, out ret);
        if (ret != null)
            return ret.Text;
        else
            return null;
    }

    /// <summary>
    /// Définit le setter de la propriété fournie
    /// </summary>
    /// <param name="c">Contrôle (ou composant) pour lequel on va définir
    /// la valeur de la propriété</param>
    /// <param name="value">la valeur de la propriété fournie
    /// pour le contrôle donné</param>
    public void SetMyDrawnText(Control c, string value)
    {
        if (c != null)
        {
            //si on affecte, null, on retire le contrôle de notre collection
            if (value == null)
            {
                //si notre collection contient notre contrôle
                if (ctrlTexts.ContainsKey(c))
                {
                    //on arrête de traiter le contrôle
                    ctrlTexts[c].ReleaseHandle();
                    ctrlTexts.Remove(c);
                }
            }
            //sinon, si on affecte une valeur
            else
            {
                //si le contrôle n'est pas présent dans la collection
                if (!ctrlTexts.ContainsKey(c))
                {
                    //on l'ajoute avec la valeur
                    ctrlTexts.Add(c, new ControlTextDrawer(c,value));
                }
                else
                {
                    //sinon, on modifie
                    ctrlTexts[c].Text = value;
                }
            }
        }
    }

    /// <summary>
    /// Permet de définir si la propriété étendue MyDrawnText
    /// pour un contrôle doit être sérialisée
    /// </summary>
    /// <param name="c"></param>
    /// <returns></returns>
    private bool ShouldSerializeMyDrawnText(Control c)
    {
        return !string.IsNullOrEmpty(GetMyDrawnText(c));
    }
    /// <summary>
    /// Remet la valeur de la propriété à sa valeur par défaut
    /// </summary>
    /// <param name="c"></param>
    private void ResetMyDrawnText(Control c)
    {
        SetMyDrawnText(c, null);
    }

    #region IExtenderProvider Members

    /// <summary>
    /// Indique quels contrôles/composants auront la propriété étendue
    /// </summary>
    /// <param name="extendee"></param>
    /// <returns></returns>
    public bool CanExtend(object extendee)
    {
        if (extendee != null && extendee is TextBox)
            return string.IsNullOrEmpty(((TextBox)extendee).Text);
        else
            return false;
    }

    #endregion

    /// <summary>
    /// Permet de dessiner un texte au milieu d'un autre contrôle
    /// </summary>
    private class ControlTextDrawer : NativeWindow
    {
        private const int WM_PAINT = 0xF;

        private string m_Text;
        private Control m_Control;

        /// <summary>
        /// Le contrôle sur lequel on dessine
        /// </summary>
        public Control Control
        {
            get { return m_Control; }
        }
            
        /// <summary>
        /// Le texte à dessiner
        /// </summary>
        public string Text
        {
            get { return m_Text; }
            set { m_Text = value; }
        }

        /// <summary>
        /// Construit cette classe avec le contrôle
        /// sur lequel dessiner et le texte à dessiner
        /// </summary>
        /// <param name="c"></param>
        /// <param name="text"></param>
        public ControlTextDrawer(Control c,string text)
        {
            this.m_Control = c;
            this.Text = text;
            //commence le sous classement du contrôle
            this.AssignHandle(c.Handle);

            //permet d'être averti du changement de handle
            //du contrôle sous classé
            c.HandleCreated += new EventHandler(c_HandleCreated);
        }

        void c_HandleCreated(object sender, EventArgs e)
        {
            //remet le sous classement
            this.AssignHandle(((Control)sender).Handle);
        }

        public override void DestroyHandle()
        {
            this.ReleaseHandle();
            base.DestroyHandle();
        }

        /// <summary>
        /// Permet d'obtenir les messages envoyés au contrôle
        /// </summary>
        /// <param name="m"></param>
        protected override void WndProc(ref Message m)
        {
            //on dessine le texte au milieu
            if (m.Msg == WM_PAINT)
            {
                //dessine le contrôle
                base.WndProc(ref m);

                //récupère un objet Graphics
                Graphics g = Graphics.FromHwnd(m.HWnd);
                Font f = new Font(FontFamily.GenericSansSerif, (int)(this.Control.ClientSize.Height * 0.8));
                SizeF sf = g.MeasureString(this.Text, f);
                Brush b = new SolidBrush(Color.HotPink);
                g.DrawString(this.Text, f, b, (this.Control.ClientSize.Width - sf.Width) / 2, (this.Control.ClientSize.Height - sf.Height) / 2);
                b.Dispose();
                g.Dispose();
            }
            else
                base.WndProc(ref m);
            }
        }
    }
}

Gestion des valeurs par défaut et effet de bord

Tout d'abord, précisons que par « valeur par défaut », le designer entend « contrôle de la sérialisation », c'est-à-dire le contrôle de la présence du code de définition de la propriété dans InitializeComponent. Si la valeur par défaut est égale à la valeur de la propriété, alors aucun code de définition de la propriété ne sera créé.

Gestion des valeurs par défaut

Vous pouvez aussi avoir, pour une propriété étendue, une valeur par défaut statique ou dynamique, en contrôler la sérialisation ou en contrôler la remise à la valeur par défaut.

Pour cela, il y a les deux méthodes traditionnelles (qui peuvent être privées car elles ne servent qu'au Designer, par réflexion) :

  • ShouldSerializePropertyName qui permet de dire si la propriété doit être sérialisée ou plus précisément, renvoie true si la valeur actuelle de la propriété est la valeur par défaut, false sinon (on ne sérialise en général que les propriétés qui n'ont pas une valeur par défaut).
  • ResetPropertyName qui permet de remettre une propriété à sa valeur par défaut uniquement si ShouldSerializePropertyName a renvoyé true (sinon, ça ne servirait à rien). Quand vous ferez un click droit sur une propriété (dans la fenêtre des propriétés), vous verrez apparaître une commande « Reset ».

Contrairement à leur version simple (pour une propriété) qui ne prend pas de paramètre, la version de ces méthodes pour les propriétés étendues doit prendre en paramètre un argument du type des contrôles qui peuvent recevoir la propriété. Ceci est nécessaire tout comme pour les méthodes GetPropertyName/SetPropertyName.

Si la valeur par défaut est une constante, on peut utiliser l'attribut DefaultValue (à placer sur la méthode GetPropertyName).

/// <summary>
/// Permet de définir si la propriété étendue MyDrawnText
/// pour un contrôle doit être sérialisée
/// </summary>
/// <param name="c"></param>
/// <returns></returns>
private bool ShouldSerializeMyDrawnText(Control c)
{
    return !string.IsNullOrEmpty(GetMyDrawnText(c));
}
/// <summary>
/// Remet la valeur de la propriété à sa valeur par défaut
/// </summary>
/// <param name="c"></param>
private void ResetMyDrawnText(Control c)
{
    SetMyDrawnText(c, null);
}

Effet de bord : corrélation entre variable interne, valeur par défaut et sérialisationn

Il est important de définir, en interne, la valeur par défaut en conformité avec la valeur par défaut que l'on indique au Designer, sans quoi, on risque de se retrouver dans un état incohérent. Au sens du Designer, le terme « par défaut » indique, non pas la valeur assignée à la propriété quand le contrôle est posé sur la Form (celle-ci est définie par la valeur renvoyée par la propriété quand le contrôle vient d'être instancié), mais bien si la propriété est affectée (sérialisée) ou pas. Par exemple, si une propriété A renvoie 15 et que sa valeur par défaut est 15, elle ne sera par sérialisée (donc absente) d'InitializeComponent. Par contre, si vous avez indiqué 15 avec l'attribut DefaultValue mais que vous n'avez pas affecté 15 à la variable membre interne à la construction du composant, une ligne affectant 0 à la propriété A sera ajoutée dans InitializeComponent.

Effet de bord de la valeur par défaut sur les propriétés étendues

Le principal effet de bord des valeurs par défaut sur les propriétés étendues est que, si vous spécifiez une valeur par défaut et que la valeur de la propriété étendue est égale pour un contrôle donné, aucune ligne de code n'est ajoutée (sérialisée) à InitializeComponent.

Il en résulte que votre extender n'a plus connaissance dudit contrôle et ne peut donc plus jamais agir dessus. Ceci peut se produire dans les cas suivants :

Si vous avez un attribut DefaultValue et que la valeur affectée dans la fenêtre de propriétés est égale à cette DefaultValue (la valeur n'est plus en gras dans la case).
Si vous renvoyez False dans une éventuelle ShouldSerializePropertyName.
Si vous avez implémenté une méthode ResetPropertyName et qu'un reset est effectué.

Il y a deux solutions à ce problème :

  • Ne pas définir de valeur par défaut, ni de Reset.
  • Définir une autre propriété étendue de type Boolean qui n'a pas de valeur par défaut et pour laquelle :
    • La méthode GetPropertyName renvoie toujours true.
    • La méthode SetPropertyName ne fait rien d'autre que d'installer les handlers d'évènements éventuels sur le contrôle passé.
    • On définit une méthode ShouldSerializePropertyName qui renvoie toujours true (ceci afin de toujours générer une ligne d'affectation de la propriété dans InitializeComponent).

Changer le nom de propriété affichée dans la fenêtre des propriétés

On peut utiliser l'attribut DisplayName en lui passant une chaîne contenant le nom à donner à la propriété, afin de spécifier le nom que portera la propriété dans la fenêtre de propriétés. Cela permet, par exemple, de ne pas avoir des propriétés d'extender portant le nom : « NomPropriété ou NomExtender » mais simplement « NomPropriété ».

[DisplayName("MyDrawnText") * 
public string GetMyDrawnText(Control c)

Composants extensibles

La seule limitation des extenders/providers est que l'objet étendu pour lequel l'extender fournit des propriétés, doit dériver de IComponent. Cela regroupe donc tout objet qui s'affiche dans la surface de Design. Bien sûr ce n'est pas le cas d'une simple classe.

On peut donc étendre :

  • des classes implémentant IComponent, interface de base des composants Designables.
  • des classes dérivant de Component qui est une implémentation de IComponent.
  • des classes dérivant de Control qui est une dérivation graphique de Component.
  • des classes dérivant de UserControl.
  • des les ToolStrip, ToolStripItem, MenuStrip, ToolStripMenuItem...

Les Extenders en résumé

Pour ajouter des propriétés à un type de contrôles, vous devez suivre les étapes suivantes :

  • Implémenter l'interface IExtenderProvider et sa méthode « Boolean CanExtend(Object) ».
  • Ajouter autant d'attributs ProvideProperty à la déclaration de votre classe que de propriétés étendues à fournir.
  • Ecrire deux méthodes permettant d'accéder à chacune des propriétés PropertyName que vous fournissez (on peut utiliser Object à la place de Control) :
    • GetPropertyName(Control)
    • SetPropertyName(Control, String)

Références

Getting to know IExtenderProvider

ETENDRE LE TEXTBOX EN Y AJOUTANT UN TEXTE D'INVITER

Ce document intitulé « Les composants extenders ou providers de WinForm » issu de CodeS SourceS (codes-sources.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.
Rejoignez-nous