Visual studio c# использование конечных автоматов для ограничения ввода в textbox. Attached свойства для ограничения текстового ввода

Жаропонижающие средства для детей назначаются педиатром. Но бывают ситуации неотложной помощи при лихорадке, когда ребенку нужно дать лекарство немедленно. Тогда родители берут на себя ответственность и применяют жаропонижающие препараты. Что разрешено давать детям грудного возраста? Чем можно сбить температуру у детей постарше? Какие лекарства самые безопасные?

WPF – это уже далеко не новая технология на рынке, но относительно новая для меня. И, как это часто бывает при изучении чего-то нового, появляется желание/необходимость в изобретении велосипедов с квадратными колесами и литыми дисками для решения некоторых типовых задач.

Одной из таких задач является ограничение ввода пользователем определенных данных. Например, мы хотим, чтобы в некоторое текстовое поле можно было вводить только целочисленные значения, а в другое – дату в определенном формате, а в третье – только числа с плавающей запятой. Конечно, окончательная валидация подобных значений все равно будет происходить во вью-моделях, но подобные ограничения на ввод делают пользовательский интерфейс более дружественным.

В Windows Forms эта задача решалась довольно легко, а когда в распоряжении был тот же TextBox от DevExpress со встроенной возможностью ограничения ввода с помощью регулярных выражений, то все было вообще просто. Примеров решения этой же задачи в WPF довольно много , большинство из которых сводится к одному из двух вариантов: использование наследника класса TextBox или добавление attached property с нужными ограничениями.

ПРИМЕЧАНИЕ
Если вам не очень интересны мои рассуждения, а нужны сразу же примеры кода, то вы можете либо скачать весь проект
WpfEx с GitHub , либо же скачать основную реализацию, которая содержится в TextBoxBehavior.cs и TextBoxDoubleValidator.cs .

Ну что, приступим?

Поскольку наследование вводит довольно жесткое ограничение, то лично мне в этом случае больше нравится использование attached свойств, благо этот механизм позволяет ограничить применение этих свойств к элементам управления определенного типа (я не хочу, чтобы это attached свойство IsDouble можно было применить к TextBlock-у для которого это не имеет смысла).
Кроме того нужно учесть, что при ограничении ввода пользователя нельзя использовать какие-то конкретные разделители целой и дробной части (такие как ‘.’ (точка) или ‘,’ (запятая)), а также знаки ‘+’ и ‘-‘, поскольку все это зависит от региональных настроек пользователя.
Чтобы реализовать возможность ограничения ввода данных, нам нужно перехватить событие ввода данных пользователем внучную, проанализировать его и отменить эти изменения, если они нам не подходят. В отличие от Windows Forms, в котором принято использование пары событий XXXChanged и XXXChanging , в WPF для этих же целей используются Preview версии событий, которые могут быть обработаны таким образом, чтобы основное событие не срабатывало. (Классическим примером может служить обработка событий от мыши или клавиатуры, запрещающие некоторые клавиши или их комбинации).

И все было бы хорошо, если бы класс TextBox вместе с событием TextChanged содержал бы еще и PreviewTextChanged , которое можно было бы обработать и «прервать» ввод данных пользователем, если мы считаем вводимый текст некорректным. А поскольку его нет, то и приходится всем и каждому свой лисапет изобретать.

Решение задачи

Решение задачи сводится к созданию класса TextBoxBehavior, содержащего attached свойство IsDoubleProperty, после установки которого пользователь не сможет вводить в данное текстовое поле ничего кроме символов +, -,. (разделителя целой и дробной части), а также цифр (не забываем, что нам нужно использовать настройки текущего потока, а не захардкодженные значения).

