数字文本框 - 使用Double.TryParse

时间:2012-02-03 16:25:44

标签: c# .net wpf xaml textbox

我知道这是一个古老的问题,有许多答案,但我没有找到任何好的,有力的答案。

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

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

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

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

这样做的最佳方式是什么?

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

3 个答案:

答案 0 :(得分:3)

方法1

TextChanged使用KeyDownTextBox个事件的组合。在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

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

使用PreviewKeyDownPreviewTextInput事件处理程序。通过观察这些事件并相应地处理,您无需担心还原到文本框中的先前文本值。 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));

答案 1 :(得分:0)

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

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

  1. 选定文字。
  2. 在我们的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. 验证文字
    2. 我们不能使用简单的Double.TryParse,因为用户可以输入'+。'输入'+.1'('+ 1' - 绝对有效的双字符串),所以我们的验证方法应该在'+'上返回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); 
          }
      
      }
      

答案 2 :(得分:0)

评论而非答案,但......

我会注意验证每个按键上的输入,因为它可能会产生意想不到的后果并使最终用户烦恼。

例如,我记得被一个不允许将来允许日期的日期选择器控件烦恼,并被初始化为今天的日期。它在输入日,月或年后执行验证,因此如果不先更改年份,则无法在当前日期之后输入一个月/日。

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

-       - invalid
-1      - valid

.       - invalid
.1      - valid

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

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