2012-02-03 2 views
3

Я знаю, что это давний вопрос со многими ответами, но я не нашел хороших, надежных ответов.Numeric TextBox - используя Double.TryParse

Требование - это текстовое поле, которое всегда будет содержать строку, в которой Double.TryParse вернет значение true.

Большинство реализаций, которые я видел, не защищают от ввода, например: «10.45.8». Это проблема.

Предпочтительный способ сделать это полностью связан с событиями, такими как TextInput и KeyDown (для пробелов). Проблема заключается в том, что довольно сложно получить строку, представляющую новый текст до его изменения (или старый текст после его изменения). Проблема с TextChanged заключается в том, что он не предоставляет способ получить старый текст.

Если бы вы могли каким-то образом получить новый текст до его изменения, это было бы наиболее полезно, поскольку вы могли бы проверить его на Double.TryParse. Однако может быть и лучшее решение.

Каков наилучший способ для этого?

Лучший ответ на этот вопрос - тот, который имеет несколько подходов и сравнивает их.

+0

** всегда ** сильное слово –

+0

@jberger: Что заставляет вас говорить это? Разве это не достаточно простое требование? –

+0

Что должно произойти, когда пользователь вводит недопустимый символ, а затем оставляет текстовое поле? –

ответ

3

подход 1

Используйте комбинацию TextChanged и KeyDown событий для TextBox. В поле KeyDown вы можете сохранить текущий текст в текстовом поле, а затем сделать свой Double.TryParse в событии TextChanged. Если введенный текст недействителен, вы вернетесь к старому текстовому значению. Это будет выглядеть так:

private int oldIndex = 0; 
private string oldText = String.Empty; 

private void textBox1_TextChanged(object sender, TextChangedEventArgs e) 
{ 
    double val; 
    if (!Double.TryParse(textBox1.Text, out val)) 
    { 
     textBox1.TextChanged -= textBox1_TextChanged; 
     textBox1.Text = oldText; 
     textBox1.CaretIndex = oldIndex; 
     textBox1.TextChanged += textBox1_TextChanged; 
    } 
} 

private void textBox1_KeyDown(object sender, KeyEventArgs e) 
{ 
    oldIndex = textBox1.CaretIndex; 
    oldText = textBox1.Text; 
} 

CaratIndex полезен не раздражает вашего пользователя к смерти с перемещением курсора в первую позицию на неудачной проверки. Однако этот метод не захватывает нажатие клавиши SpaceBar. Это позволит ввести текст так, как это «1234,56». Кроме того, вставка текста не будет правильно проверена. Помимо этого, мне не нравится возиться с обработчиками событий при обновлении текста.

подход 2

Этот подход должен соответствовать вашим потребностям.

Используйте обработчики событий PreviewKeyDown и PreviewTextInput. Наблюдая за этими событиями и обработкой соответственно, вам не нужно беспокоиться о возврате к предыдущему текстовому значению в текстовом поле. PreviewKeyDown может использоваться для просмотра и игнорирования нажатия клавиши SpaceBar, а PreviewTextInput может использоваться для проверки вашего нового значения текстового поля до его назначения.

private void textBox1_PreviewKeyDown(object sender, KeyEventArgs e) 
{ 
    if (e.Key == Key.Space) 
    { 
     e.Handled = true; 
    } 
} 

private void textBox1_PreviewTextInput(object sender, TextCompositionEventArgs e) 
{ 
    //Create a string combining the text to be entered with what is already there. 
    //Being careful of new text positioning here, though it isn't truly necessary for validation of number format. 
    int cursorPos = textBox1.CaretIndex; 
    string nextText; 
    if (cursorPos > 0) 
    { 
     nextText = textBox1.Text.Substring(0, cursorPos) + e.Text + textBox1.Text.Substring(cursorPos); 
    } 
    else 
    { 
     nextText = textBox1.Text + e.Text; 
    } 
    double testVal; 
    if (!Double.TryParse(nextText, out testVal)) 
    { 
     e.Handled = true; 
    } 
} 

Такой подход делает работу лучше улавливать недопустимый ввод, прежде чем он попадает в текстовое поле. Однако установка события будет Handled. Я полагаю, это может привести к возникновению проблем в зависимости от остальных пунктов назначения в списке маршрутизации для сообщения. Последняя часть, которая не обрабатывается здесь, - это способность пользователя вставлять неверный ввод в текстовое поле. Это можно обработать с добавлением этого кода, который строится на Paste Event in a WPF TextBox.