Public class TextBoxBehavior { // Attached свойство булевого типа, установка которого приведет к // ограничению вводимых пользователем данных public static readonly DependencyProperty IsDoubleProperty = DependencyProperty.RegisterAttached("IsDouble", typeof (bool), typeof (TextBoxBehavior), new FrameworkPropertyMetadata(false, OnIsDoubleChanged)); // Данный атрибут не позволит использовать IsDouble ни с какими другими // UI элементами, кроме 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) { // Уличная магия } }

Основная сложность реализации обработчика PreviewTextInput (как и события вставки текста из буфера обмена) заключается в том, что в аргументах события передается не суммарное значение текста, а лишь вновь введенная его часть. Поэтому суммарный текст нужно формировать вручную, учитывая при этом возможность выделения текста в TextBox-е, текущее положение курсора в нем и, возможно, состояние кнопки Insert (которое мы анализировать не будем):

// Вызовется, при установке 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); }

Класс TextBoxDoubleValidator

Вторым важным моментом является реализация логики валидации вновь введенного текста, ответственность за которую отведена методу IsValid отдельного класса TextBoxDoubleValidator .

Самым простым способом понять, как должен вести себя метод IsValid этого класса, это написать для него юнит-тест, который покроет все corner case-ы (это как раз один из тех случаев, когда параметризованные юнит-тесты рулят со страшной силой):

ПРИМЕЧАНИЕ
Это как раз тот случай, когда юнит тест – это не просто тест, проверяющий корректность реализации определенной функциональности. Это как раз тот случай, о котором неоднократно говорил Кент Бек, описывая accountability; прочитав этот тест можно понять, о чем думал разработчик метода валидации, «повторно использовать» его знания и найти ошибки в его рассуждениях и, тем самым, вероятно и в коде реализации. Это не просто набор тестов – это важная часть спецификации этого метода!

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; }

Тестовый метод возвращает true , если параметр text является валидным, и это означает, что соответствующий текст можно будет вбить в TextBox с attached свойством IsDouble. Обратите внимание на несколько моментов: (1) использование атрибута SetCulture , который устанавливает нужную локаль и (2) на некоторые входные значения, такие значения как “-.”, которые не являются корректными значениями для типа Double .

Явная установка локали нужна для того, чтобы тесты не падали у разработчиков с другими персональными настройками, ведь в русской локали, в качестве разделителя используется символ ‘,’ (запятая), а в американской – ‘.’ (точка). Такой странный текст, как “-.” является корректным, поскольку мы должны пользователю завершить ввод, если он хочет ввести строку “-.1”, которая является корректным значением для Double . (Интересно, что на StackOverflow для решения этой задачи очень часто советуют просто использовать Double.TryParse , который явно не будет работать в некоторых случаях).

ПРИМЕЧАНИЕ
Я не хочу захламлять статью деталями реализации метода IsValid , хочу лишь отметить использование в теле этого метода ThreadLocal , который позволяет получить и закэшировать DoubleSeparator локальный для каждого потока. Полную реализацию метода TextBoxDoubleValidator.IsValid можно найти , более подробную информацию об ThreadLocal можно почитать у Джо Албахари в статье Работа с потоками. Часть 3 .

Альтернативные решения

Помимо перехвата событий PreviewTextInput и вставки текста из буфера обмена существуют и другие решения. Так, например, я встречал попытку решить эту же задачу путем перехвата события PreviewKeyDown с фильтрацией всех клавиш кроме цифровых. Однако это решение сложнее, поскольку все равно придется заморачиваться с «суммарным» состоянием TextBox-а, да и чисто теоретически, разделителем целой и дробной части может быть не один символ, а целая строка (NumberFormatInfo.NumberDecimalSeparator возвращает string , а не char ).
Есть еще вариант в событии KeyDown сохранить предыдущее состояние TextBox.Text , а в событии TextChanged вернуть ему старое значение, если новое значение не устраивает. Но это решение выглядит неестественным, да и реализовать его с помощью attached свойств будет не так-то просто.

Заключение

Когда мы в последний раз обсуждали с коллегами отсутствие в WPF полезных и весьма типовых возможностей, то мы пришли к выводу, что этому есть объяснение, и в этом есть и положительная сторона. Объяснение сводится к тому, что от закона дырявых абстракций никуда не деться и WPF будучи «абстракцией» весьма сложной, течет как решето. Полезная же сторона заключается в том, что отсутствие некоторых полезных возможностей заставляет нас иногда думать (!) и не забывать о том, что мы программисты, а не умельцы копи-пасты.

Напомню, что полную реализацию классов приведенных классов, примеры их использования и юнит-тесты можно найти на github .

Метки: Добавить метки

