Search This Blog

Walkthrough on how to create a Control Extender in C#

In this Post I will discuss the steps required to create a a Extender Provider using C#.

On msdn Extender Provider is defined in the following terms:
"An extender provider is a component that provides properties to other components. For example, the ToolTip control is implemented as an extender provider. When you add a ToolTip control to a Form, all other controls on the Form have a ToolTip property added to their properties list."

We will create an Extender Provider that will provide the following properties to a Form controls.
HelpMessage: a string that will hold any description or helpfull information about the extendee
ErrorMessage: a string that will hold an error description that we want to report to end users about an invalid control value.

To create an Extender Provider component you have to basically:

  1. Implements IExtenderProvider interface.
  2. use ProvidePropertyAttribute to specify the name of the provided property, as well as the type of components that may receive the property.
IExtenderProvider: this is a simple interface with one method to implement, CanExtend(object extendee) with a boolean return value to indicate if the extendee parameter can be extended through your Extender Provider.

ProvidePropertyAttributeSpecifies the name of the property that an implementer of IExtenderProvider offers to other components. (msdn)


Here is some C# example of ProvidePropertyAttribute usage.



[ProvideProperty("DisplayName", typeof(Control))]
public class ControlDisplayNameProvider : IExtenderProvider 
{
    Hashtable displayNames = new Hashtable();   
    // Provides the Get portion of DisplayName. 
    public string GetDisplayName(Control myControl) {
        return displayNames[myControl] as string ;
    }

    // Provides the Set portion of MyProperty.
    public void SetDisplayName(Control myControl, string value) {
       displayNames[myControl] = value;
    }

    /* When you inherit from IExtenderProvider, you must implement the 
     * CanExtend method. */
    public bool CanExtend(Object target) {
        // This class can extend only control objects.
        return(target is Control);
    }

 }

Here are some test screenshots for the control at run time.
Control Runtime Settings are:
AutoValidateForm = true,
ValidateControlData event has been subscribed to and implemted the following way in this example.


private bool helpLabel1_ValidateControlData(Control control)
{
   bool valid = true;
   if (control is TextBox)
   {
       valid = !String.IsNullOrEmpty(((TextBox)control).Text);
   }
   else if (control is ListControl)
   {
       valid = ((ListControl)control).SelectedIndex != ListBox.NoMatches;
   }
   return valid;
}

Figure1: Click on Ok with all fields empty.

Figure 2: Active control is the text box fir first name field. Error is displayed only for this field.

Figure3: Last name field is active an error is displayed for this field.

Figure4: First name field is populated and is active. Help text is displayed in this case.

Figure 5: All fields are Ok. Gender list box is the active control.

Let's get back to the control we want to implement. As described above the control will provide two properties to the extended objects.

using System;
using System.Collections;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Drawing;
using System.Windows.Forms;
using System.Windows.Forms.Design;
using System.Text;

namespace CustomControls
{
    // <doc>
    // <desc>
    // HelpAndValidationProvider offers two extender properties "HelpText" and "Errortext".  
    // It monitors the active control and displays the help text for the active control if it is valid or an error  
    // message if it's data is invalid.
    // </desc>
    // </doc>
    //
    /// <summary>
    /// HelpAndValidationLabel
    /// </summary>
    [
    ProvideProperty("HelpText", typeof(Control)),
    ProvideProperty("ErrorText", typeof(Control)),
    DefaultEvent("ValidateControlData")
    ]
    public class HelpAndValidationProvider : Control, System.ComponentModel.IExtenderProvider
    {
        /// <summary>
        ///    Required designer variable.
        /// </summary>
        private System.ComponentModel.Container components;
        private Hashtable helpTexts;
        private Hashtable errorTexts;
        private System.Windows.Forms.Control activeControl;

        //
        // <doc>
        // <desc>
        //      Creates a new help label object.
        // </desc>
        // </doc>
        //
        public HelpAndValidatiponLabel()
        {
            //
            // Required for Windows Form Designer support
            //
            InitializeComponent();
            helpTexts = new Hashtable();
            errorTexts = new Hashtable();
            AutoValidateFormChanged += new AutoValidateFormDelegate(AutoValidateFormChangedHandler);
        }

        void AutoValidateFormChangedHandler(bool oldValue, bool newValue)
        {
            if (this.TopLevelControl != null  && this.TopLevelControl is Form)
            {
                if (newValue)
                {
                    EnableAutoValidation();
                }
                else
                {
                    RemoveAutoValidation();
                }
            }
        }

        private void RemoveAutoValidation()
        {
            Form form = (Form)TopLevelControl;
            form.FormClosing -= new FormClosingEventHandler(FormClosing);
        }

        private void EnableAutoValidation()
        {
            Form form = (Form)TopLevelControl;
            form.FormClosing += new FormClosingEventHandler(FormClosing);
        }

