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

A custom DecimalBox for accepting only Digits and a Decimal Point

0.00/5 (No votes)
28 Jul 2009 1  
If you need to restrict user input to digits or decimal data, this is one way!

Introduction

In a recent project I was creating for evaluating investment properties I found I was going to need as many as 20 or so text boxes to accept monetary amounts, the purchase price, down payment, taxes, rental income, miscellaneous income, and a number of expenses beyond just the mortgage payment. I initially searched the web for examples of a control that would only accept digits, but was disappointed with what I found; likewise when I considered formatting the user input as currency. The solutions I encountered didn't do anything to ensure that the user couldn't make a mistake that wasn't caught and immediately rejected by the code, and the formatting examples I found were inspirational, but not quite what I wanted to accomplish. So, I decided that I would use pieces of what I'd found to create a custom cotrol that did exactly what I wanted it to do. The criteria were as follows:

  1. Any character other than a digit or a single decimal point was to be simply rejected as user input, and not even displayed in the textbox. In this way, since users couldn't possibly make any input errors, there'd be no need for error handling, and mercifully, no annoying pop-up error messages.
  2. On entering the control, the control's background color should change so there's no question about which field the user is in; when exiting the control, the background color changes back.
  3. Since all user input was to be "money" I wanted to format the user input on leaving the control to a currency format where the appropriate monetary symbol and thousand's separator would be automatically added to the text display, precision was set to two decimal places, and all the figures lined up neatly on the form.

As I found during testing, which as it turns out was amazingly simple once I opened and had running two instances of Visual Studio 2008 - one for developing my "DecimalBox" and one to test the "DecimalBox" - anything that can possibly go wrong, will. You'll find code that on first blush seems to add unnecessary code, or unnecessarily complicate things, but actually precludes the unhandled errors I was able to somewhat accidentally introduce during testing. I'll explain them as we go.

If you've tended to shy away from custom controls in the past thinking them either unnecessary or too time-consuming to develop, I hope the simple code here will allow you to see how, in the long run, they'll actually save time and cut development time next time you need something like a "DecimalBox" on your form. C# encourages reuse, encapsulation and code efficiency, and using custom controls can help you achieve that in your own code. In much the same way that using Microsoft-provided controls saves you development and support time, the one-time creation of a custom control will pay off in not only your current project, but the next time you need to accept, for example, only decimal user input, and the time after that, and the time after that as well. Developing custom control is a great skill to develop. I hope this simple control inspires you to consider developing your own custom controls - and contributing your successes to this forum. Truly, the entire community benefits from every contribution, and we all become better programmers.

Background

One reviewer asked a wonderful question: "Why would I use your control rather than a numeric up-down counter?" which led me to consider the implications of why use a custom control at all. My response follows:

For any control I think there are three interwoven issues, functionality, maintainability, and user perception or experience. Remember, I needed 20 or more controls capable of:

  1. accepting user input in the form of a decimal value, excluding any and all extraneous characters
  2. I wanted to format the user input as currency on leaving the control, adding the appropriate culture-specific monetary symbol and the appropriate thousand's separator
  3. I wanted to change the background color on entering (to focus user attention to the appropriate control) and on leaving.

While requirement a) could be handled by the numeric up-down counter, requirement b) couldn't be accomplished because the control only displays digit or decimal values and not text (it's a 'numeric' up-down counter).

But more importantly - addressing requirement c) - is the question of managing 20 such identical controls. Should they be managed individually, that is, 20 separate "Focus - Leave" event handlers each containing identical code either to format the user's decimal input as currency or calling on a method in a separate class that formats the user input as currency? Or, could I create a custom control where in one place that format-as-currency code could be created and maintained, and the result displayed in one, 20, 100, or 10,000 controls, if so needed? From the perspective of maintainability, and not to forget the development cycle of creating this behavior, a single place, that is a single custom control, made the most logical sense.

Further, in the process of development, my initial effort was to simply add a '$' character (and later, to detect the presence of one to prevent a formatting exception error from occurring), and in testing I found that the simple approach didn't prevent or resolve problems. The point is, that doing development on one (custom) control allowed me to modify its code <i>in one place</i> and have the behavior propagate to the 20-or-so instances of the control in my form. The alternative, had it been necessary, would have been to change the code in either 20 separate locations (very inefficient) or in a single method in a separate class (more efficient, but requiring dedicated code in my project whereas in a custom control that code is in a separate). Additionally, of course, I now have a custom control that I can use in subsequent projects with no further development necessary.