В данной инструкции рассмотрим ввод только чисел от пользователя. В Microsoft Visual Studio присутствует элемент управления «MaskedTextBox », с его настраиваемой маской ввода, но будем считать, что сегодня нас интересует только « TextBox ».
Для реализации данной задачи воспользуемся событием «KeyPress », происходящее при нажатии клавиши, если элемент управления имеет фокус. Создайте проект Windows Form в Microsoft Visual Studio и добавьте на главную форму элемент управления «TextBox ». Выберете данный компонент и сделайте клик правой клавишей мыши по нему, выберете из появившегося контекстного меню, пункт «Свойства KeyPress textBox1_KeyPress », события «KeyPress ».
С каждым событием «KeyPress » передается объект «KeyPressEventArgs ». Данный объект включает свойство «KeyChar », предоставляющее символ нажатой клавиши. Например, при нажатии клавиш SHIFT + D данное свойство возвращает прописной знак D и его код 68. Так же присутствует свойство «Handled », которое используется для определения того, было ли событие обработано. Установив значение «Handled » в «true », событие ввода не будет передано операционной системе для обработки по умолчанию.
Рассмотрим несколько примеров создания ограничения вводимых данных в текстовое поле.
Пример 1:
textBox1_KeyPress».

if ((e.KeyChar <= 47 || e.KeyChar >= 58) && e.KeyChar != 8) e.Handled = true; Данный пример включает в себя составные условия, используя логические операторы такие как &&(и) , || (или) , ! (not) и выполняет проверку десятичного кода введенного символа, по двум условиям:

  • «e.KeyChar != 8» – Если была нажата клавиша «Backspace», то разрешить удаление символа.
  • «(e.KeyChar <= 47 || e.KeyChar >= 58 )» – Если введенный символ имеет ASCII код меньше или равен 47 и больше или равен 58, то ввод запрещен.
Ниже представлена таблица кодов ASCII символов , в которой красным цветом выделены символы, ввод которых запрещен и зеленым цветом, ввод которых разрешен.

Пример 2:
Добавьте приведенный ниже листинг в метод «textBox1_KeyPress ».
if (!Char.IsDigit(e.KeyChar) && e.KeyChar != Convert.ToChar(8)) { e.Handled = true; } В данном примере так же используются логически операторы, такие как &&(и) , ! (not) и выполняется проверка десятичного кода введенного символа, по двум условиям. Для проверки используется метод «Char.IsDigit », который возвращает «true », если введенный символ Юникода является десятичной цифрой и «false », если нет. Так же присутствует проверка на нажатие клавиши «Backspace ».
Пример 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; } } В данном примере так и в предыдущем реализована проверка кода вводимого символа с использованием метода «Char.IsDigit », но присутствует дополнительное условие, разрешающее ввод одного десятичного разделителя. Для этого используется метод «Text.IndexOf ». Данный метод выполняет поиск знака точки, по словам используя текущий язык и региональные параметры. Поиск начинается с первой позиции знака в данном экземпляре (текущей строке) и продолжается до последней позиции знака. Если данный символ не был найден, то метод возвращает значение «-1 ». В случае если символ был найден то метод возвращает целое десятичное число, указывающее в какой позиции находится данный символ и запрещает обработку ввода символа.

