Click here to Skip to main content
Click here to Skip to main content

WPF Maskable Number Entry TextBox

, 25 May 2013
Rate this:
Please Sign up or sign in to vote.
WPF 'Maskable' Number Entry Box

Update: Opened a GitHub project: https://github.com/SmarterDB/WPF-Maskable-Number-Entry-TextBox 

Introduction 

Our company carries out database-centered development. Our main task is to display data from the database; and to make it available for in-line editing on the user interface.

Data Entry Form

With some data types (such as Dates, Boolean, etc.), there are satisfactory data entry items in the WPF environment. There are suitable masking possibilities for alphanumeric boxes as well. Displaying numbers at the same time as creating data entry possibilities, however, introduces a number of difficulties. At first glance, it is not even obvious that there might be any problems but, as we all know, the Devil is in the details. From now on, we are dealing with the details of displaying and editing numbers.

The Problem

The following issues arise when displaying numbers:

  • The thousands separator character
  • The decimal separator
  • The minimum and maximum values according to number display type (e.g. Int32.MinValue, Decimal.MaxValue, etc.)
  • Deleting numeric characters with Backspace or Del
  • Selecting and deleting multiple characters through either character entry or delete
  • Managing positive and negative numbers
  • Selecting text and managing selections

Dead-ends

Maskable Text Boxes

Maskable Texbox

