Masked Currency TextBox for Visual Studio 2005 Beta


Figure 1 - Utilizing the MaskedTextBox in Visual Studio 2005 

In trying out the new Visual Studio 2005 MaskedTextBox, I found I ran into a problem with the MaskedTextBox acting as a currency control.  No matter what property I changed, I couldn't seem to get it to behave the way I wanted. That is,  I want the currency to be entered from right to left. Setting the RightToLeft property seemed to behave strangely (and this property is meant for RightToLeft languages not currencies), so I nixed this idea.  The best way to get it to behave the way I wanted, was to override a few event handlers. The behavior I am trying to mimic goes something like this. Say I want to type the price  $33.45. As I type the control should look as follows:

Method I:

$__._5

$__.45

$_3.45

$33.45

Currently the MaskedTextBox with Mask $99.99 behaves as follows:

Method II:

$3_.__

$33.__

$33.4_

$33.45

Although you can get method 2 to work as a user, it puts the burden on the user to figure out how to type in the price. You are probably better off making it easier on the user (as in method 1) so he doesn't throw your product in the garbage.

UML Design

The design for overriding the MaskedTextBox is shown in Figure 2. We override two important members of the MaskedTextBox here: OnKeyPress for intercepting all characters typed into the text box and ValidateText for converting our edited value to a double. The DoubleFromText method is used by ValidateText to calculate a double value from the user input. The Shift method shifts the numeric characters from right to left as the user types and is called by the OnKeyPress event handler.

Figure 2 - MaskedCurrencyTextBox reverse engineered using the WithClass UML Tool 

The Code

The code for overriding the behavior of the MaskedTextBox turns out to be not that many lines. Below is the event handler that intercepts a key press. The code checks to see if we pressed a digit. If we did, we shift the characters from right to left and insert the typed character at the last available prompt character as shown in listing 1.

Listing 1 - Overriding the KeyPress event

        protected override void OnKeyPress(KeyPressEventArgs e)
        {
            if (e.KeyChar >= '0' && e.KeyChar <= '9')
            {
               
// move all digits over to the left
                Shift(); 
                // make sure there is a prompt to replace
                int index = this.Text.LastIndexOf(PromptChar);
                if (index >= 0)
                {
                   
// insert the character typed into the last prompt character of the Text string
                    this.Text = this.Text.Substring(0, index) + e.KeyChar + this.Text.Substring(index);
                }
           }
            base.OnKeyPress(e);
        }

Shifting the user text right to left is accomplished by altering the Text property using string manipulation methods. We simply eliminate the first prompt we find and tag a new prompt onto the end of the string as shown in listing 2.

Listing 2 - Shifting the entry string right to left

     void Shift()
         {
            int firstIndex = this.Text.IndexOf(PromptChar);
            if (firstIndex >= 0)
            {
                // concatenate up to the first prompt, skip this prompt, and tack on a new prompt character
                Text = Text.Substring(0, firstIndex) + Text.Substring(firstIndex + 1) + PromptChar;
            }
        }

You may be asking yourself, "Why can't I just plop the string in each time without all these prompts?". In order to fool the MaskedTextBox control into thinking its still behaving as a MaskedTextBox control, you need to keep the prompts intact, so when the base.OnKeyPress is called, the base method still gets to work with a string it expects.

Conversion

Microsoft has an interesting mechanism for converting values out of the MaskedTextBox. They use a property called ValidatingType and a method called ValidateText. You can set the ValidatingType to a the type you want to convert your input entry string and use the ValidateText to attempt to convert the entry string to a value of that type.  For example:

Unfortunately, ValidateText doesn't work completely for our overriden control. Let's take the following situation. I enter into the currency control and type 3. Below is the result:

$__._5

The way the masked edit control does the conversion is strips off the unimportant literals and runs a convert on the remaining string. The remaining string in this case is

.<space>5

This string will throw an exception when the MaskedTextBox attempts to convert it. Therfore, we need to override the ValidateText method. Unfortunately again, the MaskedTextBox does not allow us to override this method, so we have to block the compiler from calling it by declaring it with a new parameter. Listing 3 shows the overriden ValidateText method for our MaskedCurrencyTextBox.  We create our own home brewed method, DoubleFromText shown in listing 4, to handle the situation described above where we have a space between our decimal point and our number.

Listing 3 - Newing the ValidateText of the MaskedTextBoxControl

         /// <summary>
        ///
Retrieve a double value from the text
        ///
</summary>
        /// <returns></returns>
 
        public new object ValidateText()
        {
            double val = 0.0; 
            try
            {
                val = DoubleFromText();  // convert user text into a double
            }
            catch (Exception ex)
            {
                return val;
            }
            return val;
        }

Our DoubleFromText method mimics the behavior of the current ValidateText by stripping all unnecessary characters in the double calculation. In addition, it inserts 0's in place of all prompts to prevent the unwelcome space character situation.

Listing 4 - Converting the contents of the MaskedCurrencyControl to a double value

     object DoubleFromText()
        {
            // remove all underscores, dollar signs and commas
 
            // but first replace all prompt characters with 0's
            string txtValue = Text;
            txtValue = txtValue.Replace(PromptChar, '0');
             // remove dollar signs
            int index = txtValue.IndexOf("$");
            while (index >= 0)
            {
                txtValue = txtValue.Remove(index,1);
                index = txtValue.IndexOf("$");
            }
            // remove commas that mark thousands place
            index = txtValue.IndexOf(",");
            while (index >= 0)
            {
                txtValue = txtValue.Remove(index,1);
                index = txtValue.IndexOf(",");
            }
            // Now do the conversion
           
try
            {
                double val = Convert.ToDouble(txtValue);
                return val;
            }
            catch (Exception ex)
            {
                throw (new Exception("Exception Parsing Masked Text Box"));
            }
            
finally
            {
            }
        }

Using the MaskedCurrencyTextBox

To use the MaskedCurrencyTextBox, we first need to set the properties. The mask we will use is $9999.99.   This mask causes the input to only except digits. In addition, the mask does not force us to fill all of the digits represented by the mask (If we had set the mask to $0000.00, all digits would need to be entered for a valid entry). Below is a property control describing the property settings of our price mask.

Figure 3 - MaskedCurrencyTextBox settings

In our sample we have 4 masked textboxes for determining our total price: two quantity textboxes and two price textboxes. When we enter these values, we want to compute the total that is owed as shown in figure 4.

Figure 4 - Computing the total order

Listing 5  shows how to get the values from the masked text boxes in order to fill the total field. We simply call ValidateText on each of the MaskedTextBoxes to get the value. mtxtPriceBar and mtxtPriceTee are our MaskedCurrencyControls. When ValidateText is called on these controls, it will execute ValidateText in our newed method of listing 3.

Listing 5 - Using our MaskedCurrencyControl to calculate price total

   void Recalculate()
        {
           
try
            {
                int q1 = 0;
                int q2 = 0;
                if (mtxtQuantityBar.ValidateText() != null)
                    q1 = (int)mtxtQuantityBar.ValidateText();
                if (mtxtQuantityTee.ValidateText() != null)
                    q2 = (int)mtxtQuantityTee.ValidateText();
                double p1 = (double)mtxtPriceBar.ValidateText();
                double p2 = (double)mtxtPriceTee.ValidateText();
 
                txtTotal.Text = "$" + (q1 * p1 + q2 * p2).ToString("0.00");
            }
            catch (Exception ex)
            {
            }
        }

Conclusion

The MaskedTextBox is a powerful control that lets you prompt the user for phone numbers, dates, and other formatted fields in a way you would expect to see these values. The MaskedCurrencyControl overrides the behavior of the MaskedTextBox in order to perform currency entry in a more logical way than the default behavior. Since the MaskedTextBox is only a beta, I would expect that Microsoft will probably fix the current behavior of the currency control. In the meantime, if you can't wait for the release, here is a control that may serve the purpose for the right price.