Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A Simple User Control

0.00/5 (No votes)
17 Jun 2010 1  
A simple User Control for selecting a shape and entering dimensions.

Introduction

User Controls are objects which wrap a group of standard controls such as Labels, TextBoxes, and RadioButtons which may be reused for several applications. This type of control finds use in programs which manage or print address labels, for instance, or for creating signature blocks in emails. They are also useful when the developer expects the need to use the same set of user data for different purposes. In addition to standard controls, a User Control can also display custom graphics, a feature which is utilized here.

This article demonstrates the basic principles of creating a reusable User Control that includes graphics, data entry and validation, and events. The User Control presented here is part of a larger project that will be submitted later, and may become a central feature of a few future projects. More advanced programmers will undoubtedly find much to add to my knowledge after reviewing the code, and I look forward to the education. In the meantime, though, I hope this will be helpful to those still trying to learn how to make a real, functional User Control.

Background

Though I'm an electrical engineer by training and experience, in my job, I've been asked to wear many hats. Most recently, I've been asked to start designing water and sewer systems, and that's a whole new field of knowledge for me. One of my first challenges in this role was to determine the rate of flow in a gravity line, given the slope of a pipe or channel and the dimensions. Thanks to Google, I found a bunch of information to aid me in my learning, and Amazon led me to an excellent book on the subject, "Water and Wastewater Calculations Manual," by Shun Dar Lin.

It turns out that the fundamental calculation for this stuff is called Manning's Equation, which is based on empirical data from observing open channel flows in streams and rivers. This formula has been generalized to include closed pipes, a simplification with which I disagree but can't yet correct. The math is easy, but tedious and repetitive, so I decided to write a program to make it easier to iterate through the variables. Since I can't predict what channel shapes I might need, I used GIMP to create several diagram images to choose from while guiding the user through selecting various dimensions in the data entry part of my program. I've never had much luck drawing in the Windows environment, so I took the low road, but the result looked awful. That led me to the unpleasant admission that it is time I learned to draw in Windows, and the ideal container for my efforts seemed to be a User Control that I can include in future projects which require the same sort of data entry.

What follows is my first attempt at creating a User Control for a Windows Form. It allows a user to select from three shapes of channels - circular, rectangular, and trapezoidal - then allows the user to enter the dimensions required for the equation. The selection is made using radio buttons in a group box; this causes the control to redraw itself to display the selected shape. The user is then given a set of textboxes to complete, corresponding to the displayed dimensions. Every time a selection or value changes, an event is generated to allow the hosting form to respond to the change. Within the control, there is a dummy handler that does nothing, just in case the host form doesn't provide a handler. This generates a warning, but it still compiles and runs nicely. Perhaps, someday I'll find a more elegant solution...

Explaining the code

The control is designed to be used in a Windows Form, and exposes a number of values:

  • Shape is an enum type, with values Circ, Rect, and Trap; Circ being the default.
  • Depth is a double, and represents the depth of the fluid flow in the pipe or channel.
  • Diameter is a double, and is used only for circular pipes.
  • RectWidth is a double, and is used only for a rectangular channel.
  • BotWidth is a double, used for the bottom width of a trapezoidal channel.
  • TopWidth is a double, used for the width of a trapezoidal channel at the level of flow.

It also raises an Event, ValueChanged, which can be handled by the hosting form or ignored. Internally, it checks to see that the values entered for dimensions are numeric, and blocks any ValueChanged event if a value entered does not conform.

Since this is meant to be a Beginner article, let's look at the code in detail. I've been struggling to learn this stuff for a long time, and have been frustrated by the lack of detailed explanation in most articles, so I'll probably bore most readers to death. Hopefully, though, I'll help someone else who, just like me, has been craving a clear explanation of just what the code is doing. I must warn you in advance, though, that there are a few parts that I can't explain - I just tried things until the IDE stopped whining about errors, and called it GoodTM when the code compiled and worked as expected.

