【问题标题】:Numeric TextBox - using Double.TryParse数字文本框 - 使用 Double.TryParse
【发布时间】:2012-02-03 16:25:44
【问题描述】:

我知道这是一个古老的问题,有很多答案,但我还没有找到任何好的、可靠的答案。

要求是一个始终包含 Double.TryParse 将返回 true 的字符串的文本框。

我见过的大多数实现都不防范输入,例如:“10.45.8”。这是个问题。

最好的方法是完全使用事件,例如 TextInput 和 KeyDown(用于空格)。这些问题是在更改之前获取表示新文本(或更改后的旧文本)的字符串非常复杂。 TextChanged 的​​问题在于它没有提供获取旧文本的方法。

如果您可以在新文本更改之前以某种方式获取它,那将是最有帮助的,因为您可以针对 Double.TryParse 对其进行测试。不过可能有更好的解决方案。

最好的方法是什么?

这个问题的最佳答案是有几种方法并比较它们。

【问题讨论】:

  • @jberger:是什么让你这么说?这还不够简单吗?
  • 当用户输入一个无效字符然后离开文本框时会发生什么?
  • 当他们输入一个无效的字符时,它应该什么都不做,就像他们没有按键一样。
  • 出于可用性原因,我会尝试放弃该要求 - 请参阅我的答案。

标签: c# .net wpf xaml textbox


【解决方案1】:

方法 1

TextBox 使用TextChangedKeyDown 事件的组合。在KeyDown 上,您可以将当前文本保存在文本框中,然后在TextChanged 事件中执行您的Double.TryParse。如果输入的文本无效,那么您将恢复为旧文本值。这看起来像:

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

这种方法应该可以满足您的需求。

使用PreviewKeyDownPreviewTextInput 事件处理程序。通过观察这些事件并进行相应的处理,您不必担心在文本框中恢复到以前的文本值。 PreviewKeyDown 可用于监视和忽略您的空格键按下,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));

【讨论】:

  • 关于方法 1 的问题:PreviewTextInput 会比 KeyDown 更好,对吧? (它与设备无关。我的场景恰好是在平板电脑上,用户可能不使用键盘。)
  • 如果 KeyDown 或 PreviewKeyDown 在您的设备上不起作用,您必须处理的情况是如何拿起 Space 按键,我认为这不会触发 PreviewTextInput事件。
  • 除了空格键之外还有更多问题,请参阅我的回答(在某些文化中,空格是有效的千位分隔符,例如 fr-FR)。
【解决方案2】:

TextBox 不提供 PreviewTextChanged 事件真的很烦人,每个人每次都应该发明轮子来模仿它。我最近解决了完全相同的问题,甚至在 github 上以WpfEx project 发布了我的解决方案(看看TextBoxBehavior.csTextBoxDoubleValidator.cs)。

Adam S 的回答非常好,但我们也应该考虑一些其他极端情况。

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

}

【讨论】:

  • 我绝对同意您对 PreviewTextChanged 的​​评论。使用这样的事件将很快解决这个问题。
  • 示例包括缺少的类/扩展,因此它的 -1 不能开箱即用
【解决方案3】:

评论而不是答案,但是...

我会提防在每次按键时验证输入,因为它可能会产生意想不到的后果并惹恼最终用户。

例如,我记得我对一个不允许未来日期的日期选择器控件感到恼火,并将其初始化为今天的日期。它在输入年月日后进行验证,因此如果不先更改年份,就无法输入比当前日期晚的月/日。

在双打的情况下,您可能会遇到类似的问题,例如您提出的验证会阻止用户输入完全有效的值“-1”、“.12”、“1e+5”:

-       - invalid
-1      - valid

.       - invalid
.1      - valid

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

我建议在用户离开文本框或通过单击按钮显式验证时正常进行验证。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多