Home > .NET, WPF > WPF Maskable TextBox for Numeric Values

WPF Maskable TextBox for Numeric Values

March 10th, 2009 Leave a comment Go to comments

Introduction

This article describes how to enhance WPF TextBox and make it accept numeric(integer and floating point) values. The second goal is make the TextBox smart enough to have it easier to input numerics. The easy means to provide the TextBox with some kind of intelligence not just rejecting non-numeric symbols. Provided extension also allows setting minimum and/or maximum values.

If you search in the net, you will probably find some solutions for this problem where developers create their own versions of TextBox either by inheriting from it or creating a Custom/User Controls that include standard WPF TextBox. Previous solutions have one major drawback – you would need to replace your TextBox definitions with your new MaskTextBox. Sometimes it is not painful, sometimes it is. The reason I chose another solutions is that in my case such kind of change would be painful.

The approach I’m proposing here is a usage of WPF Attached Properties, which basically are similar to Dependency Properties. The major difference among these to is that Dependency Properties are defined inside the Control, but Attached Properties – outside. For instance, TextBox.Text is Dependency Property, but Grid.Column is an Attached Property.

Background(Extending Functionality of TextBox)

First of all I will define an enumeration, which tells us whether the TextBox accepts integer, decimal or any kind of values:

public enum MaskType
{
    Any,
    Integer,
    Decimal
}

Next thing would be definition of the attached property.

public class TextBoxMaskBehavior
{
    public static MaskType GetMask(DependencyObject obj)
    {
        return (MaskType)obj.GetValue(MaskProperty);
    }

    public static void SetMask(DependencyObject obj, MaskType value)
    {
        obj.SetValue(MaskProperty, value);
    }

    public static readonly DependencyProperty MaskProperty =
        DependencyProperty.RegisterAttached(
        "Mask",
        typeof(MaskType),
        typeof(TextBoxMaskBehavior),
        new FrameworkPropertyMetadata(MaskChangedCallback)
        );

    private static void MaskChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        // ...
    }
}

Now, we may specify the Mask for WPF TextBox like this:

<TextBox local:TextBoxMaskBehavior.Mask="Integer" />

or

<TextBox local:TextBoxMaskBehavior.Mask="Decimal" />

and our assumption would be that at the particular TextBox would accept integer and decimal values correspondingly. Whenever TestBoxMaskBehavior.Mask is set, the MaskChangedCallback is being called. We will add corresponding handling there.

Background(Subscribing to TextBox Changes)

WPF TextBox comes with an event: PreviewTextInput, which allows to listen for text input. It also allows to cancel particular text input by setting the Handled property of the TextCompositionEventArgs to true. Below is an illustrative example:

...
private static void TextBox_PreviewTextInput(object sender, System.Windows.Input.TextCompositionEventArgs e)
{
    try
    {
        Convert.ToInt32(e.Text);
    }
    catch
    {
        e.Handled = true;
    }
}

Please note, that this code is just an example of PreviewTextInput event usage. My solution much more useful featurer rather then checking the input text is number or not :) .

There is another event we will need to subscribe. PreviewTextInput does not receive a notification when text is being pasted from the clipboard. For this purpose we will recourse to DataObject class. Check out this example:

DataObject.AddPastingHandler(myTextBox, TextBoxPastingEventHandler);
...
private void TextBoxPastingEventHandler(object sender, DataObjectPastingEventArgs e)
{
    string clipboard = e.DataObject.GetData(typeof(string)) as string;
    try
    {
        Convert.ToInt32(clipboard);
    }
    catch
    {
        e.CancelCommand();
        e.Handled = true;
    }
}

I think it was self-explanatory :) .

Maskable TextBox

We have covered techniques that I used in the solution. Full source code and demo binary is available from links below.
Here is list of features available for my Maskable TextBox:

  • Rejects symbols other than digits, negative sign and decimal separator.
  • Clamps to minimum/maximum values if any specified.
  • When typing negative sign regardless from the caret position, adds it in the beginning if not exits, or removes existing one.
  • When typing decimal separator removes the existing one (if exists), and places the new one in the correct place.
  • And more useful stuff. Just try to use it. I’m sure you will not regret.

Downloads