        void FormClosing(object sender, FormClosingEventArgs e)
        {
            Form formSender = sender as Form;
             // Validate only on dialog Result equal to OK. If user clicks cancel than no validation is needed.
            if (formSender != null && formSender.DialogResult == DialogResult.OK)
            {
                if (e.CloseReason == CloseReason.UserClosing || e.CloseReason == CloseReason.None)
                {
                    string summary;
                    if (!IsFormValid(out summary))
                    {
                        e.Cancel = true;
                        summaryValidation = true;
                        this.Text = summary;
                        Invalidate();
                    }
                }
            }
        }

        bool summaryValidation = false;
        private bool IsFormValid(out string errorSummary)
        {
            bool formIsValid = true;
            StringBuilder errors = new StringBuilder();
            foreach (Control control in errorTexts.Keys)
            {
                if (control != null)
                {
                    if (!OnValidateControl(control))
                    {
                        formIsValid = false;
                        errors.AppendLine(errorTexts[control] as string);
                        // If the control does not have a wrapper panel add one to it.
                        if (control.Parent != null)
                        {
                            if (control.Parent is Panel && control.Parent.Name == control.Name + "Panel")
                            {
                                control.Parent.BackColor = Color.Red;
                            }
                            else
                            {
                                CreateWrapperPanel(control);
                            }
                        }

                    }
                }
            }
            errorSummary = errors.ToString();
            return formIsValid;
        }


        private delegate void AutoValidateFormDelegate(bool oldValue, bool newValue);
        private event AutoValidateFormDelegate AutoValidateFormChanged;

        private bool autoValidateForm;


        /// <summary>
        /// If set to true, the form will be validated upon user request to close the form, and close operation will be canceled
        /// if the form is not valid
        /// </summary>
        [DefaultValue(false)]
        [Browsable(false)]
        [Category("HelpAndValidationLabel")]
        [Description("Set this to true to enable auto validation, this has to be set after the control is added to it parent container, otherwise it will not work.")]
        public bool AutoValidateForm
        {
            get { return autoValidateForm; }
            set
            {
                if (autoValidateForm != value)
                {
                    AutoValidateFormChanged(autoValidateForm, value);
                }
                autoValidateForm = value;
            }
        }


        /// <summary>
        ///    Clean up any resources being used.
        /// </summary>
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                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 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 override string Text
        {
            get
            {
                return base.Text;
            }
            set
            {
                base.Text = value;
            }
        }



        //
        // <doc>
        // <desc>
        //      This implements the IExtenderProvider.CanExtend method.  The
        //      help and validation provider will provide two will provide HelpText and ErrorText properties 
        //      to control objects except control objects of type HelpAndValidationProvider.
        // </desc>
        // </doc>
        //
        bool IExtenderProvider.CanExtend(object target)
        {
            if (target is Control &&
                !(target is HelpAndValidationProvider))
            {
                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(""),
        Category("HelpAndValidationProvider"),
        ]
        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 ErrorText 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(""),
        Category("HelpAndValidationProvider")
        ]
        public string GetErrorText(Control control)
        {
            string text = (string)errorTexts[control];
            if (text == null)
            {
                text = string.Empty;
            }
            return text;
        }

        //
        // <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)
        {
            summaryValidation = false;
            activeControl = (Control)sender;
            if (activeControl != null)
            {
                error = !OnValidateControl(activeControl);
            }
            Invalidate();
        }

        public delegate bool ValidateControlDelegate(Control control);
        public event ValidateControlDelegate ValidateControlData;

        /// <summary>
        /// Validate a control and returns true if it is valid.
        /// </summary>
        /// <param name="control">Control to validate</param>
        /// <returns>True if the control data is valid</returns>
        protected virtual bool OnValidateControl(Control control)
        {
            bool valid = true;
            if (ValidateControlData != null)
            {
                valid = ValidateControlData(control);
            }
            return valid;
        }
        bool error = false;
        //
        // <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 void OnControlLeave(object sender, EventArgs e)
        {
            summaryValidation = false;
            if (sender == activeControl)
            {
                if (activeControl.Parent != null && activeControl.Parent is Panel &&
                    activeControl.Parent.Name == activeControl.Name + "Panel")
                {
                    bool err = !OnValidateControl(activeControl);
                    if (err == false)
                        activeControl.Parent.BackColor = Color.Transparent;
                    else
                        activeControl.Parent.BackColor = Color.Red;
                }
                activeControl = null;
                Invalidate();
            }
        }

        //
        // <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 the extended property for the HelpText property.
        // </desc>
        // </doc>
        //
        public void SetErrorText(Control control, string value)
        {
            if (value == null)
            {
                value = string.Empty;
            }

            if (value.Length == 0)
            {
                errorTexts.Remove(control);
            }
            else
            {
                errorTexts[control] = value;
            }
        }

