Home .NET Attached properties to limit textinput

Attached properties to limit textinput

by admin

WPF is far from being a new technology on the market, but relatively new to me.And, as is often the case when learning something new, there is a desire/need to invent bicycles with square wheels and castings to solve some typical problems.
One of these tasks is to restrict user input of certain data.For example, we want to allow only integer values in some text field, a date in a certain format in another, and only floating point numbers in a third.Of course, the final validation of such values will still take place in view-models, but such input restrictions make the user interface friendlier.
In Windows Forms this problem was solved pretty easy, and when you had the same TextBoxfrom DevExpress with built-in possibility to limit input using regular expressions, it was pretty easy.Examples of solving the same problem in WPF quite a lot Most of them come down to one of two variants: using an heir of the TextBox or adding attached property with the desired restrictions.
note
If you are not very interested in my reasoning, and you want code examples right away, you can either download the entire project
WpfEx from GitHub , or download the basic implementation, which is contained in TextBoxBehavior.cs and TextBoxDoubleValidator.cs

Shall we begin?

Since inheritance introduces a rather strict restriction, I personally like the use of attached properties in this case, the good news is that this mechanism allows you to limit the application of these properties to controls of a certain type (I do not want this property to IsDouble can be applied to a TextBlock for which it makes no sense).
Also, note that when restricting user input, you cannot use any specific delimiters for integer and fractional parts (such as ‘.’ (such as ” (period) or ‘, ‘ (comma)), as well as ‘+’ and ‘-‘ signs, as these all depend on the user’s regional settings.
To implement the ability to limit data entry, we need to capture the user’s input event, analyze it and undo those changes if they don’t suit us. Unlike Windows Forms, which uses a pair of events XXXChanged and XXXChanging , WPF uses Preview versions of events for the same purpose, which can be handled in such a way that the main event is not triggered. (A classic example would be handling mouse or keyboard events that forbid certain keys or combinations of keys.)
And all would be well if the class TextBox along with the event TextChanged would also contain PreviewTextChanged which we could have handled and "interrupted" the user’s input if we think that the text we’re typing is invalid. And since we don’t have one, we have to invent our own lissapet.

Problem Solution

The decision of a problem comes to creation of class TextBoxBehavior containing property IsDoubleProperty after which the user cannot enter into the given text field anything except symbols +, -, . (integer and fractional separator), and also numbers (don’t forget that we need to use settings of the current thread, not hardcoded values).

public class TextBoxBehavior{// Attached property of boolean type, setting of which will lead to// restriction of the user input data.public static readonly DependencyProperty IsDoubleProperty =DependencyProperty.RegisterAttached("IsDouble", typeof (bool), typeof (TextBoxBehavior), new FrameworkPropertyMetadata(false, OnIsDoubleChanged));// This attribute will not allow the use of IsDoublewith any other// UI elements except TextBox or its descendants[AttachedPropertyBrowsableForType(typeof (TextBox))]public static bool GetIsDouble(DependencyObject element){}public static void SetIsDouble(DependencyObject element, bool value){}// Called when TextBoxBehavior.IsDouble="True" in XAMLprivate private void OnIsDoubleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e){// Street magic}}

The main difficulty in the implementation of the handler PreviewTextInput (as well as the event of pasting text from the clipboard) is that the event arguments do not pass the total text value, but only the newly entered part of it. Therefore total text should be formed manually, taking into account the possibility of text selection in TextBoxes, the current cursor position in it and, possibly, the state of Insert button (which we will not analyze):

// Called when TextBoxBehavior.IsDouble="True" is set in XAMLprivate private void OnIsDoubleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e){//because we have limited our attached property to the class of// TextBox or its descendants, the following transformation is safevar textBox = (TextBox) d;// Now we have two important cases to handle:// 1. manual user input// 2. Inserting data from the clipboardtextBox.PreviewTextInput+= PreviewTextInputForDouble;DataObject.AddPastingHandler(textBox, OnPasteForDouble);}

TextBoxDoubleValidator class

The second important point is the implementation of the validation logic for the newly entered text, the responsibility for which is assigned to the method IsValid of a single class TextBoxDoubleValidator
The easiest way to understand how a method should behave IsValid of this class is to write a unit test for it which covers all corner cases (this is exactly one of those cases when parameterized unit tests rule with a great force):
note
This is precisely the case when a unit test is not just a test that verifies that a particular functionality is implemented correctly. This is exactly the case that Kent Beck repeatedly mentioned when describing validation; by reading this test, you can understand what the developer of the validation method was thinking, "reuse" his knowledge, and find errors in his reasoning and thus probably in the implementation code as well. It’s not just a set of tests – it’s an important part of that method’s specification!

private private void PreviewTextInputForDouble(object sender, TextCompositionEventArgs e){// e.Text contains only new text, so without the current// the current state of the TextBox can't be avoidedvar textBox = (TextBox)sender;stringfullText;// If a TextBox contains the selected text, replace it with e.Textif (textBox.SelectionLength > 0){fullText = textBox.Text.Replace(textBox.SelectedText, e.Text);}else{// Otherwise we need to insert new text at the cursor positionfullText = textBox.Text.Insert(textBox.CaretIndex, e.Text);}// Now we validate the resulting textThe bool isTextValid = TextBoxDoubleValidator.IsValid(fullText);// And prevent the TextChangedevent if the text is invalide.Handled = !isTextValid;}

The test method returns true if the parameter text is valid, which means that the corresponding text can be typed into TextBox with the attached IsDoubleproperty. Note a few things : (1) the use of the attribute SetCulture which sets the right locale and (2) on some input values such as "-." which are not valid values for type Double
The explicit locale setting is needed so that tests will not crash at developers with other personal settings, because in the Russian locale, as a separator character ‘, ‘ (comma) is used, and in American – ‘. (dot).Strange text such as "-." is correct because we need the user to end his input if he wants to enter the string "-.1", which is the correct value for Double (Interestingly, on StackOverflow it is very often advised to simply use Double.TryParse. which obviously will not work in some cases).
note
I do not want to overload this article with details of the implementation of the IsValid I just want to point out the use in the body of this method ThreadLocal<T> , which allows you to retrieve and cache DoubleSeparator local for each thread. The full implementation of the method TextBoxDoubleValidator.IsValid can be found here , more information on ThreadLocal<T> can be read from Joe Albahari in the article Working with Flows. Part 3

Alternative solutions

In addition to capturing events PreviewTextInput and pasting text from the clipboard, there are other solutions. For example, I’ve seen an attempt to solve the same problem by hooking the event PreviewKeyDown with filtering out all keys except the numeric keys. However, this solution is more complicated because we still have to deal with the "summed" state of the TextBox, and theoretically, the separator of integer part and fractional part can be not one character, but the whole string ( NumberFormatInfo.NumberDecimalSeparator returns string instead of char ).
There is also an option in the event KeyDown to keep the previous state TextBox.Text and in the event TextChanged to give it back the old value if the new one is not satisfied. But this solution looks unnatural and it’s not going to be easy to implement with the attached properties.

Conclusion

When we last discussed with colleagues the lack of useful and very generic features in WPF, we came to the conclusion that there is an explanation for this, and there is a positive side to it. The explanation boils down to the fact that from law of hole abstractions. can’t get anywhere and WPF being an "abstraction" is very complex, it flows like a sieve. On the bright side, the lack of some useful features makes us think (!) sometimes and not forget that we are programmers, not copy-paste-skillers.
As a reminder, the full implementation of the above classes, examples of their use and unit tests can be found at github

You may also like