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".
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 :
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(); }
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.
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))]
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.
Ensuite, il faut que vous définissiez deux méthodes GetPropertyName et SetPropertyName où PropertyName 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)
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>();
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)
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 :
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); } } } }
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éé.
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) :
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); }
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.
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 :
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)
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 :
Pour ajouter des propriétés à un type de contrôles, vous devez suivre les étapes suivantes :