        //
        // <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 may have
            // provided.
            //
            base.OnPaint(pe);
            if (summaryValidation)
            {
                Rectangle re = ClientRectangle;
                Rectangle r = ClientRectangle;
                r.Inflate(-1,-1);
                re.Inflate(-2, -2);
                Pen pen = new Pen(ForeColor, 2);
                pe.Graphics.DrawRectangle(pen, r);
                pen.Dispose();
               // The value of Text property will have the content of the summary validation string.
               // This is set during validation operation..
                pe.Graphics.DrawString(Text, Font, Brushes.Red, re);
                return;
            }

            // Draw a rectangle around our control.
            //
            Rectangle rect = ClientRectangle;
            rect.Width -= 1;
            rect.Height -= 1;
            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 = null;

                if (error)
                {
                    text = (string)errorTexts[activeControl];
                }
                else
                {
                    text = (string)helpTexts[activeControl];
                }

                if (!String.IsNullOrEmpty(text))
                {
                    rect.Inflate(-2, -2);
                    Brush brush = null;
                    if (!error)
                        brush = new SolidBrush(ForeColor);
                    else
                        brush = new SolidBrush(Color.Red);
                    pe.Graphics.DrawString(text, Font, brush, rect);
                    brush.Dispose();
                }
                // Surround the active contro with a red border.
                if (activeControl.Parent != null)
                {
                    if (activeControl.Parent is Panel &&
                    activeControl.Parent.Name == activeControl.Name + "Panel")
                    {
                        if (error)
                            activeControl.Parent.BackColor = Color.Red;
                        else
                            activeControl.Parent.BackColor = Color.Transparent;
                    }
                    else
                    {
                        if (error)
                        {
                            CreateWrapperPanel(activeControl);
                        }
                    }
                }




            }
        }
       /// <summary>
       /// This method is called to create a wrapper panel around the control  to be able to draw Error         
       /// rectangle around the control if it is in invalid state.
       /// </summary>
        private void CreateWrapperPanel(Control control)
        {
            this.SuspendLayout();
            Panel panel = new Panel();
            panel.Anchor = control.Anchor;
            Point location = control.Location;
            location.Offset(-2, -2);
            panel.Location = location;
            panel.Size = new Size(control.Size.Width + 4, control.Size.Height + 4);
            panel.Name = control.Name + "Panel";
            Control parent = control.Parent;
            parent.Controls.Add(panel);
            Control cachedActiveControl = control;
            panel.Controls.Add(control);
            cachedActiveControl.Location = new Point(2, 2);
            panel.BackColor = Color.Red;
            this.ResumeLayout(false);
            this.PerformLayout();
        }

        // <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 the backColor should be persisted.
        // </retvalue>
        // </doc>
        //
        public bool ShouldSerializeBackColor()
        {
            return (!BackColor.Equals(SystemColors.Info));
        }

        protected override void OnSizeChanged(EventArgs e)
        {
            Refresh();
        }

        // <doc>
        // <desc>
        //     Returns true if the 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 HelpAndValidationProvider.  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>
        //

        [System.Security.Permissions.PermissionSet(System.Security.Permissions.SecurityAction.Demand, Name = "FullTrust")]
        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 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 publi
            /// object model clean.
            /// </summary>
            [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
            private bool TrackSelection
            {
                get
                {
                    return trackSelection;
                }
                set
                {
                    trackSelection = value;
                    if (trackSelection)
                    {
                        ISelectionService ss = (ISelectionService)GetService(typeof(ISelectionService));
                        if (ss != null)
                        {
                            UpdateHelpLabelSelection(ss);
                        }
                    }
                    else
                    {
                        HelpAndValidationProvider helpLabel = (HelpAndValidationProvider )Control;
                        if (helpLabel.activeControl != null)
                        {
                            helpLabel.activeControl = null;
                            helpLabel.Invalidate();
                        }
                    }
                }
            }

            //
            // <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 may 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 could not 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 = ss.PrimarySelection as Control;
                HelpAndValidationProvider helpLabel = (HelpAndValidationProvider )Control;
                if (c != null)
                {
                    helpLabel.activeControl = c;
                    helpLabel.Invalidate();
                }
                else
                {
                    if (helpLabel.activeControl != null)
                    {
                        helpLabel.activeControl = null;
                        helpLabel.Invalidate();
                    }
                }
            }
        }
    }
}



1 comment:

  1. Excellent Work, Bill Woodruff

    By the way, selecting the 'Comment As' through Google Account take me to a page in Thai that appears, in translation to be about use of 'Blogger' : yet another Chrome feature ? :)

    ReplyDelete