Although at first it seemed to be a good solution to display data in maskable text boxes (examples: http://wpftoolkit.codeplex.com/wikipage?title=MaskedTextBox, http://geekswithblogs.net/QuandaryPhase/archive/2008/12/17/wpf-masked-textbox.aspx, etc.), we soon lost our taste for this solution. One issue is that the length of a text entry box mask is fixed. With numeric values, the number of digits cannot usually be predicted, so there' no real point in a mask like this; even if it is not mandatory to fill in all positions. Another issue is that when filling in a data entry box, progress takes place from left to right. When entering numbers, however, it makes more sense to display number inputs aligned to the right (as on electronic calculator displays).

Let's see an example! If we have to enter 12,314.25 in the mask above:

Maskable Textbox filled out

Then - after a little calculation - we have to start entering data in data entry position 7. This is similar to filling in a hard copy of a form: it certainly doesn't make using the program easier.

Using the Converter Function

While creating a WPF interface, TextBox can define a Converter function, formatting the value to be displayed in the TextBox within this function:

<TextBox Text=”{Binding ElementName=myvalue, Path=Text,
    Converter={StaticResource myformatter}, Mode=TwoWay}"/>

(The name of the function in this example is myformatter and it formats the current value of TextBox according to our needs.)

The number displayed is perfect; we can format all numbers the way we want:

formatted numbers

However, when editing the numbers, the entry box resets, and no formatting takes place:

Unformatted numbers

If you work with big numbers (like millions), it is a great help during editing if thousands separators are displayed. This makes the number entered clear at once:

or

Don’t you agree that in the latter case, it is much easier to tell one number from the other? Our mission is to give an advantage to our clients in the fierce competition they are in, so it is unquestionable that this solution is not good enough yet.

Background

The Task

As we couldn’t find a ready-made solution, we examined the issue further, and then set ourselves a target. At number entry, the characters usually allowed are numbers, negative and positive symbols, and the decimal separator. (http://stackoverflow.com/questions/5511/numeric-data-entry-in-wpf)

Integers

When displaying integers, only the numbers are displayed; the decimal separator and the zero characters are not. Entering decimal separator characters is not allowed in this scenario. (String.Format() character code: #,0.) We upload data in decreasing order (…->thousands->hundreds->tens->units). Displaying values takes place from left to right; aligned to the right. The cursor does not follow data entry, i.e.: the number is inserted to the left of the cursor when entering the digits, the cursor stays in place, and the number on the left becomes longer and longer. Also, the number is formatted with the thousands separator character during entry.

Decimal Values

If the integer part of the number is 0, then this 0 is displayed in front of the decimal separator. For instance: 0.24 (String.Format() character code in case there are two decimal values: #,0.00). The decimal separator is a special character; it is always displayed when editing decimal values.

Data entry position at the beginning is on the left of the decimal separator; between the decimal separator and the first value of the integer part. Digits that represent the integer part of the number are handled as we showed at integers.

After entering a decimal separator character (at all times, no matter whether we enter integers to the left of the decimal separator or we are entering a decimal value to the right of the decimal separator), the input position resets between the decimal separator and the digit that stands for the tenths of the decimal value, in preparation for receiving the digits of the decimal value in decreasing order (tenth -> hundredth -> thousandth). The cursor follows data entry, i.e.: it moves right after each number input.

Another problem arose during usage: at most data entries, it is always enough to display the decimal value in two digits (like in the case of prices or measurements, e.g. length, weight, etc.). However, there are other data types (like unit prices for large quantities), at which it is generally enough to use two decimal digits, but sometimes more digits are needed (for example 4). This means that generally two digits are enough to display the decimal value but sometimes there is an exception.

If we always display 4 decimal digits, then the screen becomes more difficult to read:

Because of this, we chose a solution where the input box is prepared to take two decimal digits, but it lets the user input more digits as needed. When we display a number with two decimal digits entered, we do not display the additional zero characters afterwards, but when 4 decimal digits are recorded in the database, then it is displayed correctly (using four digits). (String.Format() character code: #,0.00##).

Using the Code

As a solution (with the help of the following excellent article: http://www.codeproject.com/Articles/34228/WPF-Maskable-TextBox-for-Numeric-Values), we thought of attached properties.

public class TextBoxMaskBehavior
{
    MinimumValue Property

    MaximumValue Property

    Mask Property

    ValueType Property

    Static Methods

    Private Static Methods
}    

We format numeric values with String.Format(). Character coding at integers is #,0 while at decimals it is #,0.00##. Format string remains an adjustable parameter.

myentry:TextBoxMaskBehavior.Mask="0:#,0.00##"   

We differentiated data type from data format, as data type does not clearly define the format, but it is necessary for us to know what the type is.

public enum ValueTypes { NoNumeric, Integer, Double }
myentry:TextBoxMaskBehavior.ValueType="{Binding Path=TestDataDouble,
        Converter={StaticResource mytypeconverter}, Mode=OneWay}"

As we mainly deal with database programming, we use DataBinding at data entry. We created a simple Model Class for testing (its name is: SmarterDB.Codeproject.TestData.TestDataModel). The formatted number cannot be parsed by Binding at data reading; so we had to create a Converter Class (its name is: SmarterDB.CodeProject.DataEntry.NumericConverter.ConvertBack()).

    public object ConvertBack(object value, Type targetType,
                              object parameter,
                              System.Globalization.CultureInfo culture)
    {
        //We create numbers from formatted text.
        if (null == value)
        { return null; }
        else
        {
            HashSet<Type> numericTypes = new HashSet<Type>
                                    {typeof(Byte),
                                     typeof(Decimal),
                                     typeof(Double),
                                     typeof(Int16),
                                     typeof(Int32),
                                     typeof(Int64),
                                     typeof(SByte),
                                     typeof(Single),
                                     typeof(UInt16),
                                     typeof(UInt32),
                                     typeof(UInt64)
                                     };
                                     
            if (numericTypes.Contains(targetType.UnderlyingSystemType))
            {
                HashSet<Type> intTypes = new HashSet<Type>
                                    {typeof(Byte),
                                      typeof(Int16),
                                     typeof(Int32),
                                     typeof(Int64),
                                     typeof(SByte),
                                     typeof(UInt16),
                                     typeof(UInt32),
                                      typeof(UInt64)
                                      };
                string text = value.ToString().Replace(;
                NumberFormatInfo.CurrentInfo.NumberGroupSeparator, String.Empty);
                if (intTypes.Contains(targetType.UnderlyingSystemType))
                {
                    try { return Int32.Parse(text); }
                    catch { return null; }
                }
                else
                {
                    try { return Double.Parse(text); }
                    catch { return null; }
                }
            }
            else { return value; }
        }
    }
}    

We were even more surprised to notice that Binding does not use the pre-set localization used at the implicit conversion during data upload, but it converts to string recognizing only the decimal separator. This, on the other hand, isn't recognized backwards by Parse and it throws an exception. This justifies the use of the Convert method of the NumericConverter type mentioned above.

public object Convert(object value,
                      Type targetType,
                      object parameter,
                      System.Globalization.CultureInfo culture)
{
    if (null == value)
        return null;
    else
        if (targetType.UnderlyingSystemType == typeof(String))
            return value.ToString();
        else
            return value;
}    

If the data type at the other end of Binding was known, then the ValueType attached property wouldn't be needed, but we have discovered no effective solution for this. Due to the reasons mentioned in the previous paragraph, we must use a converter, so we created another converter class (Name: SmarterDB.CodeProject.DataEntry.NumericToValueTypeConverter) which creates the value of the ValueType attached property on the other side of Binding, so filling in ValueType is automatic (although it is not too elegant that the definition of TextBox needs to be overwritten - if somebody can come up with a better solution, he is more than welcome to share it with us).

public class NumericToValueTypeConverter : IValueConverter
{
    public object Convert(object value,
                          Type targetType,
                          object parameter,
                          System.Globalization.CultureInfo culture)
    {
        if (null == value)
        { return ValueTypes.NoNumeric; }
        else
        {
            HashSet<Type> numericTypes = new HashSet<type />
                                    {typeof(Byte),
                                     typeof(Decimal),
                                     typeof(Double),
                                     typeof(Int16),
                                     typeof(Int32),
                                     typeof(Int64),
                                     typeof(SByte),
                                     typeof(Single),
                                     typeof(UInt16),
                                     typeof(UInt32),
                                     typeof(UInt64)
                                     };
                                     
            if (numericTypes.Contains(value.GetType()))
            {
                HashSet<type /> intTypes = new HashSet<type>
                                    {typeof(Byte),
                                      typeof(Int16),
                                     typeof(Int32),
                                     typeof(Int64),
                                     typeof(SByte),
                                     typeof(UInt16),
                                     typeof(UInt32),
                                      typeof(UInt64)
                                      };
                if (intTypes.Contains(value.GetType()))
                {
                    return ValueTypes.Integer;
                }
                else
                {
                    return ValueTypes.Double;
                }
            }
            else { return ValueTypes.NoNumeric; }
        }
    }
    
    public object ConvertBack(object value, Type targetType,
                              object parameter,
                              System.Globalization.CultureInfo culture)
    {
        throw new NotSupportedException();
    }
}   

Two data types are used: Double and Int32.

switch (vt)
{
    case ValueTypes.Integer:
        if (value > Convert.ToDouble(Int32.MaxValue))
            return Convert.ToDouble(Int32.MaxValue);
        break;
        
    case ValueTypes.Double:
        //We stop two characters ahead, so as not to cause an exception.
        if (value > (Double.MaxValue/100))
            return (Double.MaxValue/100);
        break;
}    

History

  • 20th February, 2012: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

smarterdb
Software Developer (Senior) SmarterDB
Hungary Hungary
Our goal is no less than to make your SQL database smarter.
SmarterDB is a Microsoft SQL database server based smart tool for developing applications. Visit us.
www.smarterdb.com.
Group type: Organisation

4 members

Follow on   Twitter

Comments and Discussions

 
QuestionA problem when using in DataGridTemplateColumn [modified] Pinmembersynbari7-Apr-13 11:41 
AnswerRe: A problem when using in DataGridTemplateColumn Pingroupsmarterdb7-Apr-13 21:56 
AnswerRe: A problem when using in DataGridTemplateColumn Pingroupsmarterdb12-Apr-13 10:48 
AnswerRe: A problem when using in DataGridTemplateColumn Pingroupsmarterdb14-Apr-13 9:40 
QuestionBinding to Nullable field PinmemberMember 39510810-May-12 10:56 
AnswerRe: Binding to Nullable field Pingroupsmarterdb14-May-12 4:26 
QuestionEuro sign PinmemberMember 39510810-May-12 10:36 
AnswerRe: Euro sign Pingroupsmarterdb14-May-12 4:26 
QuestionDownloads are broken. PinmemberYogesh Jagota23-Apr-12 22:48 
AnswerRe: Downloads are broken. Pingroupsmarterdb23-Apr-12 23:29 
AnswerRe: Downloads are broken. Pingroupsmarterdb24-Apr-12 6:00 
GeneralRe: Downloads are broken. PinmemberYogesh Jagota24-Apr-12 20:13 
GeneralRe: Downloads are broken. Pingroupsmarterdb24-Apr-12 23:35 
GeneralMy vote of 5 Pinmembersagarok22-Feb-12 20:33 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140721.1 | Last Updated 25 May 2013
Article Copyright 2012 by smarterdb
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid