Using a UserControl to combine related controls

Have you been writing an application where you needed to relate multiple controls to each other (ex: a Button and a TextBox) where multiple sets of the related controls are required?

The scenario
I have an application where I needed to have multiple Button controls which I needed to perform the same task.  This task was to read the value of a specific TextBox's Text property and call a method using that data.  My goals were to minimize redundant code and to not be required to create a new Click event handler whenever I added a TextBox / Button control pair to my form.

One of the best things about the Button (and other controls) click event is that your event handler is sent the object that generated the event in the sender argument.  I originally planned (and wrote) my click event handler such that it identified the sender (Button) and then examined each of its parent's controls to find the matching TextBox control.  After some trial and error, which underscored the fragility of my algorithm, I got the code working.  The code wasn't pretty, nor did it perform very well.  I could live with the code not being super elegant (heavy commenting helped) but I didn't like the performance characteristics (it didn't scale well) and the reliance upon adhering to specific naming conventions was a major issue.

One solution
Since I was writing my application using version 2 of the .NET Compact Framework, I decided to re-implement my solution as a UserControl.  UserControls are a great way to combine multiple UI controls such that the container (an application's form) can treat them as one unit with a unified public interface.  In a UserControl, you can specify how the default control properties (ex: Text) behave and define additional properties that make sense for your control.

And best of all, they are very easy to write using Visual Studio 2005.

My main goal in implementing this control was to make the control's click event be synonymous with the child Button's click event.  An alternative would be to expose the Button's click event as a new event (ex: ActionButtonClick) that a form could handle -- I have an example of this alternative following this discussion.

First, I added a new UserControl to my project (you can also create a separate project for your control).  Next, I used the designer to add the controls that will make up my UserControl.  After setting the properties of each child control (ex: Anchor settings), I was ready to add the code.  The example below implements a very simple Button / TextBox pair with a Label.

Please note that this is not a complete implementation.  I have left out the control designer's generated code (ex: InitializeComponent).

//--------------------------------------------------------------------- //THIS CODE AND INFORMATION ARE PROVIDED AS IS WITHOUT WARRANTY OF ANY //KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE //IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A //PARTICULAR PURPOSE. //---------------------------------------------------------------------using System;using System.ComponentModel;using System.Drawing;using System.Windows.Forms;public partial class LinkedButtonAndTextBox : UserControl{    /// <summary>    /// This variable helps to ensure that the button's click    /// is the only click we forward to the form    /// </summary>    private Boolean myButtonWasClicked;    /// <summary>    ///  The value of the Text property of the child Label control    /// </summary>    public String Label    {        get{ return this.label.Text; }        set{ this.label.Text = value; }    }    /// <summary>    ///  The value of the Text property of the child TextBox control    /// </summary>    public override String Text    {        get{ return this.textBox.Text; }        set{ this.textBox.Text = value; }    }    /// <summary>    ///  The value of the Text property of the child Button control    /// </summary>    public override String ButtonText    {        get{ return this.button.Text; }        set{ this.button.Text = value; }    }    /// <summary>    ///  Control constructor    /// </summary>    public LinkedButtonAndTextBox()    {        // by default, the button has not been clicked        this.myButtonWasClicked = false;        // designer generated method to        //  create and add child controls and to set        //  properties        InitializeComponent();    }    /// <summary>    /// Event handler called when our button is clicked.    ///  This event handler method was registered using the Visual Studio control designer.    /// </summary>    /// <param name="sender">The object on which the click occurred</param>    /// <param name="e">EventArgs describing the event</param>    private void button_Click(Object sender, EventArgs e)    {        // indicate that the click event originated from our button's        //  click handler        this.myButtonWasClicked = true;        // click the control (call LinkedButtonAndTextBox.OnClick)        this.OnClick(e);    }    /// <summary>    /// Raise the click event     /// </summary>    protected override void OnClick(EventArgs e)    {        // did the click originate from our button control?        if(this.myButtonWasClicked)        {            // yes                        // reset the button clicked flag            this.myButtonWasClicked = false;            // call the our base class's OnClick method (UserControl.OnClick)            base.OnClick(e);        }}
How it works
The key to this solution is the use of the myButtonWasClicked member variable.  By setting the value to true in the button_Click method before calling OnClick, the control knows to call any registered click handlers (by calling the base classes implementation of OnClick).  If the value is false, the click event is not forwarded to the registered click handlers.

Applications can register interest in your click event by adding a delegate to the event handler chain:

LinkedButtonAndTextBox btnTextBox1 = new LinkedButtonAndTextBox():btnTextBox1.Click += btnTextBox_Click:
Visual Studio 2005 will add code similar to the above lines when you add the control to a form and double-click on it for the first time.  An event handler method will also be generated on your behalf.

Another solution
Earlier I mentioned that an alternative solution would be to not override the control's OnClick method and, instead, to add a specific click event for the button.  We'll call this new event ActionButtonClick.  Rather than reiterate a good deal of the above example, I am going to show just an implementation for the ActionButtonClick event.

//--------------------------------------------------------------------- //THIS CODE AND INFORMATION ARE PROVIDED AS IS WITHOUT WARRANTY OF ANY //KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE //IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A //PARTICULAR PURPOSE. //---------------------------------------------------------------------using System;using System.ComponentModel;using System.Drawing;using System.Windows.Forms;public partial class LinkedButtonAndTextBox : UserControl{    //*** code omitted for size and clarity ***    /// <summary>    /// The event handler that forms can register to    /// receive notification that our button has been    /// clicked.    /// </summary>    public event EventHandler ActionButtonClick;    /// <summary>    /// Event handler called when our button is clicked    ///  This event handler method was registered using the Visual Studio control designer.    /// </summary>    /// <param name="sender">The object on which the click occurred</param>    /// <param name="e">EventArgs describing the event</param>    protected override void button_Click(Object sender, EventArgs e)    {        // raise the event        this.OnActionButtonClick(e);    }    /// <summary>    /// Raise the ActionButtonClick event    /// </summary>    protected virtual void OnActionButtonClick(EventArgs e)    {        // check to see if an event handler has been registered        if(this.ActionButtonClick != null)        {            // call the registered event handler(s)            this.ActionButtonClick(this, e);                }    }}
The above example allows an application to register to be notified whenever the LinkedButtonAndTextBox's button control has been clicked while still allowing notification of clicks occurring on other areas of the control.  Depending on the control you write, this approach may be the one you are looking for.

It is important to check the event handler's (ActionButtonClick) value against null before making the call to avoid possible NullReferenceExceptions.

Which solution you choose to use will depend on the intent of your control.  For my current application, the first solution made more sense to me.  I would not be surprised if my next application were to use the second approach. :)

Enjoy!
-- DK

[Edit: minor tweak]

Disclaimer(s):
This posting is provided "AS IS" with no warranties, and confers no rights.