Windows Forms Extender Provider Sample

The following code sample provides an extender property for Windows Forms controls. For background information about developing an extender provider, see Implementing an Extender Provider.

To download the source code and to see how the sample is used, see the HelpLabel Sample in .NET Samples – Windows Forms: Control Authoring. The sample includes a nested designer that is described in the Windows Forms Designer Sample.

The sample demonstrates the following points:

  • The extender provider HelpLabel implements IExtenderProvider.
  • HelpLabel is itself a Windows Forms control and hence derives from Control.
  • The CanExtend method returns true for any control except HelpLabel (because it is not meaningful to extend a property on itself).
  • HelpLabel has a method named GetHelpText that gets the property that HelpLabel makes available to other controls. The SetHelpText method sets the value of the property. Note that the extended property is provided by the GetHelpText and SetHelpText methods and HelpLabel does not expose a property named HelpText.
Option Strict
Option Explicit
Imports System
Imports System.Collections
Imports System.ComponentModel
Imports System.ComponentModel.Design
Imports System.Drawing
Imports System.Windows.Forms
Imports System.Windows.Forms.Design

Namespace Microsoft.Samples.WindowsForms.Vb.HelpLabel
   '
   ' <doc>
   ' <desc>
   ' Help Label offers an extender property called
   ' HelpText.  It monitors the active control
   ' and displays the help text for the active control.
   ' </desc>
   ' </doc>
   '

   <ProvideProperty("HelpText", GetType(Control)), Designer(GetType(HelpLabel.HelpLabelDesigner))>  _
   Public Class HelpLabel
      Inherits Control
      Implements System.ComponentModel.IExtenderProvider
      ' <summary>
      '    Required designer variable.
      ' </summary>
      Private components As System.ComponentModel.Container
      Private helpTexts As Hashtable
      Private activeControl As System.Windows.Forms.Control
      
      '
      ' <doc>
      ' <desc>
      '      Creates a new help label object.
      ' </desc>
      ' </doc>
      '
      Public Sub New()
         '
         ' Required for Windows Form designer support.
         '
         InitializeComponent()
         helpTexts = New Hashtable()
      End Sub
      
      ' <summary>
      '    Clean up any resources being used.
      ' </summary>
      Protected  Overloads Overrides Sub Dispose(disposing As Boolean)
        If disposing
         If Not components Is Nothing
            components.Dispose()
         End If
        End If
         MyBase.Dispose(disposing)
      End Sub
      
      ' <summary>
      '    Required method for designer support. Do not modify
      '    the contents of this method with the code editor.
      ' </summary>
      Private Sub InitializeComponent()
         Me.components = New System.ComponentModel.Container()
         Me.BackColor = System.Drawing.SystemColors.Info
         Me.ForeColor = System.Drawing.SystemColors.InfoText
         Me.TabStop = False
      End Sub
      '
      ' <doc>
      ' <desc>
      '      Overrides the text property of Control.  This label ignores
      '      the text property, so we add additional attributes here so the
      '      property does not show up in the Properties window and is not
      '      persisted.
      ' </desc>
      ' </doc>
      '
      <Browsable(False), _
      EditorBrowsable(EditorBrowsableState.Never), _
      DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)> _
      Public Overrides Property [Text]() As String
         Get
            Return MyBase.Text
         End Get
         Set
            MyBase.Text = value
         End Set
      End Property
      
      
      '
      ' <doc>
      ' <desc>
      '      This implements the IExtenderProvider.CanExtend method.  The
      '      help label provides an extender property, and the design-time
      '      framework will call this method once for each component to determine
      '      if we are interested in providing our extended properties for the
      '      component.  We return true here if the object is a control and is
      '      not a HelpLabel (because it would not be meaningful to add this property to
      '      ourselves).
      ' </desc>
      ' </doc>
      '
      Function CanExtend(target As Object) As Boolean Implements IExtenderProvider.CanExtend
         If TypeOf target Is Control And Not TypeOf target Is HelpLabel Then
            
            Return True
         End If
         Return False
      End Function

      '
      ' <doc>
      ' <desc>
      '      This is the extended property for the HelpText property.  Extended
      '      properties are actual methods because they take an additional parameter
      '      that is the object or control to provide the property for.
      ' </desc>
      ' </doc>
      '
      <DefaultValue("")> _
      Public Function GetHelpText(ctrl As Control) As String
         Dim myText As String = CStr(helpTexts(ctrl))
         If myText Is Nothing Then
            myText = String.Empty
         End If
         Return myText
      End Function

      '
      ' <doc>
      ' <desc>
      '      This is the extended property for the HelpText property.
      ' </desc>
      ' </doc>
      '
      Public Sub SetHelpText(ctrl As Control, value As String)
         If value Is Nothing Then
            value = String.Empty
         End If
         
         If value.Length = 0 Then
            helpTexts.Remove(ctrl)
            
            RemoveHandler ctrl.Enter, AddressOf OnControlEnter
            RemoveHandler ctrl.Leave, AddressOf OnControlLeave
         Else
            helpTexts(ctrl) = value
            AddHandler ctrl.Enter, AddressOf OnControlEnter
            AddHandler ctrl.Leave, AddressOf OnControlLeave
         End If
         
         If ctrl Is activeControl Then
            Invalidate()
         End If
      End Sub

      '
      ' <doc>
      ' <desc>
      '      This is an event handler that responds to the OnControlEnter
      '      event.  We attach this to each control we are providing help
      '      text for.
      ' </desc>
      ' </doc>
      '
      Private Sub OnControlEnter(sender As Object, e As EventArgs)
         activeControl = CType(sender, Control)
         Invalidate()
      End Sub

      '
      ' <doc>
      ' <desc>
      '      This is an event handler that responds to the OnControlLeave
      '      event.  We attach this to each control we are providing help
      '      text for.
      ' </desc>
      ' </doc>
      '
      Private Sub OnControlLeave(sender As Object, e As EventArgs)
         If sender Is activeControl Then
            activeControl = Nothing
            Invalidate()
         End If
      End Sub

      '
      ' <doc>
      ' <desc>
      '      Overrides Control.OnPaint.  Here we draw our
      '      label.
      ' </desc>
      ' </doc>
      '
      Protected Overrides Sub OnPaint(pe As PaintEventArgs)
         ' Let the base draw.  This will cover our back
         ' color and set any image that the user has
         ' provided.
         '
         MyBase.OnPaint(pe)
         
         ' Draw a rectangle around the control.
         '
         Dim rect As Rectangle = ClientRectangle
         
         Dim borderPen As New Pen(ForeColor)
         pe.Graphics.DrawRectangle(borderPen, rect)
         borderPen.Dispose()
         
         ' Finally, draw the text over the top of the
         ' rectangle.
         '
         If Not (activeControl Is Nothing) Then
            Dim myText As String = CStr(helpTexts(activeControl))
            If Not (myText Is Nothing) And myText.Length > 0 Then
               rect.Inflate(- 2, - 2)
               Dim brush As New SolidBrush(ForeColor)
               pe.Graphics.DrawString(myText, Font, brush, RectangleF.op_Implicit(rect))
               brush.Dispose()
            End If
         End If
      End Sub


      ' <doc>
      ' <desc>
      '     Returns true if backColor should be persisted in code gen.  We
      '      override this because we change the default back color.
      ' </desc>
      ' <retvalue>
      '     true if the backColor should be persisted.
      ' </retvalue>
      ' </doc>
      '
      Public Function ShouldSerializeBackColor() As Boolean
         Return Not BackColor.Equals(SystemColors.Info)
      End Function


      ' <doc>
      ' <desc>
      '     Returns true if foreColor should be persisted in code gen.  We
      '      override this because we change the default foreground color.
      ' </desc>
      ' <retvalue>
      '     true if foreColor should be persisted.
      ' </retvalue>
      ' </doc>
      '
      Public Function ShouldSerializeForeColor() As Boolean
         Return Not ForeColor.Equals(SystemColors.InfoText)
      End Function

      '
      ' <doc>
      ' <desc>
      '      This is a designer for the HelpLabel.  This designer provides
      '      design time feedback for the label.  The help label responds
      '      to changes in the active control, but these events do not
      '      occur at design time.  In order to provide some usable feedback
      '      that the control is working the right way, this designer listens
      '      to selection change events and uses those events to trigger active
      '      control changes.
      ' </desc>
      ' </doc>
      '
      Public Class HelpLabelDesigner
         Inherits System.Windows.Forms.Design.ControlDesigner
         
         Private _trackSelection As Boolean = True
         
         ' <summary>
         ' This property is added to the control's set of properties in the method
         ' PreFilterProperties below.  Note that on designers, properties that are
         ' explictly declared by TypeDescriptor.CreateProperty can be declared as
         ' private on the designer.  This helps to keep the designer's public
         ' object model clean.
         ' </summary>
         Private Property TrackSelection() As Boolean
            Get
               Return _trackSelection
            End Get
            Set
               _trackSelection = value
               If _trackSelection Then
                  Dim ss As ISelectionService = CType(GetService(GetType(ISelectionService)), ISelectionService)
                  If Not (ss Is Nothing) Then
                     UpdateHelpLabelSelection(ss)
                  End If
               Else
                  Dim helpLabel As HelpLabel = CType(Control, HelpLabel)
                  If Not (helpLabel.activeControl Is Nothing) Then
                     helpLabel.activeControl = Nothing
                     helpLabel.Invalidate()
                  End If
               End If
            End Set
         End Property
         
         Public Overrides ReadOnly Property Verbs() As DesignerVerbCollection
            Get
               Dim myVerbs() As DesignerVerb = {New DesignerVerb("Sample Verb", AddressOf OnSampleVerb)}
               Return New DesignerVerbCollection(myVerbs)
            End Get
         End Property
         
         '
         ' <doc>
         ' <desc>
         '      Overrides Dispose.  Here we remove our handler for the selection changed
         '      event.  With designers, it is critical that they clean up any events they
         '      have attached.  Otherwise, during the course of an editing session many
         '      designers might get created and never destroyed.
         ' </desc>
         ' </doc>
         '
         Protected Overloads Overrides Sub Dispose(disposing As Boolean)
          If disposing Then
            Dim ss As ISelectionService = CType(GetService(GetType(ISelectionService)), ISelectionService)
            If Not (ss Is Nothing) Then
               RemoveHandler ss.SelectionChanged, AddressOf OnSelectionChanged
            End If
          End If
            MyBase.Dispose(disposing)
         End Sub
         
         '
         ' <doc>
         ' <desc>
         '       Overrides initialize.  Here we add an event handler to the selection service.
         '      Notice that we are very careful not to assume that the selection service is
         '      available.  It is entirely optional that a service is available and you should
         '      always degrade gracefully if a service cannot be found.
         ' </desc>
         ' </doc>
         '
         Public Overrides Sub Initialize(component As IComponent)
            MyBase.Initialize(component)
            
            Dim ss As ISelectionService = CType(GetService(GetType(ISelectionService)), ISelectionService)
            If Not (ss Is Nothing) Then
               AddHandler ss.SelectionChanged, AddressOf OnSelectionChanged
            End If
         End Sub
         
         Private Sub OnSampleVerb(sender As Object, e As EventArgs)
            MessageBox.Show("You have just invoked a sample verb.  Normally, this would do something interesting.")
         End Sub
         
         '
         ' <doc>
         ' <desc>
         '      The handler for the selection change event.  Here we update the active control within
         '      the help label.
         ' </desc>
         ' </doc>
         '
         Private Sub OnSelectionChanged(sender As Object, e As EventArgs)
            If _trackSelection Then
               Dim ss As ISelectionService = CType(sender, ISelectionService)
               UpdateHelpLabelSelection(ss)
            End If
         End Sub
         
         Protected Overrides Sub PreFilterProperties(properties As IDictionary)
            ' Always call base first in PreFilter* methods, and last in PostFilter*
            ' methods.
            MyBase.PreFilterProperties(properties)
            
            ' We add a design-time property called TrackSelection that is used to track
            ' the active selection.  If the user sets this to true (the default), then
            ' we will listen to selection change events and update the control's active
            ' control to point to the current primary selection.
            properties("TrackSelection") = TypeDescriptor.CreateProperty( _
               Me.GetType(), _
               "TrackSelection", _
               GetType(Boolean), _
               New Attribute() {CategoryAttribute.Design})
         End Sub

         ' <summary>
         ' This is a helper method that, given a selection service, will update the active control
         ' of the help label with the currently active selection.
         ' </summary>
         ' <param name="ss"></param>
         Private Sub UpdateHelpLabelSelection(ss As ISelectionService)
            Dim c As Control = CType(ss.PrimarySelection, Control)
            Dim helpLabel As HelpLabel = CType(Control, HelpLabel)
            If Not (c Is Nothing) Then
               helpLabel.activeControl = c
               helpLabel.Invalidate()
            Else
               If Not (helpLabel.activeControl Is Nothing) Then
                  helpLabel.activeControl = Nothing
                  helpLabel.Invalidate()
               End If
            End If
         End Sub
      End Class
   End Class
