WPF Maskable TextBox for Numeric Values
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:
{
Any,
Integer,
Decimal
}
Next thing would be definition of the attached property.
{
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:
or
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:
...
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:
- Source of WPF Maskable Textbox
Includes source code and demo application for WPF Maskable Textbox.
- Binary of WPF Maskable Textbox
Demo application for WPF Maskable Textbox.
Nice work man! Thanks for sharing.
@Jeroen de Zeeuw
You’re welcome. I’m glad the article was useful.
nice, really nice!
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?
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
@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?
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
@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
@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.
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?
My bad. Its because of the styling. Sorry for the earlier post. I changed the styling and it worked,
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:
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.
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
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!
@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?
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.
Stefano, Yes you can do that from the code behind.
Just try: theTextBox.SetValue(TextBoxMaskBehavior.MaximumValue, 100);
Thank you.
>> 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.
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…
Hi Matthew,
Thanks for the fix. I will include it in next versions with your permission.
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
@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
@Luigi
That’s correct, never noticed that case before. I will work on this issue and will provide a solution.
Thank you.
@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.
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
@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?
@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.
@Tony
Yup, it makes sense to me. Please come back about in a week. Will update the code and post the fixed version.
@rubenhak
>> I will include it in next versions with your permission.
Permission granted.
Cool control, thanks! The functionality seems broken when the TextBox is bound to a property. Any idea why that may be?
@Jay
Hi Jay, can you please post here sample usage.
Thanks!
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?
@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
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
@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.
@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.