To start, let's look at the constructor for the control, and the code that it executes upon loading. This section also covers the variables used and the properties exposed.

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

namespace MyPanel
{
    public partial class FlowPanel : UserControl
    {

        //Member variables and types
        public enum myShape { Circ, Rect, Trap }; //Channel shape enumeration
        private myShape shape = new myShape();
        private double dim1;  //User data - depth of flow
        private double dim2;  //User data - diameter or width value
        private double dim3;  //User data - width value
        private double prev;  //Temporary data to suppress excess events
        private Font TextFont = new Font("Ariel", 9);
        public MyEventArgs MyArgs = new MyEventArgs();

        //Properties
        public myShape Shape        //Used by all
        {
            get { return shape; }
            set { shape = value; }
        }
        public double Depth     //Used by all
        {
            get { return dim1; }
            set { dim1 = value; }
        }
        public double Diameter  //Used for circular shape only
        {
            get { return dim2; }
            set { dim2 = value; }
        }
        public double RectWidth //Used for rectangular shape only
        {
            get { return dim2; }
            set { dim2 = value; }
        }
        public double BotWidth  //Used for trapezoidal shape only
        {
            get { return dim2; }
            set { dim2 = value; }
        }
        public double TopWidth  //Used for trapezoidal shape only
        {
            get { return dim3; }
            set { dim3 = value; }
        }

        //Constructor
        public FlowPanel()
        {
            InitializeComponent();

            this.Shape = myShape.Circ;
            //Initialize default shape as Circular
        }

