Числовой TextBox - с использованием Double.TryParse - PullRequest
3 голосов
/ 03 февраля 2012

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

Требуется текстовое поле, которое всегда будет содержать строку, для которой Double.TryParse будет возвращать true.

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

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

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

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

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

Ответы [ 3 ]

3 голосов
/ 03 февраля 2012

Подход 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, я полагаю, вы можете столкнуться с проблемами в зависимости от остальных адресатов в списке маршрутизации сообщения. Последний фрагмент, который здесь не обрабатывается, - это возможность пользователя вставить неверный ввод в текстовое поле. Это можно сделать с помощью добавления этого кода, который построен из Вставить событие в 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 голосов
/ 04 февраля 2012

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

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

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

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

-       - invalid
-1      - valid

.       - invalid
.1      - valid

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

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

0 голосов
/ 04 февраля 2012

Действительно раздражает, что TextBox не обеспечивает событие PreviewTextChanged, и каждый должен изобретать колесо каждый раз, чтобы подражать ему. Недавно я решил точно такую ​​же проблему и даже опубликовал свое решение на github как проект WpfEx (взгляните на 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 '), потому что десятичный разделитель" local-specific ".

Я думаю, что я покрываю все угловые случаи с помощью этих тестов, но с этим инструментом в ваших руках вы можете легко "эмулировать" ввод пользователя и проверять (и повторно использовать) любые случаи, которые вы хотите. А теперь давайте посмотрим на 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); 
    }

}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...