I hope it's clear that focusing the development efforts in one place made both development and subsequent maintenance far simpler. Philosophically this results in code re-usability, one of the supposed hallmarks of the C# language. From another philosophical angle, this isn't much different from using a Microsoft-provided control that already has certain functionality already built in, much like your question of why not use the already-provided numeric up-down control, and the answer should be obvious: though it has its advantages, it does not do exactly what I want the control to do. The solution is a custom control, which as I found out, wasn't as difficult to develop as I initially thought, and allowed me to add a control to my form that does do exactly what I want it to do.

The user experience is not be ignored either. Users see text boxes all the time and are used to entering values in them, whether test or numeric. When they see a numeric up-down they see a control with arrows they have to use to alter the displayed value. Unless they get a visual cue, say a change in background color, they may not be aware that they can change the value directly by typing it in. In my project - real estate evaluation - values could vary from a few hundred dollars in one field to tens of millions of dollars in another. Clearly, users should be encouraged to enter the value they wish rather than use arrow keys to describe or select it.

To take this one step further in encouraging the use of custom controls, I also have dozens of TextBoxes where users can enter text information, names, addresses, expense categories, etc. Suppose I want to have the background color of each of these controls change as they user tabs through my forms? Should I add an event handler for "Focus - Enter" and "Focus - Leave" for each individual instance of these dozens of TextBoxes, changing the background color on entering, changing it back on leaving, or should I create a custom TextBox control with the appropriate background color changes "built-in" so that adding an instance of that control to my form - or any other project for that matter - carries with it the changes of background color that I wish the control to exhibit as the user tabs through the form? I hope the answer is obvious, that is, that the little effort required to create a custom control saves tremendous amounts of development and maintenance time in the longer term even though it cost a bit of time on the front end to develop it.