private void OnPaste(object sender, DataObjectPastingEventArgs e) 
{ 
    double testVal; 
    bool ok = false; 

    var isText = e.SourceDataObject.GetDataPresent(System.Windows.DataFormats.Text, true); 
    if (isText) 
    { 
     var text = e.SourceDataObject.GetData(DataFormats.Text) as string; 
     if (Double.TryParse(text, out testVal)) 
     { 
      ok = true; 
     } 
    } 

    if (!ok) 
    { 
     e.CancelCommand(); 
    } 
} 

Добавить этот обработчик с этим кодом после InitializeComponent вызова:

DataObject.AddPastingHandler(textBox1, new DataObjectPastingEventHandler(OnPaste)); 
+0

Очень хороший ответ –

+0

Вопрос abo ut Подход 1: PreviewTextInput будет работать лучше, чем KeyDown, правильно? (Это не зависит от устройства. И мой сценарий происходит на планшете, где пользователь может не использовать клавиатуру.) –

+0

Если KeyDown или PreviewKeyDown не будут работать на вашем устройстве, дело, которое вам придется обрабатывать, - это как подобрать нажатие клавиши пробела, я не думаю, что это запустит событие PreviewTextInput. –

0

Его действительно раздражает, что TextBox не обеспечивает PreviewTextChanged события и все должны изобрести колесо каждый раз, чтобы эмулировать его. Недавно я решил точно такую ​​же проблему и даже опубликовал свое решение на github как WpfEx project (посмотрите на TextBoxBehavior.cs и TextBoxDoubleValidator.cs).

Ответ Адама Света очень хорош, но мы должны рассмотреть несколько других угловых случаев.

  1. Выбранный текст.

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

private static void PreviewTextInputForDouble(object sender, 
    TextCompositionEventArgs e) 
{ 
    // e.Text contains only new text and we should create full text manually 

    var textBox = (TextBox)sender; 
    string fullText; 

    // If text box contains selected text we should replace it with e.Text 
    if (textBox.SelectionLength > 0) 
    { 
     fullText = textBox.Text.Replace(textBox.SelectedText, e.Text); 
    } 
    else 
    { 
     // And only otherwise we should insert e.Text at caret position 
     fullText = textBox.Text.Insert(textBox.CaretIndex, e.Text); 
    } 

    // Now we should validate our fullText, but not with 
    // Double.TryParse. We should use more complicated validation logic. 
    bool isTextValid = TextBoxDoubleValidator.IsValid(fullText); 

    // Interrupting this event if fullText is invalid 
    e.Handled = !isTextValid; 
} 

И мы должны использовать ту же логику, когда будем обрабатывать событие OnPaste.

  1. Проверка текста

Мы не можем использовать простой Double.TryParse, поскольку пользователь может ввести '+'. для типа '+.1' ('+.1' - абсолютно допустимая строка для double), поэтому наш метод проверки должен возвращать true в '+.' или '-.' (я даже создал отдельный класс под названием TextBoxDoubleValidator и набор модульных тестов, потому что эта логика так важна).

Перед копаться в реализации позволяет взглянуть на набор модульных тестов, которые будут охватывать все случаи угловые для метода проверки:

[TestCase("", Result = true)] 
[TestCase(".", Result = true)] 
[TestCase("-.", Result = true)] 
[TestCase("-.1", Result = true)] 
[TestCase("+", Result = true)] 
[TestCase("-", Result = true)] 
[TestCase(".0", Result = true)] 
[TestCase("1.0", Result = true)] 
[TestCase("+1.0", Result = true)] 
[TestCase("-1.0", Result = true)] 
[TestCase("001.0", Result = true)] 
[TestCase(" ", Result = false)] 
[TestCase("..", Result = false)] 
[TestCase("..1", Result = false)] 
[TestCase("1+0", Result = false)] 
[TestCase("1.a", Result = false)] 
[TestCase("1..1", Result = false)] 
[TestCase("a11", Result = false)] 
[SetCulture("en-US")] 
public bool TestIsTextValid(string text) 
{ 
    bool isValid = TextBoxDoubleValidator.IsValid(text); 
    Console.WriteLine("'{0}' is {1}", text, isValid ? "valid" : "not valid"); 
    return isValid; 
} 

Обратите внимание, что я использую SetCulture ("EN-US") атрибут , потому что десятичный разделитель «локально-специфический».

Я думаю, что я покрываю все угловые шкалы этими тестами, но с помощью этого инструмента в ваших руках вы можете легко «эмулировать» пользователя вменять и проверять (и повторно использовать) любые случаи, которые вы хотите. И теперь давайте взглянем на метод TextBoxDoubleValidator.IsValid:

