Visual studio c# using state machines to restrict input in textbox. Attached properties to restrict text input

Antipyretics for children are prescribed by a pediatrician. But there are emergency situations for fever when the child needs to be given medicine immediately. Then the parents take responsibility and use antipyretic drugs. What is allowed to give to infants? How can you bring down the temperature in older children? What medicines are the safest?

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 alloy wheels to solve some typical problems.

One such task is to restrict user input to certain data. For example, let's say we want one text box to accept only integer values, another to accept a date in a specific format, and a third to accept only floating point numbers. 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 quite easily, and when the same TextBox from DevExpress was available with the built-in ability to restrict 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 come down to one of two options: using a TextBox class inheritor or adding an attached property with the necessary restrictions.

NOTE
If you are not very interested in my reasoning, but you immediately need code examples, then you can either download the entire project
WpfEx from GitHub , or download the main implementation, which is contained in TextBoxBehavior.cs and TextBoxDoubleValidator.cs .

Well, let's get started?

Since inheritance introduces a rather strict restriction, I personally prefer using attached properties in this case, since this mechanism allows you to restrict the application of these properties to controls of a certain type (I do not want this attached property IsDouble could be applied to a TextBlock for which it doesn't make sense).
In addition, it should be noted that when restricting user input, you cannot use any specific integer and fractional separators (such as '.' (dot) or ',' (comma)), as well as the signs '+' and '-', since it all depends on the user's regional settings.
In order to implement the ability to restrict data input, we need to intercept the user input event manually, analyze it and discard these changes if they do not 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 processed in such a way that the main event does not fire. (A classic example would be handling mouse or keyboard events that disable certain keys or their combinations.)

And everything would be fine if the TextBox class, along with the TextChanged event, also contained PreviewTextChanged, which could be processed and "abort" user input if we consider the input text to be incorrect. And since it does not exist, then everyone and everyone has to invent their own lisapet.

The solution of the problem

The solution to the problem is to create a TextBoxBehavior class that contains the attached IsDoubleProperty property, after setting which the user will not be able to enter anything other than +, -, characters into this text field. (separator of integer and fractional parts), as well as numbers (don't forget that we need to use the settings of the current stream, and not the hardcoded values).

Public class TextBoxBehavior ( // Attached property of boolean type, setting which will restrict user input public static readonly DependencyProperty IsDoubleProperty = DependencyProperty.RegisterAttached("IsDouble", typeof (bool), typeof (TextBoxBehavior), new FrameworkPropertyMetadata(false, OnIsDoubleChanged)); // This attribute will not allow IsDouble to be used with any other // UI elements other than TextBox or its descendants public static bool GetIsDouble(DependencyObject element) () public static void SetIsDouble(DependencyObject element, bool value) () / / Called when TextBoxBehavior.IsDouble="True" is set in XAML private static void OnIsDoubleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) ( // Street magic ) )

The main complexity of the handler implementation PreviewTextInput(as well as the event of pasting text from the clipboard) lies in the fact that not the total value of the text is transmitted in the event arguments, 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 the TextBox, the current position of the cursor in it, and, possibly, the state of the Insert button (which we will not analyze):

// Called when TextBoxBehavior.IsDouble="True" is set in XAML private static void OnIsDoubleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) ( // Since we've limited our attached property to only the // TextBox class or its descendants, the following transformation is - safe var textBox = (TextBox) d; // Now we need to handle two important cases: // 1. Manual user input // 2. Pasting data from the clipboard textBox.PreviewTextInput += PreviewTextInputForDouble; DataObject.AddPastingHandler(textBox, OnPasteForDouble ); )

TextBoxDoubleValidator class

The second important point is the implementation of the logic of validation of the newly entered text, the responsibility for which is assigned to the method IsValid separate class TextBoxDoubleValidator.

by the most in a simple way understand how the method should behave IsValid of this class, is to write a unit test for it that will cover all corner cases (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 certain functionality. This is exactly the case that Kent Beck talked about repeatedly when describing accountability; by 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. It's not just a test suite - it's an important part of the specification for this method!

Private static void PreviewTextInputForDouble(object sender, TextCompositionEventArgs e) ( // e.Text contains only new text, so the current // state of the TextBox is indispensable var textBox = (TextBox)sender; string fullText; // If the TextBox contains selected text, then replace it with e.Text if (textBox.SelectionLength > 0) ( fullText = textBox.Text.Replace(textBox.SelectedText, e.Text); ) else ( // Otherwise, we need to insert new text at the cursor position fullText = textBox.Text.Insert(textBox.CaretIndex, e.Text); ) // Now validate the resulting text bool isTextValid = TextBoxDoubleValidator.IsValid(fullText); // And prevent the TextChanged event if the text is not valid e.Handled = !isTextValid; )

The test method returns true if the parameter text is valid, which means that the corresponding text can be typed into text box with attached property IsDouble. Pay attention to a few things: (1) the use of the attribute SetCulture, which sets the desired locale and (2) some input values, such as “-.”, which are not valid values ​​for the type Double.

An explicit locale setting is needed so that tests do not fail for developers with other personal settings, because in the Russian locale, the symbol ',' (comma) is used as a separator, and in the American one - '.' (dot). Strange text like “-.” is correct because we need the user to complete input if they want to enter the string “-.1”, which is the correct value for Double. (Interestingly, StackOverflow is very often advised to just use Double.TryParse to solve this problem, which obviously won't work in some cases).

NOTE
I don't want to litter the article with method implementation details IsValid, I just want to note the use of the ThreadLocal method in the body of this , which allows you to get and cache DoubleSeparator local to each thread. Full implementation of the method TextBoxDoubleValidator.IsValid you can find more information about ThreadLocal You can read Joe Albahari's article Working with Threads. Part 3 .

Alternative Solutions

In addition to capturing events PreviewTextInput 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 event PreviewKeyDown with filtering of all keys except digital. However, this solution is more complicated, because you still have to bother with the “summary” state of the TextBox, and purely theoretically, the separator of the integer and fractional parts can be not one character, but the whole string (NumberFormatInfo.NumberDecimalSeparator returns string, but not char).
There is another option in the event key down save previous state TextBox.Text, and in the event TextChanged 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 boils down to the fact that there is no escape from the law of leaky abstractions, and WPF, being a very complex "abstraction", flows like a sieve. The useful side is that the lack of some useful features makes us sometimes think (!) And do not forget that we are programmers, not copy-paste craftsmen.

Let me remind you that the full implementation of the classes of the above classes, examples of their use and unit tests can be found on github.

Tags: Add tags

In this manual, we will consider entering only numbers from the user. AT Microsoft Visual Studio there is a control " MaskedTextBox”, with its customizable input mask, but we will assume that today we are only interested in« text box».
To implement this task, we will use the event " keypress” that occurs when a key is pressed while the control has focus. Create a project windows form in Microsoft Visual Studio and add a control to the main form text box". Select this component and right-click on it, select from the context menu that appears, the item " PropertieskeypresstextBox1_KeyPress", developments " keypress».
With every event keypress» passed object « KeyPressEventArgs". This object includes the property " keychar” representing the character of the key pressed. For example, when pressing the SHIFT + D keys, this property returns the capital character D and its code 68. There is also a property " handled' which is used to determine if the event has been handled. By setting the value " handled" in " true", the input event will not be dispatched operating system for default processing.
Let's look at a few examples of creating data entry restrictions in a text field.
Example 1:
textBox1_KeyPress".

if ((e.KeyChar<= 47 || e.KeyChar >= 58) && e.KeyChar != 8) e.Handled = true; This example includes compound conditions using logical operators such as &&(and), || (or), ! (not) and checks the decimal code of the entered character, according to two conditions:

  • "e.KeyChar != 8" - If the "Backspace" key was pressed, then allow character deletion.
  • "(e.KeyChar<= 47 || e.KeyChar >= 58 )" - If the entered character has an ASCII code less than or equal to 47 and greater than or equal to 58, then input is prohibited.
Below is ASCII code table, in which characters are highlighted in red, the entry of which is prohibited and in green, the entry of which is allowed.

Example 2:
Add the following listing to the " textBox1_KeyPress».
if (!Char.IsDigit(e.KeyChar) && e.KeyChar != Convert.ToChar(8)) ( e.Handled = true; ) This example also uses logical operators such as &&(and), ! (not) and the decimal code of the entered character is checked, according to two conditions. The method is used to check Char.IsDigit", which returns " true"" if the entered Unicode character is a decimal digit and " false", if not. There is also a check for pressing the key " backspace».
Example 3:
if (!(Char.IsDigit(e.KeyChar)) && !((e.KeyChar == ".") && (((TextBox)sender).Text.IndexOf(".") == -1) && ( ((TextBox)sender).Text.Length != 0))) ( if (e.KeyChar != (char)Keys.Back) ( e.Handled = true; ) ) In this example, as well as in the previous one, the code of the input character is checked using the " Char.IsDigit”, but there is an additional condition that allows the input of one decimal separator. For this, the method is used Text.IndexOf". This method searches for a period character by words using the current culture. The search starts at the first character position in this instance (the current line) and continues to the last character position. If the given symbol was not found, then the method returns the value " -1 ". If the character was found, the method returns a decimal integer indicating the position of the given character and prohibits the processing of the character input.

Decimal separator- the sign used to separate the integer and fractional parts of a real number in the form of a decimal fraction in the decimal system. For fractions in other number systems, the term separator of the integer and fractional parts of a number can be used. Sometimes the terms decimal point and decimal point may also be used. (http://ru.wikipedia.org).
For more information on the method Text.IndexOf» you can get at: http://msdn.microsoft.com .

Example 4:
Add the following listing to the " textBox1_KeyPress».

if (!System.Text.RegularExpressions.Regex.Match(e.KeyChar.ToString(), @"").Success) ( e.Handled = true; ) To check the input characters, in this example, the method " Regex Matches". This method searches the input string for all occurrences of the given regular expression. A regular expression is a character pattern that represents a sequence of characters of arbitrary length. You can specify any regular expression, for example, only allow characters " ' or allow input of decimal separator ' ,|.| ».
Example 5:
Add the following listing to the " textBox1_KeyPress».

if (!System.Text.RegularExpressions.Regex.IsMatch(e.KeyChar.ToString(),@"\d+")) e.Handled = true; This example also uses the given regular expression to implement the validation of the entered character. The regular expression uses the " + ”, which means that one or more characters of the same type are to be found. For example, \d+ matches the numbers "1", "11", "1234", etc. If you are not sure what numbers will follow the number? You can specify that either any number of digits or no digits are allowed. For this, the symbol " * ».
Example 6:
To implement this example, you need to use the event " key down", the control element " textBox1". Go to the main form constructor and select the component " textBox1". Make a right-click on this control, select from the context menu that appears, the item " Properties". In the window that opens, go to component events (the lightning bolt icon at the top of the window) and find the event " key down”, double click on this event. After completing all the steps, you will go to the automatically generated method " textBox1_KeyDown", developments " key down". This event occurs every time a key is pressed while the control has focus.

Keyboard input events

When the user presses a key, a series of events are fired. The table lists these events in the order in which they occur:

Chronology of occurrence of events
Name Routing type Description
PreviewKeyDown tunneling Occurs when a key is pressed.
key down bubble spread Same
PreviewTextInput tunneling Occurs when a keystroke is completed and the element receives text input. This event does not occur for keys that do not "print" characters (for example, it does not occur when the keys are pressed , , , cursor keys, function keys, etc.)
TextInput bubble spread Same
PreviewKeyUp tunneling Occurs when a key is released
key up bubble spread Same

Handling keyboard events is by no means as easy as it might seem. Some controls may block some of these events in order to perform their own keyboard handling. The most notable example is the TextBox element, which blocks the TextInput event as well as the KeyDown event for pressing certain keys, such as cursor keys. In such cases, you can usually still use tunneled events (PreviewTextInput and PreviewKeyDown).

The TextBox element adds one new event, TextChanged. This event fires immediately after a keystroke changes the text in the text field. However, at this point, the new text is already visible in the text field, so it's too late to undo the unwanted keystroke.

Keystroke Handling

Understanding how keyboard events work and are used is best done with an example. The following is an example program that monitors and logs all possible keystrokes while a text field has focus. In this case, the result of typing a capital S is shown.

This example demonstrates one important point. The PreviewKeyDown and KeyDown events are raised each time a key is pressed. However, the TextInput event only fires when a character has been "input" into the element. In fact, this may mean pressing many keys. In the example, you need to press two keys to get capital letter S: key first , and then the key . This results in two KeyDown and KeyUp events, but only one TextInput event.

Ignore repeated character presses

Public partial class MainWindow: Window ( public MainWindow() ( InitializeComponent(); ) private void Clear_Click(object sender, RoutedEventArgs e) ( lbxEvents.Items.Clear(); txtContent.Clear(); i = 0; ) protected int i = 0; private void KeyEvents(object sender, KeyEventArgs e) ( if ((bool)chkIgnoreRepeat.IsChecked && e.IsRepeat) return; i++; string s = "Event" + i + ": " + e.RoutedEvent + " Key : " + e.Key; lbxEvents.Items.Add(s); ) private void TextInputEvent(object sender, TextCompositionEventArgs e) ( i++; string s = "Event" + i + ": " + e.RoutedEvent + " Key: " + e.Text; lbxEvents.Items.Add(s); ) )

The PreviewKeyDown, KeyDown, PreviewKey, and KeyUp events each pass the same information to the KeyEventArgs object. The most important part is the Key property, which returns a value from the System.Windows.Input.Key enumeration and identifies the pressed or released key.

The Key value does not take into account the state of any other key - for example, whether the key was pressed at the time of pressing ; either way you will get the same Key (Key.S) value.

There is one difficulty here. Depending on how your Windows keyboard is set up, holding down a key causes the key to be pressed again after a short period of time. For example, pressing a key will cause a series of S characters to be entered into the text field. Similarly, pressing the key results in repeated clicks and a series of KeyDown events. In a real example, when pressing a combination the text field will generate a series of KeyDown events for the key , then the KeyDown event for the key , the TextInput event (or the TextChanged event in case text field) and then the KeyUp event for the keys and . If you want to ignore keystrokes , you can check if the press is the result of a key press using the KeyEventArgs.IsRepeat property.

The PreviewKeyDown, KeyDown, PreviewKey, and KeyUp events are more suitable for writing low-level code for handling keyboard input (which is rarely needed, except in user controls) and handling special key presses (such as function keys).

The KeyDown event is followed by the PreviewTextInput event. (The TextInput event is not raised because the TextBox is blocking it.) At this point, the text is not yet displayed in the control.

TextInput event provides object code TextCompositionEventArgs. This object contains a Text property that gives the rendered text ready to be passed to the control.

Ideally, the PreviewTextInput event could be used to perform validation on controls like the TextBox. For example, if you're creating a text field that only accepts numbers, you can check to see if the current keystroke has entered a letter, and set the Handled flag if it has. Alas, the PreviewTextIlnput event is not generated for some keys that need to be handled. For example, pressing the spacebar in a text field skips the PreviewTextInput event altogether. This means that you will also need to handle the PreviewKeyDown event.

Unfortunately, it is difficult to implement robust data validation logic in the PreviewKeyDown event handler, as only the Key value is available, and this is too low-level piece of information. For example, the Key enumeration distinguishes between keys on the numeric keypad (a block for entering only numbers) and a regular keyboard. This means that depending on where the 9 key is pressed, you will get either Key.D9 or Key.NumPad9. Checking all valid values ​​is at least very tedious.

One way out is to use the class KeyConverter, which allows you to convert the Key value to a more useful string. For example, a function call KeyConverter.ConvertToString() with any of the Key.D9 and Key.NumPad9 values, returns the string result "9". Calling the Key.ToString() conversion yields a less useful enum name (either "D9" or "NumPad9"):

KeyConverter converter = new KeyConverter(); string key = converter.ConvertToString(e.Key);

However, using KeyConverter is also not very convenient, since you have to handle long lines (for example, "Backspace") for those keystrokes that do not result in text input.

The most appropriate option is to handle the PreviewTextInput event (where most of the validation is done) in conjunction with the PreviewKeyDown event for key presses that do not generate a PreviewTextInput event on the text field (such as the space bar).



Support the project - share the link, thanks!
Read also
cockfight game rules cockfight game rules Mod for minecraft 1.7 10 watch recipes.  Recipes for crafting items in Minecraft.  Weapons in Minecraft Mod for minecraft 1.7 10 watch recipes. Recipes for crafting items in Minecraft. Weapons in Minecraft Shilling and sterling - the origin of words Shilling and sterling - the origin of words