65.9K
CodeProject 正在变化。 阅读更多。
Home

在 .NET 3.5 中创建一个可解析的文本框

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.50/5 (2投票s)

2009 年 8 月 2 日

GPL3

5分钟阅读

viewsIcon

27566

downloadIcon

218

可解析的文本框会验证并解析其中的文本,将其转换为强类型 .NET 对象

引言

.NET 3.5 SP1 框架提供的标准 WPF 控件不包含数字文本框等一系列控件。开发者们经常需要这些文本框来限制用户输入,以便接受整数或浮点数等。我在开发一个个人爱好项目时开发了这些控件。在此过程中,我想将这些控件的范围扩展到数字数据类型之外,并通用地将任何文本解析为强类型的 .NET 对象。本文将介绍如何扩展该库,以针对您想要创建的任何 .NET 数据类型,从纯文本输入开始。

注意除非另有说明,否则我们将把需要解析的输入视为 System.Double 对象。我们将其文本表示称为浮点数表示。

背景

目标是创建接受文本输入并能将其解析为强类型 .NET 对象的文本框。现在,大多数人会逐个字符地输入文本(可能还会按退格键、光标键编辑/删除先前输入的字符,或在任何位置添加新字符),因此仅截获 TextInput 事件并检查 Double.TryParse 的返回值是 true 还是 false 是不够的。话虽如此,我们还希望阻止用户输入那些一旦输入,即使附加任何字符也无法使输入成为有效的浮点数的字符。此外,我们还希望通知他们需要附加哪些字符才能使输入有效。

例如,在下面的屏幕截图中,第一行显示了一些可以(在第二行)附加特定字符后变为有效的输入示例。

MyControls

有些输入永远无法使其有效,也就是说,任何字符组合都无法修正它。例如,-1.2. -1.. -- 12p 12e-1. 等。我们应阻止文本框达到这种状态。

由于本文的主要目的是演示如何扩展此控件,让我们详细回顾一下其设计。如果让我编写一个程序以逐个字符地接受字符串输入,并确定迄今为止收到的输入是否构成有效的浮点数,您可能会开始考虑有限自动机。也就是说,您会定义一些状态,并根据一些预定义的(即设计时)规则在状态之间跳转。其中一些状态将是最终状态——这意味着如果您处于该状态,输入就是有效的,否则就是无效的。虽然这种方法没有问题,并且您可以使用这种方法构建正确的程序,但代码看起来会有点混乱。此外,如果您要扩展此代码以接受浮点数表示的变体(例如接受千位分隔符或指数部分),那么您将不得不添加新的状态、新的跳转等。

现在,让我们利用一种称为正则表达式的东西。正则表达式等同于有限自动机——也就是说,对于每个有限自动机,都存在一个(在同一组字母表上的)正则表达式,反之亦然。因此,如果我们能为有效的浮点数模式定义一个正则表达式,那么我们只需要找出输入是否匹配该正则表达式。为此,您需要再次编写自动机代码,这样整个目的就落空了。但是,一些好心人已经编写了代码来将任何文本与任何正则表达式进行匹配,并且可以在 .NET 的 System.Text.RegularExpressions 命名空间下找到。

所以,首先我们需要两个正则表达式:一个用于有效模式,一个用于存在某种字符组合,当附加到该组合时可以使其有效。对于 System.Double 对象,这些可以是

^[-+]?(\d+,)*\d+(\.?\d+)?([eE]?[-+]?\d+)?$
^[-+]?((\d+,)*(\d+\.?\d*)?)?([eE]([-+]?\d*)?)?$

分别。请注意,我们故意将这些模式设置得比 Double.TryParse 能够容忍的更严格。这个决定纯粹是出于美观。您可以根据需要放宽它,只要您知道如何从中创建 System.Double 对象即可。

现在,我们还需要以某种方式禁止讨论的模式之外的模式。因此,我们将处理 TextBox 类的 OnPreviewTextInput 事件,并保存其中当前输入的文本。在文本输入后(即 TextInput 事件),我们可以检查它是否不匹配上述模式,并恢复该文本。您可以通过归纳法证明,通过这种方法我们永远不会保存不合规的输入。

为了提供视觉提示,我们声明了两个属性(准确来说是自动属性)

/// <summary>
/// Gets/Sets the brush to indicate that the current input needs
/// characters to be appended to it so that it becomes valid.
/// </summary>
public Brush InvalidInputBrush { get; set; }

/// <summary>
/// Gets/Sets the brush to indicate that the control holds valid input text
/// in it.
/// </summary>
public Brush ValidInputBrush { get; set; }

