MaskedTextBox and Custom Types

You always pass failure on the way to success. — Mickey Rooney



While implementing the MaskedTextBox class is pretty straight forward when working with existing types, such as DateTime, and its use is very well documented, I found it slightly more difficult to implement it using a custom type. Since most of the information I was able to find on the web was either discussing the former or problems implementing the latter, I decided to give it a go and share my findings.

In a private project of mine I needed to provide some means of input for a latitude/longitude object. Instead of using three different textboxes to hold the three values those are composed off (degrees, minutes and seconds), I thought it would be neat to have one single textbox, nicely formatted, to hold the input. That is where the MaskedTextBox came in. Therefore I will, in this tutorial, create a MaskedTextBox which lets the user enter 3 formatted integers – the degree, the minute and the second of a longitude. The input will get checked against a custom type of “Longitude”.

We start by creating a new MS Visual C# 2008 WindowsFormsApplication called “LongitudeMTB”. Before we do anything with the user interface, the form that is, we need to implement our custom type. Add a new class to the project (right click your project, choose “add new …”, select “Class” and call it “Longitude.cs”.  After a few minor customizations to the autogenerated code of “Longitude.cs” it should look like this:

using System;

namespace LongitudeMTB
{
    class Longitude
    {
    }
}

Now we implement the fields and properties. As this struct represents a latitude, you need three fields (degrees (°), minutes (‘) and seconds (“)). Also we need a constructor that passes those three values.

using System;

namespace LongitudeMTB
{
    class Longitude
    {
        // FIELDS
        private int _degrees;
        private int _minutes;
        private int _seconds;

        // PROPERTIES
        public int Degrees
        {
            get { return this._degrees; }
            set { this._degrees = value; }
        }
        public int Minutes
        {
            get { return this._minutes; }
            set { this._minutes = value; }
        }
        public int Seconds
        {
            get { return this._seconds; }
            set { this._seconds = value; }
        }

        // CONSTRUCTOR(s)
        public Longitude()
        {
            this._degrees = 0;
            this._minutes = 0;
            this._seconds = 0;
        }

        public Longitude(int degrees, int minutes, int seconds)
        {
            this._degrees = degrees;
            this._minutes = minutes;
            this._seconds = seconds;
        }
    }
}

That is our custom type struct for now. Our MaskedTextBox will need a MaskDescriptor. Add a new class to your project and call it “LongitudeMaskDescriptor.cs”. The MaskDescriptor defines some read only properties for our MaskedTextBox, namely the standard mask to use, its name, a sample string, the ValidatingType and its Culture. It is primarily used by the Form Design Manager to autogenerate code, which is not quite that useful to us as we wont be using this feature as we are creating our own, inherited class, but it can’t hurt to provide the mentioned functionality either. Also I will be using it (the MaskDescriptor) to fill in the standard values for our derived MaskedTextBox class later on. Note that especially the Culture property is vital. Using a Culture which differs from your projects Culture will prevent the MaskedTextBox from working as intended as it will have trouble formatting the input string properly. Also it will not appear in the MaskedTextBox Input Mask list (i.e. the list that appears when you click on the little arrow in the upper right of the MaskedTextBox in the Form Design Manager and choose “Set Mask …”).

It might be worth mentioning that you will need to add a reference to System.Design via rightclicking on “References” and choosing “Add Reference …” in the Solution Explorer. The namespaces to be added are System.Globalization (for the CultureInfo) and System.Windows.Forms.Design (for the MaskDescriptor).

Also have a look at the string Mask. We are trying to represent a longitude which consists of a degree value between -180° and 180°, a minutes value between 0′ and 60′ and a seconds value between 0″ and 60″.

A “#” denotes an optional digit or space or a plus and minus sign. As a longitudes degree can either be positive or negative this place should always only hold the signature (+,-) of the degree. We can not provide errorchecking in the mask and there is no explicit mask symbol only denoting plus and minus. Nevertheless this can be done in other parts in the code, namely the Parse function which I will cover later in this tutorial.