Using the Code

  1. Open Visual Studio 2008, click the “File” pull-down at the top of the screen, then “New” and “Project…”
  2. Click on “Windows Forms Control Library, then name it “DecimalBox”.
  3. In the “Solution Explorer” on the right of the screen, right-click the entry ending in “.cs” (by default this will be “UserControl1” and delete it.
  4. At the top of Visual Studio, click the “Project” pull-down, then click “Add New Item”.
  5. From the list of items, click on “Custom Control” and name it “DecimalBox”; this will become both the name of the project’s namespace, and the DLL file that will be created.
  6. Click “Add”.
  7. The project screen will come up with dialogue “To add components….” At the end of this sentence, click on the highlighted portion that says “click here to switch to code view.”
  8. On the code view screen you’ll see the namespace line “public partial class DecimalBox : Control. Replace “Control” with “TextBox” which is the control we’ll be inheriting from.
  9. Add the following line as the first line of the class code:
    private string textData = "";

    As we'll see explained later, having a local variable to store the user input prevents some potential errors. Omitting the " = "" " would create the need for later, additional code, as there's a difference between null, and an empty character string.

  10. Switch to the Design screen (which will still be blank except for the dialogue noted is step 8), then click on the lightning bolt on the property window on the right side of the screen to bring up the control’s events window.  
  11. We'll create the four necessary event handlers as follows: double-click on the "Enter" event; switch back to the design page and double-click the “KeyPress” event; switch back to the design page, and double-click the "TextChanged" event, finally, switch back to the design page, and double-click the "Leave" event. We're ready to code the event handlers now.
  12. To change the background color of our custom control, add the following code to the "DecimalBox_Enter" event handler:
    this. BackColor = System. Drawing. SystemColors. GradientInactiveCaption;

    feel free to change this color to whatever you want.

  13. Next, we want to filter the user input to accept only digits or a single decimal point, but we also want them to be able to use the backspace or delete keys to correct their data data, so the following code gets added to the "DecimalBox_KeyPress" event:
            if (this.Text.Contains('.'))
                {
                    if ( !char. IsControl ( e. KeyChar ) &&
                        !char. IsDigit ( e. KeyChar ) )
                        e. Handled = true;
                }
                        //        If we don't already have a decimal point,
                        //        accept control keys, digits, OR a decimal point
                else
                    if ( !char. IsControl ( e. KeyChar ) &&
                        !char. IsDigit ( e. KeyChar ) &&
                        !char. Equals ( e. KeyChar, '.' ) )
                        e. Handled = true;

    The first 'if' statement checks to see if the DecimalBox already contains a decimal point; if it does, the embedded 'if' excludes any characters except digits or the control characters, i.e. backspace or delete keys. If we don't already have a decimal point, the 'else' statement will accept one. Since "KeyPress" event occurs before the textbox control accepts any characters or adds the character to its text property, we can reject any character we won't accept with the "e.Handled = true" statement.

  14. As each character we accept is added to "this.text" we want to ensure it's added to our local variable, so we add the following line of code to the "DecimalBox_TextChanged" event handler:
    textData = this. Text;

    It might appear that adding this code to the "KeyPress" event would accomplish the same thing, but, you'd always be one character shy of the characters the user input since characters aren't added to the control's text property ("this.text") until after the "KeyPress" event has been handled.

  15. Lastly, we format the user's input as currency, and explicitly reset the background color to its original color by adding the following code to the "DecimalBox_Leave" event handler:
                if ( textData != "" )
                {        //        we'll need to flag unexpected characters
                    bool formattedAlready = false;
                    
                        //        Any form using this control might
                        //        programmatically alter the string data by
                        //        formatting it or adding characters that
                        //        would cause an unexpected error to occur
                        //        when the currency format code attempted to
                        //        format it. So, check the string for any
                        //        chars other than digits or a decimal point...
    
                    foreach ( char item in textData )
                    {
                        if ( !char. IsDigit ( item ) &&
                            !char. Equals ( item, '.' ) )
                            formattedAlready = true;
                    }        //    setting the bool var to true if true
    
                            //    if no unexpected characters were found,
                    if ( formattedAlready == false )
                    {        //    format the string as currency...
                        this. Text = ( Convert. ToDecimal ( textData ) ).
                            ToString ( "C" );
                    }        //    otherwise this code is skipped
                }
                this.BackColor = System. Drawing. Color. White;

    Because the variable was defined as "" no checking for null is necessary. As you can see in the comments, an unexpected error would occur if an attempt were made to format the string as currency and the string contained any characters other than digits or decimal points. This could be expected to happen any time you programmatically alter the text of a DecimalBox then format it, and a user subsequently tabbed into then out of that DecimalBox.

    Formatting the text - once - at the level of the custom control simplifies the code of any form using this control, but necessitates parsing the data at the form level to decimal data so that it can be used in computations. Again, a generic routine simplifies the process by not tying it to any specific culture, and hence the DecimalBox control has universal appeal. You first need to add a 'using' statement to access the necessary parsing method as follows:

    using System. Globalization;

    Then, the following parsing method will do the heavy lifting for you, obviating the need to manually parse the string to remove currency symbols and commas or other thousand's separators unique to any currency or culture, purchasePrice being a decimal variable I declared at the class level:

        purchasePrice = decimal. Parse ( purchasePriceDecimalBox. Text,
            NumberStyles. Currency );

    Obviously, the control is named purchasePriceDecimalBox.

using System;
using System. Linq;
using System. Windows. Forms;

namespace DecimalBox
{
public partial class DecimalBox : TextBox
{
    private string textData = "";

    public DecimalBox ( )
    {
        InitializeComponent ( );
    }

    protected override void OnPaint ( PaintEventArgs pe )
    {
        base. OnPaint ( pe );
    }

    //		On getting the focus change the back-color
    private void DecimalBox_Enter ( object sender, EventArgs e )
    {
        this. BackColor = System. Drawing. SystemColors. GradientInactiveCaption;
    }


    //		reject all but digits and the decimal point keys
    private void DecimalBox_KeyPress ( object sender, KeyPressEventArgs e )
    {
        //		If we already have a decimal point in the string,
        //		only accept control keys or digits
        if (this.Text.Contains('.'))
        {
            if ( !char. IsControl ( e. KeyChar ) &&
            !char. IsDigit ( e. KeyChar ) )
                e. Handled = true;
        }
        //		If we don't already have a decimal point,
        //		accept control keys, digits, OR a decimal point
        else
            if ( !char. IsControl ( e. KeyChar ) &&
        !char. IsDigit ( e. KeyChar ) &&
        !char. Equals ( e. KeyChar, '.' ) )
            e. Handled = true;
    }


    //		copy the amount to the local variable
    private void DecimalBox_TextChanged ( object sender, EventArgs e )
    {
        textData = this. Text;
    }


    private void DecimalBox_Leave ( object sender, EventArgs e )
    {
        if ( textData != "" )
        {		//		we'll need to flag unexpected characters
            bool formattedAlready = false;

            //		Any form using this control might
            //		programmatically alter the string data by
            //		formatting it or adding characters that
            //		would cause an unexpected error to occur
            //		when the currency format code attempted to
            //		format it. So, check the string for any
            //		chars other than digits or a decimal point...

            foreach ( char item in textData )
            {
                if ( !char. IsDigit ( item ) &&
                !char. Equals ( item, '.' ) )
                    formattedAlready = true;
            }		//	setting the bool var to true if true

            //	if no unexpected characters were found,
            if ( formattedAlready == false )
            {		//	format the string as currency...
                this. Text = ( Convert. ToDecimal ( textData ) ).
                    ToString ( "C" );
            }		//		otherwise skip this code
        }
        this.BackColor = System. Drawing. Color. White;
    }
}
}

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