Десятичный разделитель — знак, используемый для разделения целой и дробной частей вещественного числа в форме десятичной дроби в системе десятичного исчисления. Для дробей в иных системах счисления может использоваться термин разделитель целой и дробной частей числа. Иногда также могут употребляться термины десятичная точка и десятичная запятая. (http://ru.wikipedia.org).
Дополнительную информацию по методу «Text.IndexOf » вы можете получить по адресу: http://msdn.microsoft.com .

Пример 4:
Добавьте приведенный ниже листинг в метод «textBox1_KeyPress ».

if (!System.Text.RegularExpressions.Regex.Match(e.KeyChar.ToString(), @"").Success) { e.Handled = true; } Для проверки вводимых символов, в данном примере используется метод «Regex.Matches ». Данный метод ищет во входящей строке все вхождения заданного регулярного выражения. Регулярное выражение представляет собой состоящий из символов шаблон, обозначающий последовательность символов произвольной длины. Вы можете указать любое регулярное выражение, например, разрешить ввод только символов « » или разрешить ввод десятичного разделителя «,|.| ».
Пример 5:
Добавьте приведенный ниже листинг в метод «textBox1_KeyPress ».

if (!System.Text.RegularExpressions.Regex.IsMatch(e.KeyChar.ToString(),@"\d+")) e.Handled = true; Данный пример так же использует заданное регулярное выражение для реализации проверки введенного символа. В регулярном выражении используется оператор «+ », который означает, что требуется найти один или несколько символов одного и того же типа. Например, \d+ соответствует числам "1", "11", "1234", и т.д. Если вы не уверены, что за числом будут следовать какие-нибудь цифры? Вы можете указать, что допускается как любое число цифр, так и отсутствие цифр. Для этого служит символ «* ».
Пример 6:
Для реализации данного примера вам необходимо воспользоваться событием «KeyDown », элемента управления «textBox1 ». Перейдите в конструктор главной формы и выберете компонент «textBox1 ». Сделайте клик правой клавишей мыши по данному элементу управления, выберете из появившегося контекстного меню, пункт «Свойства ». В открывшемся окне перейдите в события компонента (значок молнии в верхней части окна) и найдите событие «KeyDown », сделайте двойной клик по данному событию. После выполнения всех действий, вы перейдите в автоматически созданный метод «textBox1_KeyDown », события «KeyDown ». Данное событие происходит при каждом нажатии клавиши, если элемент управления имеет фокус.

События ввода с клавиатуры

Когда пользователь нажимает клавишу, возникает целая серия событий. В таблице эти события перечислены в порядке их возникновения:

Хронология возникновения событий
Имя Тип маршрутизации Описание
PreviewKeyDown Туннелирование Возникает при нажатии клавиши.
KeyDown Пузырьковое распространение То же
PreviewTextInput Туннелирование Возникает, когда нажатие клавиши завершено, и элемент получает текстовый ввод. Это событие не возникает для тех клавиш, которые не "печатают" символы (например, оно не возникает при нажатии клавиш , , , клавиш управления курсором, функциональных клавиш и т.д.)
TextInput Пузырьковое распространение То же
PreviewKeyUp Туннелирование Возникает при отпускании клавиши
KeyUp Пузырьковое распространение То же

Обработка событий клавиатуры отнюдь не так легка, как это может показаться. Некоторые элементы управления могут блокировать часть этих событий, чтобы выполнять свою собственную обработку клавиатуры. Наиболее ярким примером является элемент TextBox, который блокирует событие TextInput, а также событие KeyDown для нажатия некоторых клавиш, таких как клавиши управления курсором. В подобных случаях обычно все-таки можно использовать туннелируемые события (PreviewTextlnput и PreviewKeyDown).

Элемент TextBox добавляет одно новое событие - TextChanged. Это событие возникает сразу после того, как нажатие клавиши приводит к изменению текста в текстовом поле. Однако в этот момент новый текст уже видим в текстовом поле, потому отменять нежелательное нажатие клавиши уже поздно.

Обработка нажатия клавиши

Понять, как работают и используются события клавиатуры, лучше всего на примере. Ниже представлен пример программы, которая отслеживает и протоколирует все возможные нажатия клавиш, когда в фокусе находится текстовое поле. В данном случае показан результат ввода заглавной буквы S.

Этот пример демонстрирует один важный момент. События PreviewKeyDown и KeyDown возникают при каждом нажатии клавиши. Однако событие TextInput возникает только тогда, когда в элементе был "введен" символ. На самом деле это может означать нажатие многих клавиш. В примере, нужно нажать две клавиши, чтобы получить заглавную букву S: сначала клавишу , а затем клавишу . В результате получаются по два события KeyDown и KeyUp, но только одно событие TextInput.

Игнорировать повторное нажатие символов

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 + " Клавиша: " + e.Key; lbxEvents.Items.Add(s); } private void TextInputEvent(object sender, TextCompositionEventArgs e) { i++; string s = "Event" + i + ": " + e.RoutedEvent + " Клавиша: " + e.Text; lbxEvents.Items.Add(s); } }