Attached Files:

  1. avatar
    Jeroen de Zeeuw
    March 22nd, 2009 at 04:13 | #1

    Nice work man! Thanks for sharing.

  2. avatar
    ruben hakopian
    March 22nd, 2009 at 12:33 | #2

    @Jeroen de Zeeuw
    You’re welcome. I’m glad the article was useful.

  3. April 16th, 2009 at 19:29 | #3

    nice, really nice!

  4. avatar
    vazoffsky
    May 25th, 2009 at 03:21 | #4

    Sorry mate
    I’m not familiar with WPF and framework 3.0. So excuse me if this question sounds stupid… but still:
    The only text check for the right type I saw was
    Convert.ToInt32(e.Text); in a try catch….
    is this what you have to write for every type you add
    into the MaskType enum? or is this done automatically someway?
    if the case is the first option… then what is the point of doing all this AT ALL?!? can’t you just subscribe to the TextBox.TextChanged event and check if the text is of correct format?

  5. June 2nd, 2009 at 02:40 | #5

    Hey dude! Answering your questions – yes you may subscribe to TextChanged, and if the text is not in the correct format than disallow the text change. However you should also max/min coercing when needed. My suggestion is that instead of coding all this just specify local:TextBoxMaskBehavior.Mask=”Decimal”, local:TextBoxMaskBehavior.Minimum=”-5.5″, local:TextBoxMaskBehavior.Maximum=”15.4″ properties and it will do all the magic for you. It also provides some convenient way of typing decimal numbers. Whenever you type a decimal point, it would remove the existing one and some other cool stuff. You want to format non-numerical data other then you would need to modify the MaskEnum, and corresponding TextChange handlers.

    P.S. The “Convert.ToInt32(e.Text); in a try catch” was is not the best solution, I should have use “System.Int32.TryParse(e.Text)” instead :)

  6. avatar
    vazoffsky
    June 2nd, 2009 at 03:18 | #6

    @rubenhakopian
    you still did not answer the following question:
    Convert.ToInt32(e.Text); in a try catch….
    is this what you have to write for every type you add
    into the MaskType enum?

  7. avatar
    Nino Strmo
    June 24th, 2009 at 00:22 | #7

    Nice work,
    just one small issue: max and min are not eveluated when pasting.
    ValidateValue shoud have something like
    double dValue = Convert.ToDouble(value);
    return ValidateLimits(min, max, dValue).ToString();

    Best regards,
    Nino

  8. avatar
    ruben hakopian
    July 6th, 2009 at 23:00 | #8

    @vazoffsky
    answering your questions – yes, whenever you add a check for a new type, you will need to add it to the try… catch.. block

  9. avatar
    ruben hakopian
    July 6th, 2009 at 23:01 | #9

    @Nino Strmo
    Thanks for posting the problem. I’ve already fixed and posted it. I also got rid of unnecessary try/catch blocks, so please get the new version instead.

    Thank you.

  10. avatar
    Steve
    October 8th, 2009 at 14:57 | #10

    This was a really wonderful article. Thanks for posting it. I am new to WPF and this article was really helpful in understanding. One thing I was trying to do was in TextBox_PreviewTextInput at the bottom I added _this.ToolTip = _this.Text;
    I added this line so that every time I change my text tool tip is updated. However this line is not working and my tool tip is not updated. Do you have an idea how to do this?

  11. avatar
    Steve
    October 8th, 2009 at 15:39 | #11

    My bad. Its because of the styling. Sorry for the earlier post. I changed the styling and it worked,

  12. October 18th, 2009 at 11:44 | #12

    Steve, Thanks for feedback! I appreciate it.
    Concerning the Tooltip, if you want to make it same as text, you may just try to bind the Text property to Tooltip. In XAML it will look like:
    .

  13. avatar
    Dimi
    February 4th, 2010 at 16:28 | #13

    There is a short of a problem when you want to start typing with the negative sign which complicates things a bit. Pls could you fix it?

    Thanks.

  14. avatar
    Harold Chattaway
    April 2nd, 2010 at 13:43 | #14

    Thanks for a great article. its amazing this isn’t built into the control natively!

    I am looking for another mask ability.. where you can specify how many positions before and after a decimal point is allowed. For example, I may want to limit it to 99.999. Is there a way of specifying these kinds of masks?
    Thanks
    Harold

  15. May 11th, 2010 at 02:27 | #15

    Harold, I believe you can enhance keypress handlers and add corresponding validations there. Let me know if you encounter some difficulties with that.

    Thanks for nice comments!

  16. May 11th, 2010 at 02:28 | #16

    @Dimi, At any caret position, when the ‘-’ button is pressed, the textbox changes the sign. Just like in windows calculator. Please let me know what is confusing there?

  17. avatar
    Stefano
    May 19th, 2010 at 09:43 | #17

    Good work,
    is there a way to assign (or change) properties like MinimumValue and MaximumValue from code? Sometimes this values are not known in the xaml moment.
    Thanks.

    • June 30th, 2010 at 09:10 | #18

      Stefano, Yes you can do that from the code behind.
      Just try: theTextBox.SetValue(TextBoxMaskBehavior.MaximumValue, 100);
      Thank you.

  18. avatar
    Matthew Wills
    June 2nd, 2010 at 20:05 | #19

    >> At any caret position, when the ‘-’ button is pressed, the textbox changes the sign. Just like in windows calculator. Please let me know what is confusing there?

    Setup a textbox of type decimal with min value of -360 and max of 360. Then type in -1 (in that order). The result will be 1.

    If you type in 1- (in that order). The result will be -1.

    It would be great it -1 resulting in -1 as well. Currently, – is ‘ignored’ if it is the first character due to a failing call to Convert.ToDouble.

  19. avatar
    Matthew Wills
    June 2nd, 2010 at 20:37 | #20

    You can fix the specific issue I mention above by replacing the relevant try catch block with:

    try
    {
    double val = 0;
    bool useNegativeZero = false;

    if (text == NumberFormatInfo.CurrentInfo.NegativeSign)
    {
    useNegativeZero = true;
    }
    else
    {
    val = Convert.ToDouble(text);
    }

    double newVal = ValidateLimits(GetMinimumValue(_this), GetMaximumValue(_this), val);
    if (val != newVal)
    {
    text = newVal.ToString();
    }
    else if (val == 0)
    {
    if (!text.Contains(NumberFormatInfo.CurrentInfo.NumberDecimalSeparator))
    {
    if (useNegativeZero)
    {
    text = “-0″;
    caret++;
    }
    else
    {
    text = “0″;
    }
    }
    }
    }
    catch
    {
    text = “0″;
    }

    Note that it isn’t perfect though – for example, if type in 0 then select the text (0 – to overwrite it) and type in -360 then it doesn’t result in -360 (instead it results in 360).

    But you get the general idea…

    • June 29th, 2010 at 17:32 | #21

      Hi Matthew,
      Thanks for the fix. I will include it in next versions with your permission.

  20. avatar
    Luigi
    July 19th, 2010 at 03:56 | #22

    I have tested the example and it seems a very good job.

    I have a question, if I set the minimum value of the third textbox in your example to 10, for me is impossible to set the value 25 (I select current text in textbox and press 2 to write 25, but text is forced to 10).

    There is a solution to this problem?

    Regards

  21. avatar
    Luigi
    July 19th, 2010 at 05:36 | #23

    @Luigi
    I have added this ‘dirt’ trick:

    In the TextBox_PreviewTextInput event, in the try catch block, where is verified the new text, I added control on the length of string, if lower of the minimum value lenght I ignore the limits.

    try
    {
    double val = Convert.ToDouble(text);
    double newVal = ValidateLimits(GetMinimumValue(_this), GetMaximumValue(_this), val);
    bool forceLimits = (text.Length >= GetMinimumValue(_this).ToString().Length);

    if (val != newVal)
    {
    if (forceLimits)
    text = newVal.ToString();
    //else
    // _this.Background = System.Windows.Media.Brushes.Red;
    }
    else if (val == 0)
    {
    if (!text.Contains(NumberFormatInfo.CurrentInfo.NumberDecimalSeparator))
    text = “0″;
    }
    }
    catch
    {
    text = “0″;
    }

    To check the text when the control lose the focus, I added in MaskChangedCallback the handler to the event PreviewLostKeyboardFocus

    private static void MaskChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
    if (e.OldValue is TextBox)
    {
    (e.OldValue as TextBox).PreviewTextInput -= TextBox_PreviewTextInput;
    (e.OldValue as TextBox).PreviewLostKeyboardFocus -= TextBox_PreviewLostKeyboardFocus;
    DataObject.RemovePastingHandler((e.OldValue as TextBox), (DataObjectPastingEventHandler)TextBoxPastingEventHandler);
    }

    TextBox _this = (d as TextBox);
    if (_this == null)
    return;

    if ((MaskType)e.NewValue != MaskType.Any)
    {
    _this.PreviewTextInput += TextBox_PreviewTextInput;
    _this.PreviewLostKeyboardFocus += TextBox_PreviewLostKeyboardFocus;
    DataObject.AddPastingHandler(_this, (DataObjectPastingEventHandler)TextBoxPastingEventHandler);
    }

    ValidateTextBox(_this);
    }

    And in the event I simple call the ValidateTextBox to check the limits

    private static void TextBox_PreviewLostKeyboardFocus(object sender, System.Windows.Input.KeyboardFocusChangedEventArgs e)
    {
    TextBox _this = (sender as TextBox);
    ValidateTextBox(_this);
    }

    This is the first ‘quick’ solution that I found

    Sorry for my english

  22. July 22nd, 2010 at 09:39 | #24

    @Luigi
    That’s correct, never noticed that case before. I will work on this issue and will provide a solution.

    Thank you.

  23. July 22nd, 2010 at 09:41 | #25

    @Luigi
    Thank you! The idea looks good. Most probably i might need to do minimum/maximum validation when the focus is lost. Will include it to the package.

  24. avatar
    Tony
    August 24th, 2010 at 06:13 | #26

    Thanks for sharing this class, it has been very useful for me.

    One problem I am having with it though is that properties bound to the TextBoxes are not updated when the last character is deleted. This means if I entered the string “123″ and then deleted it again my bound property will have the value of “1″.

    I’ve not yet tried to fix this problem and if I do solve it before you post a fix I will send you the code.

    Thanks

  25. August 24th, 2010 at 14:33 | #27

    @Tony
    Happy to hear that it was useful for you.

    What do you expect the value of the bound property to be when the text is completely deleted?

  26. avatar
    Tony
    August 24th, 2010 at 16:10 | #28

    @rubenhak
    Hi there,
    I would think it should be the default value for the property type, i.e. if a nullable type then “null” otherwise 0.

    Setting a non-nullable type to 0 is obviously not ideal, however I don’t see any better option. I would imagine having it set to 0 is much less troublesome than leaving the property in an indeterminate state – the last number deleted.

  27. August 26th, 2010 at 22:51 | #29

    @Tony
    Yup, it makes sense to me. Please come back about in a week. Will update the code and post the fixed version.

  28. avatar
    Matthew Wills
    September 20th, 2010 at 17:41 | #30

    @rubenhak

    >> I will include it in next versions with your permission.

    Permission granted. :)

  29. avatar
    Jay
    October 7th, 2010 at 11:41 | #31

    Cool control, thanks! The functionality seems broken when the TextBox is bound to a property. Any idea why that may be?

  30. October 7th, 2010 at 17:21 | #32

    @Jay
    Hi Jay, can you please post here sample usage.
    Thanks!

  31. avatar
    Bjørnar
    October 27th, 2010 at 05:02 | #33

    Hi
    Great code. There is just one problem I experience with using MaskType.Decimal, and that is when I click decimal separator, it is not entered into the textbox, and I’m not able to specify a decimal. When debugging the code, the last few lines in TextBox_PreviewTextInput assign new values to text, caret, etc for the textbox. When stepping over these assignments, the value doesn’t change, and I can’t see why.

    I also added the following code to the beginning of the TextBox_PreviewTextInput method to validate against the MaxLength property of the textbox.
    if (_this.MaxLength > 0 && _this.Text.Length >= _this.MaxLength)
    {
    e.Handled = true;
    return;
    }

    Also, timestamps on the files in the zip are from last year. Is there a different download to get the latest version?

  32. avatar
    Bjørnar
    October 27th, 2010 at 05:17 | #34

    @Bjørnar
    That is, to get the value 10.3, i need to enter 103 and then add the comma at the correct location. I can’t enter them in the order 1, 0, ., 3

  33. avatar
    lasko
    November 7th, 2010 at 04:00 | #35

    dear friend
    thank you for good job
    but there is some confusing i applyed your work on my text box that in DataTemplate witch would be choosen from DataTemplateSelecor class and the result was good but i loost TwoWay binding for TextBox.Text as shown below:

    by the way the binding of MaxValue and MinValue is Working
    but this section never work <TextBox Text="{Binding Path=Value,Mode=TwoWay}"
    please reply me ASAP
    best regard

  34. November 9th, 2010 at 15:04 | #36

    @Bjørnar
    Hi, I didn’t really get the idea what you’re trying to do. I try to type “1″, “0″, “.”, “3″ and eventually get “10.3″. Please explain hos is that related to maxlength?

    Its been a while I didn’t make any changes to this project, but if you have improvements I’d happily include them.

  35. November 9th, 2010 at 15:14 | #37

    @lasko
    Binding to Text works fine to me since this is a regular WPF property. Please remember that you need to leave the focus of textbox in order to update binding. Otherwise try to put:

    Hope it helps.

  1. No trackbacks yet.