I am using PreviewTextInput to validate a decimal number. Here is my code:
private void AmountTextBox_PreviewTextInput(object sender, System.Windows.Input.TextCompositionEventArgs e)
{
TextBox t = sender as TextBox;
string newText = t.Text.Insert(t.SelectionStart,e.Text);
Regex regex = new Regex(@"^[0-9]{1,12}(?:\.[0-9]{0,2})?$");
e.Handled = !regex.IsMatch(newText);
}
This properly accepts only digits and one decimal point, but the decimal point is only accepted if the user inserts it between numbers. If the decimal is entered at the end of the string, it is not added to the textbox, even though the regex returns a match (i.e., e.Handled = false). Naturally, when entering a value, a user would enter the left of the decimal, then the decimal point, and then the right of the decimal. Why is the decimal getting dropped? Is it an artifact of the PreviewTextInput processing? Am I just missing something obvious?
There is an implicit type conversion executed by the WPF binding engine. And when you convert "1."
to double
the decimal separator gets dropped by the parser. If you bind the TextBox.Text
property to a string
property then the issue disappears as the numeric conversion is avoided.
It should be noted that you should override the TextInput
or PreviewTextInput
instead of registering an event handler. In general, you should always override inherited virtual event handlers instead of registering a new handler. If your code is not implemented in a custom class that extends TextBox, you should change it. This kind of logic should be encapsulated in the control.
Regex is too expensive for that simple task (checking if a text is numeric). You should use e.g., double.TryParse
.
You should not hardcode the decimal separator. Instead use the CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator
value to allow your control to adapt to different cultures (considering that 50% of the world's countries use ',' as decimal separator).
Also note that pasting text into the TextBox
does not raise the TextInput
event and therefore bypasses your input validation or fatting. To account for that case, you should register a data pasted event handler by calling the DataObject.AddPastingHandler
method: DataObject.AddPastingHandler(OnInputPasted)
One solution to solve your conversion problem is to detect a trailing decimal separator and then append a 0
to make it a valid decimal number. You can then automatically overwrite that generated 0
with the next user input:
protected override void OnTextInput(TextCompositionEventArgs e)
{
e.Handled = true;
string input = e.Text;
string currentContent = this.Text;
string numberDecimalSeparator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator;
ReadOnlySpan<char> newContent;
if (this.IsSelectionActive && this.SelectionLength > 0)
{
currentContent = currentContent.Remove(this.SelectionStart, this.SelectionLength);
}
bool isInputDecimalSeparator = input.Equals(numberDecimalSeparator, StringComparison.OrdinalIgnoreCase);
// Input is a decimal separator: append a "0" (if this is the only separator)
if (isInputDecimalSeparator)
{
e.Handled = true;
int existinDecimalSeparatorIndex = currentContent.LastIndexOf(numberDecimalSeparator, StringComparison.OrdinalIgnoreCase);
bool isDecimalSeparatorPresent = existinDecimalSeparatorIndex != -1;
if (isDecimalSeparatorPresent)
{
// The next character is already a decimal separator. Just advance the caret.
// Otherwise ignore the input.
if (existinDecimalSeparatorIndex == this.CaretIndex)
{
++this.CaretIndex;
}
return;
}
// Only auto complete input if decimal separator is trailing the cotent
this.isContentAutoCompleted = this.CaretIndex == currentContent.Length;
if (this.isContentAutoCompleted)
{
newContent = currentContent + input + "0";
}
else
{
newContent = currentContent.Insert(this.CaretIndex, input);
newContent = EnsureDecimalPlaces(newContent);
}
}
else
{
bool isInputNumeric = double.TryParse(input, out _);
if (!isInputNumeric)
{
return;
}
// Overwrite the auto appended "0" with the new input
if (this.isContentAutoCompleted)
{
this.isContentAutoCompleted = false;
newContent = currentContent[..^1] + input;
}
else
{
newContent = currentContent.Insert(this.CaretIndex, input);
}
newContent = EnsureDecimalPlaces(newContent);
}
// Setting the Text property will reset the caret position.
// Therefore we need to restore it.
int currentCaretIndex = this.CaretIndex;
SetCurrentValue(TextBox.TextProperty, newContent.ToString());
// Advance the caret
this.CaretIndex = currentCaretIndex + 1;
}
private ReadOnlySpan<char> EnsureDecimalPlaces(ReadOnlySpan<char> newContent)
{
double numberValue = double.Parse(newContent, CultureInfo.CurrentCulture);
string numbersFormatSpecifier = "F" + this.numberOfDecimalPlaces;
newContent = numberValue.ToString(numbersFormatSpecifier, CultureInfo.CurrentCulture);
return newContent;
}