It is followed by “000″. Here it becomes apparent, that MaskedTextBoxes are primarily made for strings of a fixed length. The mask symbol “0″ denotes a required digit. Though it is possible to use the mask symbol “9″ to denote an optional digit we will run into troubles when formatting a string with a minus sign at the first location (where we used the mask symbol “#”). Therefore I decided to force the user to fill in the whole string using leading zeroes if applicable. Those three ’0′s tell the mask to accept three (required) digital numbers.

The next mask symbol is a “°” which does not denote anything and therefore is used as literal, that is it appears as itself. It occupies a static position in the mask and cannot be moved or overwritten by the user. The rest of the mask string should be selfexplanatory if you followed the explanation so far. Some people might have noted, that there is no explicit error checking done, i.e. the minutes and seconds values are not supposed to be greater than 60. This is done in some other part of the code, namely the Parse method of the Longitude class, and covered later in this tutorial.

For a more in depth look at all those masking elements have a look at MSDNs MaskedTextBox.Mask properties.

using System;
using System.Globalization;
using System.Windows.Forms.Design;

namespace LongitudeMTB
{
    public class LongitudeMaskDescriptor : MaskDescriptor // add reference to System.Design ->
    {
        public override string Mask
        {
            get { return "#000°00'00\""; }
        }

        public override string Name
        {
            get { return "Longitude"; }
        }

        public override string Sample
        {
            get { return "+012°34'56\""; }
        }

        public override Type ValidatingType
        {
            get { return typeof(Longitude); }
        }

        public override CultureInfo Culture
        {
            get { return CultureInfo.CurrentCulture; }
        }
    }
}

Lets turn our attention to the MaskedTextBox. Instead of using the existing component (i.e. via drag and drop in the Form Design Manager) we create our own, personal MaskedTextBox which inherits MaskedTextBox. Of course we then can drag and drop that one too. The groovie thing about doing it this way is, that all our error checking and initialization, etc. is done in the derived class. This is easier to debug and a lot cleaner, especially if this is to be part of a bigger project. To do so, again, add a new class to your project and call it “LongitudeMaskedTextBox.cs”. Delete the references you wont need and inherit MaskedTextBox. MaskedTextBox itself is member of System.Windows.Forms, so you might want to tell C# that you are using that namespace.

using System;
using System.Windows.Forms;

namespace LongitudeMTB
{
    class LongitudeMaskedTextBox : MaskedTextBox
    {
    }
}

In the constructor we put all the code that should get performed when we are first initializing this component (as usual). First we instanciate the LongitudeMaskDescriptor class and apply its values to the respective values of the LongitudeMaskedTextBox class. The LongitudeMaskDescriptor is initialized outside of the constructor so that other parts of the class will have access to it as well – which is needed later on. After we set those properties we might need to set some other, more general properties of the MaskedTextBox, such as what character serves as PromptChar (the character that represents absense of user input) and the behaviour of the text insertion mode (i.e. overwrite the characters or not) and others. I am not going into detail, feel free to have a look at Microsofts documentation and/or play around with those properties to get a feeling for what they do. I will only describe those that I found important for this particular case.

The property MaskedTextBox.TextMaskFormat for example deserves an explanation as its value will determine how we have to implement other parts of the code. It determines weather the prompt character and/or literals are included in the formatted string. I chose to set it to MaskFormat.IncludePromptAndLiterals and here is why. The prompt character (MaskedTextBox.PromptChar) is the character which represents absence of user input in the string. If it is included it will appear as part of the MaskedTextBox.Text string, if it is not included it will get converted into spaces. I chose to include the Prompt character in the string as when we need to determine weather the entered longitude is valid we do have to check weather all numbers have been filled in. The validate method does this internally, but if we happen to press a button while the MaskedTextBox still has focus not checking for Prompt characters in the formatted string can lead to an exception. Obviously its a matter of choice weather to check for spaces in the string or for “_”s, imho it does make more sense to use the actual Prompt character so the code is more readable.

The choice to include literals is a purely cosmetic one, cosmetic as in “it will still influence how to implement our Parse function, but that doesnt matter much”. It pretty much only determines what means we have to use to manipulate the formatted string to get us our values. I find that the literals act as nice delimiters which can easily be used to Split() the formatted string as you will see in the Parse function later in this tutorial. Also there is no need to manipulate the string if you decide that you need to display it in a label or somewhere else just like it is. If you ever need another representation of the string you can always provide a function which does just that.

MaskedTextBox.ResetOnPrompt and MaskedTextBox.ResetOnSpace must both be set to true (which is what they default to anyway) if using a prompt character that also is a possible input character (such as the ’0′ would be in our example) and the MaskFormat property is set to exclude Prompt characters. Else the MaskedTextBox will internally “reset” the symbol at the specified location (i.e. when pressing space or ’0′) and externally it will display the prompt character (’0′). This means that even though we would effectively see a ’0′ in our string, the formatted string would hold a space which can lead to a System.ArgumentException. In our case, as the prompt character is a ‘_’ and therefore differs from any allowed input, it doesnt really matter. If ResetOnPrompt is set to true it will allow the user to input ‘_’s, else it will just tell the user that he is inputting invalid stuff. The same is true for ResetOnSpace. If we would be using the zero as Prompt character it would not matter either in this example as the MaskFormat property is set to include Prompt characters in the formatted string. This, of course, also means that it would be impossible for the user to submit an falsely formatted string of numbers: the validate method checks weather user input is a number or prompt character and the prompt character is a number as well. So this approach can often be quite useful. Except of course if you want to distinguish between the Prompt character and the actual input.

Last but not least I like to set MaskedTextBox.InsertKeyMode to InsertKeyMode.Overwrite. When the user enters something into the MaskedTextBox he will overwrite existing characters instead of shifting them to the right (which causes MaskInputRejected Exceptions).

using System;
using System.Windows.Forms;

namespace LongitudeMTB
{
    class LongitudeMaskedTextBox : MaskedTextBox
    {
        LongitudeMaskDescriptor LongMD = new LongitudeMaskDescriptor();

        public LongitudeMaskedTextBox()
        {
            this.Mask = LongMD.Mask;                        // set the mask
            this.Text = LongMD.Sample;                      // give an example
            this.Culture = LongMD.Culture;                  // set the culture
            this.ValidatingType = LongMD.ValidatingType;    // validating type = object Longitude

            this.PromptChar = '_';
            this.ResetOnSpace = true;
            this.ResetOnPrompt = true;
            this.RejectInputOnFirstFailure = true;
            this.TextMaskFormat = MaskFormat.IncludePromptAndLiterals;
            this.InsertKeyMode = InsertKeyMode.Overwrite;
        }
    }
}

To actually validate the data entered in the MaskedTextBox we need to set two event handlers. MaskedTextBox.MaskInputRejected gets fired when the users input does not match the correspondending format element of the input box, for example if the user presses a letter instead of a number. MaskedTextBox.TypeValidationCompleted occurs when the MaskedTextBox has finished parsing the entered value using the MaskedTextBox.ValidatingType property. To display any possible error message I am gonna use tooltips, as done in the MSDN example.

using System;
using System.Windows.Forms;

namespace LongitudeMTB
{
    class LongitudeMaskedTextBox : MaskedTextBox
    {
        ToolTip toolTip = new ToolTip();
        LongitudeMaskDescriptor LongMD = new LongitudeMaskDescriptor();

        public LongitudeMaskedTextBox()
        {
            // Set up the delays for the ToolTip.
            toolTip.AutoPopDelay = 15000;
            toolTip.InitialDelay = 1000;
            toolTip.ReshowDelay = 500;
            // Force the ToolTip text to be displayed whether or not the form is active.
            toolTip.ShowAlways = true;
            toolTip.ToolTipIcon = ToolTipIcon.Warning;

            this.Mask = LongMD.Mask;                        // set the mask
            this.Text = LongMD.Sample;                      // give an example
            this.Culture = LongMD.Culture;                  // set the culture
            this.ValidatingType = LongMD.ValidatingType;    // validating type = object Longitude

            this.PromptChar = '_';
            this.ResetOnSpace = true;
            this.ResetOnPrompt = true;
            this.RejectInputOnFirstFailure = true;
            this.TextMaskFormat = MaskFormat.IncludePromptAndLiterals;
            this.InsertKeyMode = InsertKeyMode.Overwrite;

            this.MaskInputRejected +=
                new MaskInputRejectedEventHandler(LongitudeMaskedTextBox_MaskInputRejected);
            this.TypeValidationCompleted +=
                new TypeValidationEventHandler(LongitudeMaskedTextBox_TypeValidationCompleted);
        }

        void LongitudeMaskedTextBox_MaskInputRejected(object sender, MaskInputRejectedEventArgs e)
        {
            this.toolTip.ToolTipTitle = "Invalid Input";
            this.toolTip.Show("We're sorry, but only digits (0-9)\n" +
                              "are allowed in this editbox.",
                              this,
                              new System.Drawing.Point(0, this.Height), 5000);
        }

        void LongitudeMaskedTextBox_TypeValidationCompleted(object sender, TypeValidationEventArgs e)
        {
            if (!e.IsValidInput)
            {
                this.toolTip.ToolTipTitle = "Invalid Longitude";
                // the error message to be given out is created in the Parse()
                // method of the Latitude class (and shown here using e.Message)
                this.toolTip.Show(e.Message, this,
                    new System.Drawing.Point(0, this.Height), 5000);
            }
        }
    }
}

We are still not done yet! To validate the input of the MaskedTextBox when using custom data types, one needs to implement a static Parse method (into the type that is to be parsed) that takes a string as parameter (as explained in the MSDN article for the MaskedTextBox.ValidatingType property). The Parse method basically splits the string into its components (in our case into the degrees, minutes and seconds) and then performs some bounds checking. If all went right it returns the object, else it throws an exception. Note that if you are working with several delimiters in your MaskedTextBox (as we are (°,’ and “)) you should simply replace them all with a single delimiter at the start of the parse function (i.e. “.”) and then start splitting the string into its components. Add the following method Parse() to the Longitude class (and the overridden ToString() one as well …):

        public static Longitude Parse(string s)
        {
            // replace prompt char occurences with "0"
            string ts = s.Replace("_", "0");
            // if both strings differ there must be prompt chars left -> no valid longitude
            if (String.Compare(ts, s) != 0)
                return new Longitude(0,0,0); // make sure to return a valid longitude

            // check weather the first char is neither "+" "-" nor " "
            if ((s.Substring(0, 1) != "+") &&
                (s.Substring(0, 1) != "-") &&
                (s.Substring(0, 1) != " "))
            { // its neither, so it must be a digit - throw exception
                throw new ArgumentException(String.Format(CultureInfo.CurrentCulture,
                    "\nThe provided string {0} is not a valid longitude.\n" +
                    "The first character cannot be a number (it is \"{1}\")\n" +
                    "It must be either a +, - or empty (space).",
                    s,
                    s.Substring(0,1)));
            }

            // an array for holding the coordinates
            int[] values = new int[3];

            ts = s;

            // for several different delimiters simply replace
            // them all with the same delimiter (i.e. space or ".")
            ts = s.Replace("°", "."); // convert ° delimiter to . delimiter
            ts = ts.Replace("'", "."); // convert ' delimiter to . delimiter
            ts = ts.Replace("\"", ""); // remove " - its not delimiting

            // put all different values (3), delimited by '.' into a new array of strings
            string[] strValues = ts.Split(new char[] { '.' });

            int valIndex = 0; // counter
            foreach (string strValue in strValues)
            {
                // try to parse the current value to an integer
                values[valIndex] = int.Parse(strValue);
                // check bounds -180 - 180 for degrees and 0 - 60 for minutes and seconds
                if (
                    ((valIndex == 0) && ((values[valIndex] > 180) || (values[valIndex] < -180))) ||
                    (((valIndex == 1) || (valIndex == 2)) && ((values[valIndex] > 60) || (values[valIndex] < 0)))
                   )
                {
                    throw new ArgumentException(String.Format(CultureInfo.CurrentCulture,
                        "\nThe provided string {0} is not a valid longitude.\n" +
                        "Value #{1} ({2}) is out of bounds.",
                        s,
                        valIndex+1,
                        values[valIndex]));
                }
                valIndex++;
            }
            return new Longitude(values[0], values[1], values[2]);
        }

        public override string ToString()
        {
            return string.Format(CultureInfo.CurrentCulture, "{0}°{1}'{2}\"",
                _degrees.ToString(CultureInfo.CurrentCulture),
                _minutes.ToString(CultureInfo.CurrentCulture),
                _seconds.ToString(CultureInfo.CurrentCulture));
        }

 

Before you can use your own, personal LongitudeMaskedTextBox component (well actually mine, but we can share it if you like ;) you will need to rebuild the solution. After you have done so you will find the component in the Form Design Manager Toolbar (at the very top). Put it onto your form, add any other component (a button for example, to enable the user to “leave” the LongitudeMaskedTextBox component to fire the validating event) and run the application.

To experience the MaskInputRejected event handler select the MaskedTextBox and input a non-digit value. The TypeValidationCompleted handler will fire when you input a digit as first character and leave the MaskedTextBox.

This all only makes sense if we actually got a way of putting the formatted string into our Longitude object. To achieve this I added a method GetLongitude() of type Longitude to the LongitudeMaskedTextBox class. This method calls the static method Longitude.Parse() while passing its own Text member. Even though we have done loads of error checking in several different places it is still possible that the Text member holds invalid values. For example when the entered degrees are bigger than 180°, the MaskedTextBox still has focus and the user presses a button which again calls the GetLongitude() method. When loosing focus the MaskedTextBox will fire the error events but GetLongitude() will get executed nevertheless, returning a new Longitude object holding the default values. To solve this problem I added another member to the LongitudeMaskedTextBox class: IsValidLongitude() which basically does the same as the Parse() method of the Longitude class and returns true if the formatted string actually holds a valid longitude. As mentioned those two methods go into the LongitudeMaskedTextBox class:

        public bool IsValidLongitude()
        {
            // replace prompt char occurences with "0"
            string ts = this.Text.Replace("_", "0");
            // if both strings differ there must be prompt chars left -> no valid longitude
            if (String.Compare(ts, this.Text) != 0)
                return false;

            // check weather the first char is neither "+" "-" nor " "
            if ((this.Text.Substring(0, 1) != "+") &&
                (this.Text.Substring(0, 1) != "-") &&
                (this.Text.Substring(0, 1) != " "))
            { // its neither, so it must be a digit - throw exception
                return false;
            }

            // an array for holding the longitude values
            int[] values = new int[3];

            ts = this.Text;

            // for several different delimiters simply replace
            // them all with the same delimiter (i.e. space or ".")
            ts = this.Text.Replace("°", "."); // convert ° delimiter to . delimiter
            ts = ts.Replace("'", "."); // convert ' delimiter to . delimiter
            ts = ts.Replace("\"", ""); // remove " - its not delimiting

            // put all different values (3), delimited by '.' into a new array of strings
            string[] strValues = ts.Split(new char[] { '.' });

            int valIndex = 0; // counter
            foreach (string strValue in strValues)
            {
                // try to parse the current value to an integer
                values[valIndex] = int.Parse(strValue);
                // check bounds -180 - 180 for degrees and 0 - 60 for minutes and seconds
                if (
                    ((valIndex == 0) && ((values[valIndex] > 180) || (values[valIndex] < -180))) ||
                    (((valIndex == 1) || (valIndex == 2)) && ((values[valIndex] > 60) || (values[valIndex] < 0)))
                   )
                {
                    return false;
                }
                valIndex++;
            }
            return true;
        }

        public Longitude GetLongitude()
        {
            return Longitude.Parse(this.Text);
        }

 

Last but not least a little example on how to put this all to use. Add a LongitudeMaskedTextBox component to your form, make sure its called “longitudeMaskedTextBox1″. Add a button called “button1″ and four labels named “label1″ to “label4″. Double click the button to create a new event handler. In the code view of your form and copy and paste the following:

using System;
using System.Windows.Forms;

namespace LongitudeMTB
{
    public partial class Form1 : Form
    {
        Longitude longitude = new Longitude(0,0,0);

        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            if (longitudeMaskedTextBox1.IsValidLongitude() == true)
            {
                longitude = longitudeMaskedTextBox1.GetLongitude();
                label1.Text = "Degrees: " + longitude.Degrees.ToString();
                label2.Text = "Minutes: " + longitude.Minutes.ToString();
                label3.Text = "Seconds: " + longitude.Seconds.ToString();
                label4.Text = "Full String: " + longitude.ToString();
            }
            else
            {
                label1.Text = "Degrees: invalid longitude";
                label2.Text = "Minutes: invalid longitude";
                label3.Text = "Seconds: invalid longitude";
                label4.Text = "Full String: invalid longitude";
            }
        }
    }
}

So what is happening? First we instanciate our class Longitude. When we click the button the application first test weather the formatted string holds a valid longitude, if so it creates a new Longitude object and displays its members. If the formatted string does not hold a valid longitude it fills the labels with an error message.

If you like you can download the full source of this tutorial:

[dm]1[/dm]

Also, here a few interesting links for people who like the input to shift from right to left (i.e. when inputting currencies):

    And one at StackOverflow about how to position the caret:

      For people who have trouble displaying short dates:

        VN:F [1.9.18_1163]
        Rating: 5.0/5 (3 votes cast)
        MaskedTextBox and Custom Types, 5.0 out of 5 based on 3 ratings
        Tuesday, February 3rd, 2009 at 23:29
        242 visits
        • Fabian
          Sep 9th, 2009 at 10:48 | #1

          Great article, helped me alot! Thanks for sharing!

          VA:F [1.9.18_1163]
          Rating: 0.0/5 (0 votes cast)

        Leave a comment

        XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>