        //Panel Events
        private void FlowPanel_Load(object sender, EventArgs e)
        {
            if (this.FindForm() != null)
            {
                Form MyForm = this.FindForm();
                this.BackColor = MyForm.BackColor;
            }

        }

The first section is created by the Visual Studio User Control template, and contains several entries that aren't required; I didn't bother to remove them. The class FlowPanel is derived from UserControl, and contains three radio buttons within a groupbox, along with three textboxes. These can be found in the file FlowPanel.Designer.cs. The member variables are used to hold the user selections and data entered, and can be accessed by the parent form using the properties exposed for each of them. Two other variables are declared here, as well - TextFont and MyArgs. These are used for drawing text in the graphical portion of the control, and for carrying custom information in the Event Handler for the FlowPanel. I'll cover these later.

The Properties section uses the get and set operators to provide access to the private global variables used within the FlowPanel class. These are necessary to allow communication between the control and the host form. As a rule, it is consistent with the OOP philosophy to make all member variables private to enhance encapsulation and information hiding. The theory is that the end user has no need to know how something is done internally, and a chunk of code is more reusable if using it doesn't require knowledge of the implementation. This allows the original code to be completely rewritten without breaking other code that depends on it, so long as the publicly accessible variables aren't changed. In a more complex piece of work, one that performs many computations perhaps, this would make more sense. In this case, it doesn't, since nearly every member variable needs to be exposed to the outside world, but I think it's good practice to be consistent, if only to help me form good habits.

Because C# is case sensitive, it's convenient to use the same name for a public property as is used for the private member variable, differing only in the case of the first letter. It isn't necessary to include both the get and set operations - leaving out the set part will make the property read-only - but I may want to be able to change the values from the host form later, so I left them in the code. An interesting thing to note here (at least I find it interesting) is that the private variable dim2 has three public properties. I use the same textbox on the FlowPanel to hold three different values. For a circular shape, it's the diameter, but for the other shapes, it holds a width value. In the host form, if I'm working with a circle, I want to be able to access the diameter directly, and if I'm working on a rectangular channel, I want to fetch the width (RectWidth). By using more than one getter, I can make my code for the form more readable by using these different names. The control doesn't care that both are actually accessing the same textbox, and it will save me from having to remember later that diameter really means width when the selected shape is a rectangle.

Next, we come to the initialization code for the FlowPanel, consisting of a constructor, and an event that fires when the control is loaded into the parent form. The constructor first calls InitializeComponent(), which instantiates the various labels, textboxes, radiobuttons and such that are contained in the FlowPanel.Designer.cs file. This is automatically created when using the Wizard to create the project, and doesn't need to be modified. Since the default color of a UserControl is an extraordinarily ugly gray (presumably because no one at Microsoft could find a more hideous shade), I thought it would be more pleasing to have the control blend in by inheriting the parent form's background color. The function FlowPanel_Load() takes care of that. The if() statement tests to ensure that the control is in fact contained in a form, then sets the control's background color to match that of the host. The keyword this references the current class, and FindForm() returns a reference to the hosting container. A new Form instance, MyForm, is used to get the host form background color, and to set the current object's color to match.

Before continuing with the code, let's look at the actual product:

Circle

Trapezoid

Note that when the selected shape is Trapezoidal, a new textbox appears. This is done by using the Visible property of a TextBox, which is set by testing the Shape value. Note also that the labels have changed. These effects are all accomplished using Events generated by the individual controls on the FlowPanel, and are handled locally without the host form having to do anything. This seems a good time to introduce those handlers, so:

//Control Event Handlers
private void rbCirc_CheckedChanged(object sender, EventArgs e)
{
    if (rbCirc.Checked == true)
    {
        Shape = myShape.Circ;
        MyArgs.MyControl = "rbCirc";
        RaiseEvent(sender, MyArgs);
        lblDim1.Text = "Depth, d";
        lblDim2.Text = "Diameter, D";
        lblDim3.Visible = false;
        txtDim3.Visible = false;
        Invalidate();
    }
}

private void rbRect_CheckedChanged(object sender, EventArgs e)
{
    if (rbRect.Checked == true)
    {
        Shape = myShape.Rect;
        MyArgs.MyControl = "rbRect";
        RaiseEvent(sender, MyArgs);
        lblDim1.Text = "Depth, d";
        lblDim2.Text = "Width, W";
        lblDim3.Visible = false;
        txtDim3.Visible = false;
        Invalidate();
    }
}

private void rbTrap_CheckedChanged(object sender, EventArgs e)
{
    if (rbTrap.Checked == true)
    {
        Shape = myShape.Trap;
        MyArgs.MyControl = "rbTrap";
        RaiseEvent(rbTrap, MyArgs);
        lblDim1.Text = "Depth, d";
        lblDim2.Text = "Bottom Width, W1";
        lblDim3.Text = "Top Width, W2";
        lblDim3.Visible = true;
        txtDim3.Visible = true;
        Invalidate();
    }
}

These three event handlers all respond to a change in the Checked property of their respective radio buttons. Each sets the value of Shape to the newly selected shape value, then they generate an event for the parent form to handle, which simply passes to the form the name of the newly selected shape. The RaiseEvent() function and MyArgs will be discussed later. After this, they each change the label text and determine which labels and textboxes will now be visible to the user. Finally, they each call Invalidate(), which forces Windows to redraw the control by invoking the OnPaint() method. This is probably a good time to introduce the OnPaint() method, which I found to be the most tedious part of the whole exercise. In the interest of brevity, I'm only going to show the part that draws the circular shape, but the principles remain the same for all shapes.

//Graphics
protected override void OnPaint(PaintEventArgs e)
{
    using (Pen blackPen = new Pen(Color.Black, 1))
    {
        e.Graphics.DrawRectangle(blackPen, 200, 18, 195, 195);
        switch (Shape)
        {
            case myShape.Circ: //Circular pipe
                {
                    //Draw the pipe
                    e.Graphics.DrawEllipse(blackPen, 265, 50, 120, 120);
                    //Add the dimension Diameter, D
                    e.Graphics.DrawLine(blackPen, 220, 50, 300, 50);
                    e.Graphics.DrawLine(blackPen, 220, 170, 300, 170);
                    e.Graphics.DrawLine(blackPen, 220, 60, 230, 50);
                    e.Graphics.DrawLine(blackPen, 240, 60, 230, 50);
                    e.Graphics.DrawLine(blackPen, 220, 160, 230, 170);
                    e.Graphics.DrawLine(blackPen, 240, 160, 230, 170);
                    e.Graphics.DrawLine(blackPen, 230, 100, 230, 50);
                    e.Graphics.DrawLine(blackPen, 230, 120, 230, 170);
                    using (SolidBrush blackBrush = new SolidBrush(Color.Black))
                    {
                        e.Graphics.DrawString("D", TextFont, blackBrush, 225, 105);
                    }
                    using (Pen bluePen = new Pen(Color.Blue, 1))
                    {
                        //Draw the water level
                        e.Graphics.DrawLine(bluePen, 265, 110, 385, 110);
                        //Add the dimension Depth, d
                        e.Graphics.DrawLine(bluePen, 325, 160, 325, 110);
                        e.Graphics.DrawLine(bluePen, 315, 120, 325, 110);
                        e.Graphics.DrawLine(bluePen, 335, 120, 325, 110);
                        using (SolidBrush blueBrush = new SolidBrush(Color.Blue))
                        {
                        e.Graphics.DrawString("d",TextFont,blueBrush, 335,120);
                        }
                    }
                    break;
                }

As you can see, I've broken the OnPaint() implementation into switch blocks, which test the current value of Shape to determine which shape to draw. Upon entering the method, a using block is initiated to save having to specify which Pen to use for drawing. This has the additional benefit of ensuring that the pen resource will be destroyed when the method exits. The Pen object requires two parameters - a color, and a width in pixels. Much to my surprise, circles are drawn by Windows as ellipses. That shouldn't be a surprise, really, as a circle is really a special case of an ellipse, with both foci located at the same point. The following lines draw the dimension lines and arrows. Every line segment has to be individually drawn, which is why I have tried hard to avoid learning to do this for so many years.

I should mention here that, though all of the books I've read have told me to create a device context, then draw into it, the PaintEventArgs object, e, takes care of that for me, and provides easy access to all these neat functions. I still don't have a strong grasp on the concepts, but that will come in time. For now, I'm passing on to you the shortcuts I've learned from so many helpful CodeProject members.

Drawing the dimension lines isn't terribly difficult, using the Pen object plus a start and end point - that's what the parameters specify in the calls to e.Graphics.DrawLine() provide. The first argument is the selected Pen, the next two are the starting point, and the last two the end point, in X,Y format. These values are in pixel units, with the origin at the top left corner of the control. The X values are measured from left to right, while the Y values increase from top to bottom.

Drawing simple text requires a new object, a Brush. Although I might have created the Brush object along with the Pen, I didn't for some reason, and I don't feel like changing it now. Another using block is employed to draw the text to identify each of the dimension lines with a one or two character label. As an afterthought, it occurred to me that it might be cool to show water in the pipe in a different color, and to display the depth of the water in the same color. That led to the last bit, enclosed in another using block much like the previous one, but using a blue Pen and Brush. The code for the rectangular and trapezoidal cases is much the same, but with different parameters, so there's no reason to include it here. Note that the e.Graphics.DrawString() method requires a font selection, TextFont. This was defined in the first section, though in retrospect, it probably would be better to define it within the OnPaint() method. This would free up the resource whenever the method completes.

Next, I'd like to discuss the textbox behaviors. The application I have in mind for this control expects the values entered by a user to be doubles, and it shouldn't have to worry about accidental errors in user input. The control must implement some kind of validation to ensure that only real numbers are entered as dimensions for the pipes and channels under consideration. My first thought was to use the TextChanged event for the textboxes to raise an event for the host form to handle, and to trigger validation. That had some unexpected side effects. For one, it caused an event to fire that told the host form that a value had changed, even if the value was invalid. That was unacceptable. The solution I found was as follows:

private double ValidateEntry(TextBox MyTextBox)
{
    try
    {
        return Double.Parse(MyTextBox.Text);
    }
    catch (FormatException ex)
    {
        MessageBox.Show("Enter a valid numeric value\n" + ex.Message);
        MyTextBox.Focus();
        return 0.0;
    }
}

//txtDim1
private void txtDim1_Enter(object sender, EventArgs e)
{
    txtDim1.SelectAll();
}

private void txtDim1_Leave(object sender, EventArgs e)
{
    prev = dim1;                    //Save the current value
    dim1 = ValidateEntry(txtDim1);  //Get the new value
    if (dim1 != 0.0 & prev != dim1)
    //Raise an event if Validation passed AND value changed
    {
        MyArgs.MyControl = "txtDim1";
        RaiseEvent(txtDim1, MyArgs);
    }
}

private void txtDim1_KeyPress(object sender, KeyPressEventArgs e)
{
    if (e.KeyChar == 13)
    {
        txtDim2.Focus();
    }
}

//txtDim2
private void txtDim2_Enter(object sender, EventArgs e)
{
    txtDim2.SelectAll();
}

private void txtDim2_Leave(object sender, EventArgs e)
{
    prev = dim2;
    dim2 = ValidateEntry(txtDim2);
    if (dim2 != 0.0 & prev != dim2)
    {
        MyArgs.MyControl = "txtDim2";
        RaiseEvent(txtDim2, MyArgs);
    }
}

private void txtDim2_KeyPress(object sender, KeyPressEventArgs e)
{
    if (e.KeyChar == 13)
    {
        if (txtDim3.Visible == true)
        {
            txtDim3.Focus();
        }
        else
        {
            txtDim1.Focus();
        }
    }
}

//txtDim3
private void txtDim3_Enter(object sender, EventArgs e)
{
    txtDim3.SelectAll();
}

private void txtDim3_Leave(object sender, EventArgs e)
{
    prev = dim3;
    dim3 = ValidateEntry(txtDim3);
    if (dim3 != 0.0 & prev != dim3)
    {
        MyArgs.MyControl = "txtDim3";
        RaiseEvent(txtDim3, MyArgs);
    }
}

private void txtDim3_KeyPress(object sender, KeyPressEventArgs e)
{
    if (e.KeyChar == 13)
    {
        txtDim1.Focus();
    }
}

There's a lot going on here, so pay attention. First off, the method ValidateEntry() is called every time a user attempts to leave a textbox and move on to the next textbox. The method Double.Parse() attempts to convert the text passed to it into a double type; if that works, it returns the double value, but if it doesn't, it raises a FormatException. In the try/catch block, this exception is captured, and generates a MessageBox to inform the user that a bad entry has been detected. A value of 0.0 is returned to the calling method to suppress raising an event to the parent form. Since, in this context, an input value of 0.0 is fairly meaningless, I used it to detect a bad entry.

Since I meant this to be an interactive control eventually, behaving like a spreadsheet component for designing sewer pipes, it was important to be able to change or accept previously entered data. I originally used the Enter event for each textbox to clear the box for new input, but I later found that the SelectAll() method is a better choice, as it gives the user a choice to keep the entry, or to easily delete it and start over. It also doesn't immediately raise a TextChanged event, which I was originally using to catch user input. The current solution works much better.

A quirk I did not expect was that, when a user presses Enter after entering a value in a textbox, the cursor doesn't automatically move to the next textbox. Every program I've used does this, so I expected it to be the default behavior. Bad assumption! Thanks to my fellow CPians, I learned that I have to implement a KeyPress handler for each textbox and test for the Enter key (e.KeyChar == 13) in order to change the cursor position to a new textbox. This is accomplished using the Focus() method of the target textbox. That was, in turn, complicated by the fact that not all textboxes are Visible, depending on the current setting of Shape. You can see this in the KeyPress() handler for txtDim2; it tests whether the txtDim3 textbox is currently visible; if so, it moves the focus to txtDim3, and if not, it moves it to the first textbox on the control.

I've put off until last the part that I understand the least, though I'm trying hard to understand it - Events and Delegates. Many thanks to DaveyM69, Luc Pattyn, Henry Minute, and several others for helping me to add to my understanding on this topic, but the job of educating me is still not done. This control implements an Event to notify the parent form when a value changes; whether the form does anything with that information is irrelevant. The standard EventArgs passes to the elements which subscribe to it only the fact that an event occurred in the FlowPanel object, so I had to create a new MyArgs structure with one member - MyControl - which contains the name of the control whose value changed. The code which creates and handles the event is:

//Events
public event EventHandler ValueChanged;
public class MyEventArgs : EventArgs
{
    private string myControl;
    public string MyControl
    {
        get { return myControl; }
        set { myControl = value; }
    }
}

protected virtual void OnValueChanged(object sender, MyEventArgs e)
{
    EventHandler eh = ValueChanged;
    if (eh != null)
        eh(this, e);
}

public void RaiseEvent(object sender, MyEventArgs e)
{
    OnValueChanged(sender, e);
}

This bit creates an event called ValueChanged, defines a new class of EventArgs called MyEventArgs, and declares a function to handle the event locally, called OnValueChanged. It also defines a public method, RaiseEvent(), which invokes the protected member, OnValueChanged(). MyEventArgs became necessary because the default EventArgs returns to the parent form only the name of the FlowPanel control, not the name of the control within the FlowPanel. While this would work, it would require the form to fetch the current value of each control, then test to determine which had changed, then respond appropriately; too much work! Instead, I implemented a custom EventArgs class which contains a single text value, MyControl. In each control handler that calls RaiseEvent(), the name of the calling textbox or radio button is passed to MyControl, and correctly returned to the parent form. At this point, my understanding fails me. I have only a vague idea why or how this works, but it does. The test form that I'll show next successfully displays the FlowPanel described here, and correctly responds to the events generated.

Using the code

namespace FlowTestForm
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            flowPanel1.ValueChanged += new EventHandler(flowPanel1_ValueChanged);
        }

        private void flowPanel1_ValueChanged(object sender, EventArgs e)
        {
            MessageBox.Show(flowPanel1.MyArgs.MyControl + "changed");
        }
    }
}

Creating a Windows Forms application using the wizard generated the usual code, and the boilerplate parts aren't shown. Using the Tools menu to add my FlowPanel class to the Toolbox, I located it in the list and dragged a copy to the form. Clicking on the Events button, I located the event, ValueChanged, and double clicked it. That automatically created a handler for the event, flowPanel1_ValueChanged(), to which I simply added a MessageBox to display the name of the control that changed. In order to make the form respond to the event, it was necessary to add the line following InitializeComponent() to wire things together. This line effectively causes the form to subscribe to the event, allowing it to receive notification that the event was raised.

Form Example

For now, all this form does is show a message, but it gives me a handle to grab for expanding the functionality. My first use of this User Control will be to completely revamp my ugly stepchild to build a nicer version of the simple flow calculator. After that, who knows?

Points of interest

I found it interesting and useful that a single member variable can have multiple properties with different names, which will make using this control much easier. It was also fun to discover how to make the control adopt a property of the parent form, and I suspect that one day I'll want to revisit it to make it resizable, depending on the font size of the parent. That should make for a more consistent appearance. I was surprised to see that the control, once embedded in a form, exposed the Event, ValueChanged, rather than the public RaiseEvent() function, but I suppose my understanding of events will improve over time.

Comments and suggestions are welcome - I've learned a bunch doing this project, and I have many CodeProject members to thank for that. I know that this is trivial for most of the denizens of our little community, but I hope it proves instructive to others who, like me, are struggling to understand the basics still.

History

  • Version 1.1.0.0 - 15 June 2010.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here