End Namespace
[C#]
namespace Microsoft.Samples.WindowsForms.Cs.HelpLabel {
    using System;
    using System.Collections;
    using System.ComponentModel;
    using System.ComponentModel.Design;
    using System.Drawing;
    using System.Windows.Forms;
    using System.Windows.Forms.Design;

    //
    // <doc>
    // <desc>
    // Help Label offers an extender property called
    // HelpText.  It monitors the active control
    // and displays the help text for the active control.
    // </desc>
    // </doc>
    //
    [
    ProvideProperty("HelpText",typeof(Control)),
    Designer(typeof(HelpLabel.HelpLabelDesigner))
    ]
    public class HelpLabel : Control, System.ComponentModel.IExtenderProvider {
        /// <summary>
        ///    Required designer variable.
        /// </summary>
        private System.ComponentModel.Container components;
        private Hashtable helpTexts;
        private System.Windows.Forms.Control activeControl;

        //
        // <doc>
        // <desc>
        //      Creates a new help label object.
        // </desc>
        // </doc>
        //
        public HelpLabel() {
            //
            // Required for Windows Forms designer support.
            //
            InitializeComponent();

            helpTexts = new Hashtable();
        }

        /// <summary>
        ///    Clean up any resources being used.
        /// </summary>
        protected override void Dispose(bool disposing) {
   if (disposing) {
        if (components != null) {
            components.Dispose();
        }
   }
   base.Dispose(disposing);
}

        /// <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.BackColor = System.Drawing.SystemColors.Info;
            this.ForeColor = System.Drawing.SystemColors.InfoText;
            this.TabStop = false;
        }
        //
        // <doc>
        // <desc>
        //      Overrides the text property of Control.  This label ignores
        //      the text property, so add additional attributes here so the
        //      property does not show up in the Properties window and is not
        //      persisted.
        // </desc>
        // </doc>
        //
        [
        Browsable(false), 
        EditorBrowsable(EditorBrowsableState.Never),       DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)
        ]
        public override string Text {
            get {
                return base.Text;
            }
            set {
                base.Text = value;
            }
        }

        //
        // <doc>
        // <desc>
        //      This implements the IExtenderProvider.CanExtend method.  The
        //      help label provides an extender property, and the design-time
        //      framework will call this method once for each component to determine
        //      if we are interested in providing our extended properties for the
        //      component.  We return true here if the object is a control and is
        //      not a HelpLabel (because it would not be meaningful to add this property to
        //      ourselves).
        // </desc>
        // </doc>
        //
        bool IExtenderProvider.CanExtend(object target) {
            if (target is Control &&
                !(target is HelpLabel)) {

                return true;
            }
            return false;
        }

        //
        // <doc>
        // <desc>
        //      This is the extended property for the HelpText property.  Extended
        //      properties are actual methods because they take an additional parameter
        //      that is the object or control to provide the property for.
        // </desc>
        // </doc>
        //
        [
        DefaultValue("")
        ]
        public string GetHelpText(Control control) {
            string text = (string)helpTexts[control];
            if (text == null) {
                text = string.Empty;
            }
            return text;
        }

        //
        // <doc>
        // <desc>
        //      This is the extended property for the HelpText property.
        // </desc>
        // </doc>
        //
        public void SetHelpText(Control control, string value) {
            if (value == null) {
                value = string.Empty;
            }

            if (value.Length == 0) {
                helpTexts.Remove(control);

                control.Enter -= new EventHandler(OnControlEnter);
                control.Leave -= new EventHandler(OnControlLeave);
            }
            else {
                helpTexts[control] = value;

                control.Enter += new EventHandler(OnControlEnter);
                control.Leave += new EventHandler(OnControlLeave);
            }

            if (control == activeControl) {
                Invalidate();
            }
        }
        
        //
        // <doc>
        // <desc>
        //      This is an event handler that responds to the OnControlEnter
        //      event.  We attach this to each control we are providing help
        //      text for.
        // </desc>
        // </doc>
        //
        private void OnControlEnter(object sender, EventArgs e) {
            activeControl = (Control)sender;
            Invalidate();
        }

        //
        // <doc>
        // <desc>
        //      This is an event handler that responds to the OnControlLeave
        //      event.  Attach this to each control that we are providing help
        //      text for.
        // </desc>
        // </doc>
        //
        private void OnControlLeave(object sender, EventArgs e) {
            if (sender == activeControl) {
                activeControl = null;
                Invalidate();
            }
        }

        //
        // <doc>
        // <desc>
        //      Overrides Control.OnPaint.  Here we draw our
        //      label.
        // </desc>
        // </doc>
        //
        protected override void OnPaint(PaintEventArgs pe) {

            // Let the base draw.  This will cover our back
            // color and set any image that the user has
            // provided.
            //
            base.OnPaint(pe);

            // Draw a rectangle around our control.
            //
            Rectangle rect = ClientRectangle;

            Pen borderPen = new Pen(ForeColor);
            pe.Graphics.DrawRectangle(borderPen, rect);
            borderPen.Dispose();

            // Finally, draw the text over the top of the
            // rectangle.
            //
            if (activeControl != null) {
                string text = (string)helpTexts[activeControl];
                if (text != null && text.Length > 0) {
                    rect.Inflate(-2, -2);
                    Brush brush = new SolidBrush(ForeColor);
                    pe.Graphics.DrawString(text, Font, brush, rect);
                    brush.Dispose();
                }
            }
        }

        // <doc>
        // <desc>
        //     Returns true if the backColor should be persisted in code gen.  We
        //      override this because we change the default back color.
        // </desc>
        // <retvalue>
        //     true if backColor should be persisted.
        // </retvalue>
        // </doc>
        //
        public bool ShouldSerializeBackColor() {
            return(!BackColor.Equals(SystemColors.Info));
        }

        // <doc>
        // <desc>
        //     Returns true if foreColor should be persisted in code gen.  We
        //      override this because we change the default foreground color.
        // </desc>
        // <retvalue>
        //     true if the foreColor should be persisted.
        // </retvalue>
        // </doc>
        //
        public bool ShouldSerializeForeColor() {
            return(!ForeColor.Equals(SystemColors.InfoText));
        }

        //
        // <doc>
        // <desc>
        //      This is a designer for the HelpLabel.  This designer provides
        //      design-time feedback for the label.  The help label responds
        //      to changes in the active control, but these events do not
        //      occur at design time.  In order to provide some usable feedback
        //      that the control is working the right way, this designer listens
        //      to selection change events and uses those events to trigger active
        //      control changes.
        // </desc>
        // </doc>
        //
        public class HelpLabelDesigner : System.Windows.Forms.Design.ControlDesigner {

      private bool trackSelection = true;

      /// <summary>
      /// This property is added to the control's set of properties in the 
      /// PreFilterProperties method.  Note that on designers, properties that are
      /// explictly declared by TypeDescriptor.CreateProperty can be declared as
      /// private on the designer.  This helps to keep the designer's public
      /// object model clean.
      /// </summary>
      private bool TrackSelection
      {
        get
        {
          return trackSelection;
        }
        set
        {
          trackSelection = value;
          if (trackSelection)
          {
            ISelectionService ss = (ISelectionService)GetService(typeof(ISelectionService));
            if (ss != null)
            {
              UpdateHelpLabelSelection(ss);
            }
          }
          else
          {
            HelpLabel helpLabel = (HelpLabel)Control;
            if (helpLabel.activeControl != null)
            {
              helpLabel.activeControl = null;
              helpLabel.Invalidate();
            }
          }
        }
      }

      public override DesignerVerbCollection Verbs
      {
        get
        {
          DesignerVerb[] verbs = new DesignerVerb[] {
                                  new DesignerVerb("Sample Verb", new EventHandler(OnSampleVerb))
                                };
          return new DesignerVerbCollection(verbs);
        }
      }

            //
            // <doc>
            // <desc>
            //      Overrides Dispose.  Here we remove our handler for the selection changed
            //      event.  With designers, it is critical that they clean up any events they
            //      have attached.  Otherwise, during the course of an editing session many
            //      designers might get created and never destroyed.
            // </desc>
            // </doc>
            //
    protected override void Dispose(bool disposing) {
               if (disposing){
                   ISelectionService ss = (ISelectionService)GetService(typeof(ISelectionService));
                   if (ss != null) {
                       ss.SelectionChanged -= new EventHandler(OnSelectionChanged);
                   }
                }

         base.Dispose(disposing);
     }

            //
            // <doc>
            // <desc>
            //       Overrides initialize.  Here we add an event handler to the selection service.
            //      Notice that we are very careful not to assume that the selection service is
            //      available.  It is entirely optional that a service is available and you should
            //      always degrade gracefully if a service cannot be found.
            // </desc>
            // </doc>
            //
            public override void Initialize(IComponent component) {
                base.Initialize(component);

                ISelectionService ss = (ISelectionService)GetService(typeof(ISelectionService));
                if (ss != null) {
                    ss.SelectionChanged += new EventHandler(OnSelectionChanged);
                }
            }

      private void OnSampleVerb(object sender, EventArgs e)
      {
        MessageBox.Show("You have just invoked a sample verb.  Normally, this would do something interesting.");
      }

            //
            // <doc>
            // <desc>
            //      Our handler for the selection change event.  Here we update the active control within
            //      the help label.
            // </desc>
            // </doc>
            //
            private void OnSelectionChanged(object sender, EventArgs e) {
        if (trackSelection)
        {
          ISelectionService ss = (ISelectionService)sender;
          UpdateHelpLabelSelection(ss);
        }
            }

      protected override void PreFilterProperties(IDictionary properties)
      {
        // Always call base first in PreFilter* methods, and last in PostFilter*
        // methods.
        base.PreFilterProperties(properties);

        // We add a design-time property called TrackSelection that is used to track
        // the active selection.  If the user sets this to true (the default), then
        // we will listen to selection change events and update the control's active
        // control to point to the current primary selection.
        properties["TrackSelection"] = TypeDescriptor.CreateProperty(
          this.GetType(),   // the type this property is defined on
          "TrackSelection", // the name of the property
          typeof(bool),   // the type of the property
          new Attribute[] {CategoryAttribute.Design});  // attributes
      }

      /// <summary>
      /// This is a helper method that, given a selection service, will update the active control
      /// of our help label with the currently active selection.
      /// </summary>
      /// <param name="ss"></param>
      private void UpdateHelpLabelSelection(ISelectionService ss)
      {
        Control c = (Control)ss.PrimarySelection;
        HelpLabel helpLabel = (HelpLabel)Control;
        if (c != null)
        {
          helpLabel.activeControl = c;
          helpLabel.Invalidate();
        }
        else
        {
          if (helpLabel.activeControl != null)
          {
            helpLabel.activeControl = null;
            helpLabel.Invalidate();
          }
        }
      }
        }
    }
} 

See Also

Implementing an Extender Provider | Design-Time Support for Windows Forms