/// <summary> 
/// Helper class that validates text box input for double values. 
/// </summary> 
internal static class TextBoxDoubleValidator 
{ 
    private static readonly ThreadLocal<NumberFormatInfo> _numbersFormat = new ThreadLocal<NumberFormatInfo>( 
     () => Thread.CurrentThread.CurrentCulture.NumberFormat); 

    /// <summary> 
    /// Returns true if input <param name="text"/> is accepted by IsDouble text box. 
    /// </summary> 
    public static bool IsValid(string text) 
    { 
     // First corner case: null or empty string is a valid text in our case 
     if (text.IsNullOrEmpty()) 
      return true; 

     // '.', '+', '-', '+.' or '-.' - are invalid doubles, but we should accept them 
     // because user can continue typeing correct value (like .1, +1, -0.12, +.1, -.2) 
     if (text == _numbersFormat.Value.NumberDecimalSeparator || 
      text == _numbersFormat.Value.NegativeSign || 
      text == _numbersFormat.Value.PositiveSign || 
      text == _numbersFormat.Value.NegativeSign + _numbersFormat.Value.NumberDecimalSeparator || 
      text == _numbersFormat.Value.PositiveSign + _numbersFormat.Value.NumberDecimalSeparator) 
      return true; 

     // Now, lets check, whether text is a valid double 
     bool isValidDouble = StringEx.IsDouble(text); 

     // If text is a valid double - we're done 
     if (isValidDouble) 
      return true; 

     // Text could be invalid, but we still could accept such input. 
     // For example, we should accepted "1.", because after that user will type 1.12 
     // But we should not accept "..1" 
     int separatorCount = CountOccurances(text, _numbersFormat.Value.NumberDecimalSeparator); 

     // If text is not double and we don't have separator in this text 
     // or if we have more than one separator in this text, than text is invalid 
     if (separatorCount != 1) 
      return false; 

     // Lets remove first separator from our input text 
     string textWithoutNumbersSeparator = RemoveFirstOccurrance(text, _numbersFormat.Value.NumberDecimalSeparator); 

     // Second corner case: 
     // '.' is also valid text, because .1 is a valid double value and user may try to type this value 
     if (textWithoutNumbersSeparator.IsNullOrEmpty()) 
      return true; 

     // Now, textWithoutNumbersSeparator should be valid if text contains only one 
     // numberic separator 
     bool isModifiedTextValid = StringEx.IsDouble(textWithoutNumbersSeparator); 
     return isModifiedTextValid; 
    } 

    /// <summary> 
    /// Returns number of occurances of value in text 
    /// </summary> 
    private static int CountOccurances(string text, string value) 
    { 
     string[] subStrings = text.Split(new[] { value }, StringSplitOptions.None); 
     return subStrings.Length - 1; 

    } 

    /// <summary> 
    /// Removes first occurance of valud from text. 
    /// </summary> 
    private static string RemoveFirstOccurrance(string text, string value) 
    { 
     if (string.IsNullOrEmpty(text)) 
      return String.Empty; 
     if (string.IsNullOrEmpty(value)) 
      return text; 

     int idx = text.IndexOf(value, StringComparison.InvariantCulture); 
     if (idx == -1) 
      return text; 
     return text.Remove(idx, value.Length); 
    } 

} 
+0

Я определенно согласен с вашим комментарием о PreviewTextChanged. Использование такого события позволило бы решить эту проблему. –

+0

Пример включает отсутствующие классы/расширения, поэтому его -1 нельзя использовать из коробки –

0

Комментарий, а не ответ, но ...

Я бы остерегаться проверка ввода на каждом нажатии клавиши, как это может иметь непредсказуемые последствия и раздражают конечного пользователя.

Например, я помню, что меня раздражало контрольное устройство datepicker, которое не давало даты в будущем и было инициализировано на сегодняшний день. Он выполнил проверку после ввода дня, месяца или года, так что было невозможно ввести месяц/день позже текущей даты без первого изменения года.

В случае двойников у вас может возникнуть аналогичная проблема, например, предлагаемая проверка не позволит пользователю ввести совершенно допустимые значения «-1», «.12" , „1e + 5“:..

-  - invalid 
-1  - valid 

.  - invalid 
.1  - valid 

1  - valid 
1e  - invalid 
1e+  - invalid 
1e+5 - valid 

Я бы рекомендовал проверки в обычном режиме, когда пользователь оставляет текстовое поле или явно подтверждает, нажав на кнопку

Смежные вопросы