protected override void OnPreviewTextInput(
    System.Windows.Input.TextCompositionEventArgs e)
{
    //We always keep text that is ok, so save the last ok text
    m_oldText = this.Text;
    base.OnPreviewTextInput(e);
}

protected override void OnTextChanged(TextChangedEventArgs e)
{
    base.OnTextChanged(e);
    /*
     * Empty text is ok.
     * Text that is valid is obviously valid.
     * Text that can become valid later by "appending" characters is a valid input
     * but it won't update the this.Value. We can also give a hint to the user.
     */
    if (String.IsNullOrEmpty(this.Text))
    {
    	m_value = default(T);
    	this.Background = this.ValidInputBrush;
    }
    else if (this.ValidT.IsMatch(this.Text))
    {
	T t;
	//We can still fail, like overflow exceptions, etc. so double check    
	if (this.TryParse(this.Text, out t))
		m_value = t;
	else
		this.RestoreOldText();

	this.Background = this.ValidInputBrush;
    }
    else if (this.PotentialT.IsMatch(this.Text))
    {
        this.Background = this.InvalidInputBrush;
    }
    else
    {
        //In other words, cancel the effect of this text input
        this.RestoreOldText();
    }
}

private void RestoreOldText()
{
    this.Text = m_oldText;
    //The following code ensures that the cursor is always at the end
    this.SelectionStart = this.Text.Length;
    this.SelectionLength = 0;
}

private bool TryParse(string text_, out double d)
{
    return Double.TryParse(text_, out d);
}

现在,为了使其可扩展,让我们将所有这些代码放入一个 abstract class Parsable<T> : TextBox 中,它公开了以下契约(我们上面的画笔是该契约的一部分)

/// <summary>
/// Gets/Sets the value corresponding to the input.
/// </summary>
public T Value { get; set; }

/// <summary>
/// Gets a value indicating if the current input is valid.
/// </summary>
public bool IsValid { get; private set; }

/// <summary>
/// Gets/Sets the brush to indicate that the current input needs
/// characters to be appended to it so that it becomes valid.
/// </summary>
public Brush InvalidInputBrush { get; set; }

/// <summary>
/// Gets/Sets the brush to indicate that the control holds valid input text
/// in it.
/// </summary>
public Brush ValidInputBrush { get; set; }

下面的 abstract 方法已声明

/// <summary>
/// Parses text to an object of type <typeparamref name="T"/> and
/// returns a value indicating if the text was successfully parsed.
/// </summary>
/// <param name="text_">The text to parse.</param>
/// <param name="t_">The object in which to hold the parsed object,
/// to be passed uninitialized.</param>
/// <returns>True if the text could be parsed as <typeparamref name="T"/>;
/// false otherwise.</returns>
protected abstract bool TryParse(string text_, out T t_);

/// <summary>
/// Gets the text that when parsed by <see cref="TryParse"/> will return 
/// an object equal to <paramref name="t_"/>.
/// </summary>
/// <param name="t_">The object.</param>
/// <returns>The text that when parsed by <see cref="TryParse"/> will return 
/// an object equal to <paramref name="t_"/>.</returns>
protected abstract string ToText(T t_);

/// <summary>
/// Gets the regular expression for all text for which a certain character
/// combination exists that when appended to it will make it a valid input.
/// </summary>
protected abstract Regex PotentialT { get; }

/// <summary>
/// Gets the regular expression for valid input text.
/// </summary>
protected abstract Regex ValidT { get; }

您会注意到有一个名为 ToTextabstract 方法。当为 this.Value 赋值时,将使用此方法来设置文本。

因此,要扩展它,您只需要实现这些方法。在附带的源代码中有三个示例。作为一项练习,尝试用于社会安全号码。(如果您从未为 SSN 设计过类,而是仅仅将它们作为字符串传递,那么这将是学习编写良好代码的又一个教训。如果字符串对象具有特殊意义,那么它们应该有自己的类。这大大简化了您的代码架构。)

Using the Code

要在 WPF 窗体中使用此控件,请执行以下操作

<my:LongIntegerTextBox Grid.Column="1" Grid.Row="1"
    HorizontalAlignment="Center" VerticalAlignment="Center" Margin="5,5,5,5"
    Width="100" Height="30" InvalidInputBrush="LightPink" ValidInputBrush="White"/>

属性 this.Value 将为您提供用户最近一次输入的有效值。

历史

  • 2009 年 8 月 2 日:版本 1.0 上传
© . All rights reserved.