WPF is no longer a new technology on the market, but relatively new to me. And, as often happens when learning something new, there is a desire / need to invent bicycles with square wheels and alloy wheels to solve some typical tasks.
One such task is to limit user input to certain data. For example, we want to allow only integer values ​​to be entered in some text field, and a date in a specific format to another, and only floating-point numbers to the third. Of course, the final validation of such values ​​will still occur in the view models, but such input restrictions make the user interface more friendly.
In Windows Forms, this task was solved fairly easily, and when the same TextBox from DevExpress was available with the built-in ability to limit input using regular expressions, then everything was generally simple. There are
quite a few examples of solving the same problem in WPF, most of which boil down to one of two options: using a
TextBox class heir or adding
attached property with necessary restrictions.
NOTE
If you are not very interested in my reasoning, and you need code samples right away, then you can either download the entire WpfEx project from GitHub , or download the main implementation contained in TextBoxBehavior.cs and TextBoxDoubleValidator.cs .')
Well, let's get started?
Since inheritance introduces a rather rigid restriction, I personally prefer using attached properties in this case, since this mechanism allows you to limit the application of these properties to controls of a certain type (I don’t want this attached property
IsDouble to be applied to TextBlock for which it does not make sense).
In addition, you should take into account that when restricting user input, you cannot use any specific separators for the integer and fractional parts (such as '.' (Dot) or ',' (comma)), as well as the '+' and '-', since it all depends on the user's regional settings.
To realize the possibility of restricting data entry, we need to intercept the data entry event by the user as a student, analyze it and cancel these changes if they do not suit us. Unlike Windows Forms, in which the use of a pair of events
XXXChanged and
XXXChanging is taken , WPF uses for these purposes Preview versions of events that can be processed in such a way that the main event does not work. (A classic example is the handling of mouse or keyboard events that prohibit certain keys or their combinations).
And everything would be nice if the
TextBox class together with the
TextChanged event would also
contain a PreviewTextChanged , which could be processed and “interrupted” by the user if we consider the text to be entered incorrect. And since it does not exist, it is necessary for everyone to invent their foolish fools.
The solution of the problem
The solution of the problem is reduced to the creation of the TextBoxBehavior class containing the attached property IsDoubleProperty, after setting which the user cannot enter anything except the symbols +, -, in this text field. (separator of the whole and fractional parts), as well as numbers (do not forget that we need to use the settings of the current stream, and not the hard-coded values).
public class TextBoxBehavior { // Attached , // public static readonly DependencyProperty IsDoubleProperty = DependencyProperty.RegisterAttached( "IsDouble", typeof (bool), typeof (TextBoxBehavior), new FrameworkPropertyMetadata(false, OnIsDoubleChanged)); // IsDouble // UI , TextBox [AttachedPropertyBrowsableForType(typeof (TextBox))] public static bool GetIsDouble(DependencyObject element) {} public static void SetIsDouble(DependencyObject element, bool value) {} // , TextBoxBehavior.IsDouble="True" XAML- private static void OnIsDoubleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { // } }
The main difficulty in implementing the
PreviewTextInput handler (as well as the text paste events from the clipboard) is that the event arguments are not transmitted the total value of the text, but only the newly entered part of it. Therefore, the summary text must be formed manually, taking into account the possibility of selecting text in a TextBox, the current position of the cursor in it and, possibly, the state of the Insert button (which we will not analyze):
// , TextBoxBehavior.IsDouble="True" XAML- private static void OnIsDoubleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { // attached // TextBox , - var textBox = (TextBox) d; // : // 1. // 2. textBox.PreviewTextInput += PreviewTextInputForDouble; DataObject.AddPastingHandler(textBox, OnPasteForDouble); }
Class TextBoxDoubleValidator
The second important point is the implementation of the validation logic of the newly entered text, responsibility for which is assigned to the
IsValid method of a separate
TextBoxDoubleValidator class.
The easiest way to understand how the
IsValid method of this class should behave is to write a unit test for it that covers all corner case (this is just one of those cases when parameterized unit tests rule with terrible force):
NOTE
This is exactly the case when a unit test is not just a test that checks the correctness of the implementation of a certain functionality. This is exactly the case that Kent Beck repeatedly referred to, describing accountability; After reading this test, you can understand what the developer of the validation method was thinking about, “reuse” his knowledge and find errors in his reasoning and, thus, probably in the implementation code. This is not just a test suite - this is an important part of the specification of this method! private static void PreviewTextInputForDouble(object sender, TextCompositionEventArgs e) { // e.Text , // TextBox- var textBox = (TextBox)sender; string fullText; // TextBox , e.Text if (textBox.SelectionLength > 0) { fullText = textBox.Text.Replace(textBox.SelectedText, e.Text); } else { // fullText = textBox.Text.Insert(textBox.CaretIndex, e.Text); } // bool isTextValid = TextBoxDoubleValidator.IsValid(fullText); // TextChanged e.Handled = !isTextValid; }
The test method returns
true if the
text parameter is valid, and this means that the corresponding text can be inserted into the
TextBox with the IsDouble property attached. Pay attention to a few points: (1) use the
SetCulture attribute, which sets the desired locale and (2) on some input values, such as “-.”, Which are not valid values ​​for the
Double type.
Explicit setting of the locale is needed so that tests do not fall off developers with other personal settings, because in the Russian locale, the symbol ',' (comma) is used as a separator, and in the US -. (point). Strange text such as “-.” Is correct, since we must complete the input if the user wants to enter the string “-.1”, which is the correct value for
Double . (It is interesting that on StackOverflow for solving this task it is often advised to simply use
Double.TryParse , which obviously will not work in some cases).
NOTE
I do not want to clutter up the article with details of the implementation of the IsValid method, I just want to note the use of ThreadLocal <T> in the body of this method, which allows you to get and DoubleSeparator local for each stream. The full implementation of the TextBoxDoubleValidator.IsValid method can be found here , more information about ThreadLocal <T> can be found in Joe Albahari's article Working with Threads. Part 3Alternative solutions
In addition to intercepting the
PreviewTextInput event and pasting text from the clipboard, there are other solutions. So, for example, I met an attempt to solve the same problem by intercepting the
PreviewKeyDown event with filtering all keys except the digital ones. However, this solution is more complicated, since you still have to bother with the “total” state of the TextBox, and theoretically, the integer and fractional part separator can be not one character, but an entire string (
NumberFormatInfo.NumberDecimalSeparator returns
string , not
char ).
There is another option in the
KeyDown event to keep the previous state of
TextBox.Text , and in the
TextChanged event
, return the old value to it if the new value does not suit. But this solution looks unnatural, and it will not be so easy to implement it using attached properties.
Conclusion
When we last discussed with colleagues the lack of useful and very typical features in WPF, we came to the conclusion that there is an explanation for this, and there is a positive side to this. The explanation comes down to the fact that there is no way out of the
law of leaky abstractions, and WPF, being an “abstraction” rather complicated, flows like a sieve. The useful side is that the lack of some useful features sometimes makes us think (!) And not forget that we are programmers, not masters of paste and paste.
I recall that the full implementation of the classes of classes, examples of their use and unit tests can be found on
github .