Каждое из событий PreviewKeyDown, KeyDown, PreviewKey и KeyUp передает объекту KeyEventArgs одну и ту же информацию. Самой важной ее частью является свойство Key, которое возвращает значение из перечисления System.Windows.Input.Key и идентифицирует нажатую или отпущенную клавишу.

Значение Key не учитывает состояние любой другой клавиши - например, была ли прижата клавиша в момент нажатия ; в любом случае вы получите одно и то же значение Key (Key.S).

Здесь присутствует одна сложность. В зависимости от настройки клавиатуры в Windows, удержание клавиши в прижатом состоянии приводит к повторам нажатия после короткого промежутка времени. Например, прижатие клавиши приведет к вводу в текстовое поле целой серии символов S. Точно так же прижатие клавиши приводит к повторам нажатия и возникновению серии событий KeyDown. В реальном примере при нажатии комбинации текстовое поле сгенерирует серию событий KeyDown для клавиши , потом событие KeyDown для клавиши , событие TextInput (или событие TextChanged в случае текстового поля), а затем событие KeyUp для клавиш и . Если нужно игнорировать повторы нажатия клавиши , то можно проверить, является ли нажатие результатом прижатия клавиши, с помощью свойства KeyEventArgs.IsRepeat.

События PreviewKeyDown, KeyDown, PreviewKey и KeyUp больше подходят для написания низкоуровневого кода обработки ввода с клавиатуры (что редко бывает нужно - разве что в пользовательских элементах управления) и обработки нажатий специальных клавиш (например, функциональных).

За событием KeyDown следует событие PreviewTextInput. (Событие TextInput не возникает, поскольку элемент TextBox блокирует его.) В этот момент текст еще не отображается в элементе управления.

Событие TextInput обеспечивает код объекта TextCompositionEventArgs . Этот объект содержит свойство Text, которое дает обработанный текст, подготовленный к передаче элементу управления.

В идеале событие PreviewTextInput можно было бы использовать для выполнения проверки в элементах управления наподобие TextBox. Например, если вы создаете текстовое поле для ввода только чисел, можно проверить, не была ли введена при текущем нажатии клавиши буква, и установить флаг Handled, если это так. Увы - событие PreviewTextIlnput не генерируется для некоторых клавиш, которые бывает нужно обрабатывать. Например, при нажатии клавиши пробела в текстовом поле событие PreviewTextInput вообще пропускается. Это означает, что придется обрабатывать также событие PreviewKeyDown.

К сожалению, трудно реализовать надежную логику проверки данных в обработчике события PreviewKeyDown, т.к. в наличии имеется только значение Key, а это слишком низкоуровневый фрагмент информации. Например, в перечислении Key различаются клавиши цифровой клавиатуры (блок, предназначенный для ввода только цифр) и обычной клавиатуры. Это означает, что в зависимости от того, где нажата клавиша с цифрой 9, вы получите или значение Key.D9, или значение Key.NumPad9. Проверка всех допустимых значений как минимум очень утомительна.

Одним из выходов является использование класса KeyConverter , который позволяет преобразовать значение Key в более полезную строку. Например, вызов функции KeyConverter.ConvertToString() с любым из значений Key.D9 и Key.NumPad9 возвращает строковый результат "9". Вызов преобразования Key.ToString() дает менее полезное имя перечисления (либо "D9", либо "NumPad9"):

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

Однако использовать KeyConverter тоже не очень удобно, поскольку приходится обрабатывать длинные строки (например, "Backspace") для тех нажатий клавиш, которые не приводят к вводу текста.

Наиболее подходящим вариантом является обработка события PreviewTextlnput (где выполняется большая часть проверки) в сочетании с событием PreviewKeyDown для нажатий тех клавиш, которые не генерируют событие PreviewTextInput в текстовом поле (например, пробела).



Поддержите проект — поделитесь ссылкой, спасибо!
Читайте также
Игра петушиные бои правила Игра петушиные бои правила Мод для майнкрафт 1.7 10 смотреть рецепты. Рецепты крафтинга предметов в Minecraft. Оружие в Minecraft Мод для майнкрафт 1.7 10 смотреть рецепты. Рецепты крафтинга предметов в Minecraft. Оружие в Minecraft Шиллинг и стерлинг - происхождение слов Шиллинг и стерлинг - происхождение слов