WPF 中的 Regex 验证






4.85/5 (37投票s)
演示如何在 Windows Presentation Foundation 应用程序中使用正则表达式执行输入验证。
引言
本文演示如何在 Windows Presentation Foundation (WPF) 应用程序中使用正则表达式验证用户输入。本文介绍的技术为开发人员提供了两种通过正则表达式验证 TextBox
文本的方法:显式地将派生自 ValidationRule
的对象添加到 Binding
的 ValidationRules
中,或者简单地使用静态服务提供程序类的附加属性。
此代码已针对 .NET Framework v3.0 的 2006 年 6 月 CTP 版进行编译和测试,但在更高版本的框架中也应能正常工作。
背景
WPF 提供了一种与 WinForms 和 ASP.NET 中执行验证方式截然不同的输入验证方法。本文假设读者熟悉 WPF 验证的基础知识。如果您还不熟悉 WPF 中的验证,我强烈推荐 Paul Stovell 关于该主题的优秀文章。SDK 中的此页面也信息量很大。
WPF 中输入验证的基本思想涉及抽象的 ValidationRule
类。派生自 ValidationRule
的类必须重写 Validate
方法以执行自定义验证逻辑,并返回一个指示输入值是否有效的值。ValidationRule
派生类的实例可以添加到 Binding
的 ValidationRules
集合中。当需要验证绑定值时,将查询所有 ValidationRule
派生对象以查看输入值是否有效。
RegexValidationRule
我的目标是创建一种通过正则表达式验证用户输入的可重用方法。我通过创建 RegexValidationRule
类来实现这一点,该类派生自 ValidationRule
,并在其 Validate
重写中执行 Regex
。RegexValidationRule
旨在验证 TextBox
的 Text
属性。
下面的 XAML 展示了如何使用此类别
<TextBox Name="txtProductCode">
<TextBox.Text>
<Binding Path="ProductCode">
<Binding.ValidationRules>
<jas:RegexValidationRule
RegexText="^[A-Z]{3}\.[0-9]{3}$"
ErrorMessage="Invalid product code"
RegexOptions="IgnoreCase"
/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
上面在 XAML 中创建的 Binding
和 RegexValidationRule
可以在代码隐藏中创建,如下所示
Binding binding = new Binding();
binding.Path = new PropertyPath( "ProductCode" );
RegexValidationRule rule = new RegexValidationRule();
rule.RegexText = @"^[A-Z]{3}\.[0-9]{3}$";
rule.ErrorMessage = "Invalid product code.";
rule.RegexOptions = RegexOptions.IgnoreCase;
binding.ValidationRules.Add( rule );
txtProductCode.SetBinding( TextBox.TextProperty, binding );
RegexValidationRule
类有三个公共属性,可用于调整其行为
RegexText
- 存储验证期间使用的正则表达式。
ErrorMessage
- 存储验证失败时使用/显示的文本(例如工具提示中的消息)。
RegexOptions
- 允许您修改用于验证输入值的Regex
对象的RegexOptions
属性。
在 XAML 文件中编写正则表达式时,请务必记住 XAML 要求某些字符必须转义,例如“&”号 (& = &) 和撇号 (' = ')。有关这些必要麻烦的更多信息,请参阅 SDK 中的此页面。
这都很好,但是仅仅向 Text
属性的关联 Binding
在 XAML 中添加一个 RegexValidationRule
实例似乎需要付出很多努力。如果不需要创建多个嵌套元素,只需向 Binding
的 ValidationRules
集合添加一个规则,那将更加方便。本文的下一节将展示一种更容易使用 RegexValidationRule
类的方法。
RegexValidator
RegexValidator
类提供了一种流线型的方式,使用正则表达式来验证 TextBox
的文本。通过附加属性的魔力,可以避免上一节中看到的 XAML 中的多个嵌套元素,只需将 RegexValidationRule
添加到 Binding
的 ValidationRules
中。
RegexValidator
是一个静态类,其唯一工作是创建 RegexValidationRule
对象并将它们添加到相应的 Binding
的 ValidationRules
中。它通过附加属性公开其功能,因此您无需创建 RegexValidator
实例即可使用它提供的服务。事实上,您无法实例化 RegexValidator
类,因为它是一个静态类。
下面是 RegexValidator
的使用示例
<TextBox Name="txtProductCode"
Text="{Binding Path=ProductCode}"
jas:RegexValidator.RegexText="^[a-zA-Z]{3}\.[0-9]{3}$"
jas:RegexValidator.ErrorMessage="Invalid product code."
/>
上面看到的 XAML 可以用 C# 表示,如下所示
// Establish the Text property's binding.
Binding binding = new Binding();
binding.Path = new PropertyPath( "ProductCode" );
this.txtProductCode.SetBinding( TextBox.TextProperty, binding );
// Assign the regular expression.
RegexValidator.SetRegexText(
this.txtProductCode, @"^[a-zA-Z]{3}\.[0-9]{3}$" );
// Assign the error message.
RegexValidator.SetErrorMessage(
this.txtProductCode, "Invalid product code." );
正如您所看到的,RegexValidator
比 RegexValidationRule
更易于使用,尤其是在 XAML 中使用时。您只需指定附加属性的值,其余工作将由系统为您处理。
细心的观察者会注意到,这是上一节中使用的示例的修改版本,但它没有设置 RegexOptions
属性。我决定从 RegexValidator
中省略 RegexOptions
属性,因为我怀疑该属性不会经常使用(我可能错了!)。省略 RegexValidator
中的该属性的好处是,该类的 API 保持非常小巧和简单。如果您需要设置 RegexOptions
属性,您将需要像本文上一节中那样显式创建 RegexValidationRule
。
工作原理
本文的其余部分解释了 RegexValidationRule
和 RegexValidator
的工作原理。您不需要阅读本节即可在您的应用程序中使用这些类。
RegexValidationRule
非常简单。下面是该类的精简版本
public class RegexValidationRule : ValidationRule
{
// All other members were omitted for clarity.
/// <summary>
/// Validates the 'value' argument using the regular expression and
/// RegexOptions associated with this object.
/// </summary>
public override ValidationResult Validate( object value,
CultureInfo cultureInfo )
{
ValidationResult result = ValidationResult.ValidResult;
// If there is no regular expression to evaluate,
// then the data is considered to be valid.
if( ! String.IsNullOrEmpty( this.RegexText ) )
{
// Cast the input value to a string (null becomes empty string).
string text = value as string ?? String.Empty;
// If the string does not match the regex, return a value
// which indicates failure and provide an error mesasge.
if( ! Regex.IsMatch( text, this.RegexText, this.RegexOptions ) )
result = new ValidationResult( false, this.ErrorMessage );
}
return result;
}
Validate
方法(继承自 ValidationRule
)只是调用 Regex
类的静态 IsMatch
方法来执行验证。如果输入值与存储在 RegexText
属性中的正则表达式不匹配,则返回一个 ValidationResult
,指示该值无效并提供错误消息。错误消息从 ErrorMessage
属性中检索。如果输入值有效,则返回 ValidResult
属性。该属性返回 ValidationResult
的单例实例,因此使用它来指示成功可以避免用所有表达相同内容(“该值有效”)的冗余 ValidationResult
实例来碎片化托管堆。
RegexValidator
类更为复杂。首先,让我们看看该类是如何声明的
public static class RegexValidator
{
// There's more to come...
}
由于它是一个静态类,因此永远无法实例化,并且其所有成员都必须是静态的。这是有道理的,因为这个类是一个服务提供者,它的所有功能都通过附加属性公开。没有理由实例化该类。
接下来,我们将检查其附加属性的实现
/// <summary>
/// Identifies the RegexValidator's ErrorMessage attached property.
/// This field is read-only.
/// </summary>
public static readonly DependencyProperty ErrorMessageProperty;
/// <summary>
/// Returns the error message used when validation fails for the
/// specified TextBox.
/// </summary>
/// <param name="textBox">The TextBox whose error message is returned.</param>
public static string GetErrorMessage( TextBox textBox )
{
return textBox.GetValue( ErrorMessageProperty ) as string;
}
/// <summary>
/// Sets the error message used when validation fails for the
/// specified TextBox.
/// </summary>
/// <param name="textBox">The TextBox being validated.</param>
/// <param name="value">The error message.</param>
public static void SetErrorMessage( TextBox textBox, string value )
{
textBox.SetValue( ErrorMessageProperty, value );
}
/// <summary>
/// Identifies the RegexValidator's RegexText attached property.
/// This field is read-only.
/// </summary>
public static readonly DependencyProperty RegexTextProperty;
/// <summary>
/// Returns the regular expression used to validate the specified TextBox.
/// </summary>
/// <param name="textBox">
/// The TextBox whose regular expression is returned.
/// </param>
public static string GetRegexText( TextBox textBox )
{
return textBox.GetValue( RegexTextProperty ) as string;
}
/// <summary>
/// Sets the regular expression used to validate the
/// specified TextBox.
/// </summary>
/// <param name="textBox">The TextBox being validated.</param>
/// <param name="value">The regular expression.</param>
public static void SetRegexText( TextBox textBox, string value )
{
textBox.SetValue( RegexTextProperty, value );
}
这些附加属性遵循了公共静态 Get/Set 方法的约定,以将属性值与 DependencyObject
相关联(在本例中,它只适用于 TextBox
类)。附加属性在静态构造函数中注册
static RegexValidator()
{
RegexTextProperty = DependencyProperty.RegisterAttached(
"RegexText",
typeof( string ),
typeof( RegexValidator ),
new UIPropertyMetadata( null, OnAttachedPropertyChanged ) );
ErrorMessageProperty = DependencyProperty.RegisterAttached(
"ErrorMessage",
typeof( string ),
typeof( RegexValidator ),
new UIPropertyMetadata( null, OnAttachedPropertyChanged ) );
}
请注意,注册属性时,在 UIPropertyMetadata
参数中提供了一个回调方法。当 TextBox
的 RegexValidator
附加属性之一的值被修改时,会调用该回调。该回调方法的实现如下
/// <summary>
/// Invoked whenever an attached property of the
/// RegexValidator is modified for a TextBox.
/// </summary>
/// <param name="depObj">A TextBox.</param>
/// <param name="e"></param>
static void OnAttachedPropertyChanged( DependencyObject depObj,
DependencyPropertyChangedEventArgs e )
{
TextBox textBox = depObj as TextBox;
if( textBox == null )
throw new InvalidOperationException(
"The RegexValidator can only be used with a TextBox." );
VerifyRegexValidationRule( textBox );
}
您可能想知道,为什么我费心使用回调来调用 VerifyRegexValidationRule
方法,而不是直接从为附加属性创建的静态 Set
方法中调用该方法。这种方法可能看起来可以正常工作,并且更简单
// This is not how it works!
public static void SetErrorMessage( TextBox textBox, string value )
{
textBox.SetValue( ErrorMessageProperty, value );
VerifyRegexValidationRule( textBox );
}
如果 ErrorMessage
属性的值始终通过为 TextBox
调用 RegexValidator.SetErrorMessage
来设置,那么这种方法就会奏效。然而,完全有可能通过直接调用 TextBox
本身的 SetValue
来为 TextBox
设置 ErrorMessage
附加属性(或任何其他附加属性),就像 SetErrorMessage
方法所做的那样。在这种情况下,对 VerifyRegexValidationRule
的调用将不会发生,并且 TextBox
的 Text
属性绑定将永远不会向其 ValidationRules
集合中添加 RegexValidationRule
。通过使用注册附加属性时提供的回调方法,我们可以确保每当 TextBox
的附加属性值被修改时,我们的回调都会调用 VerifyRegexValidationRule
。
现在,是时候检查 VerifyRegexValidationRule
方法的作用了。请记住,只要 RegexValidator
附加属性的值被修改,就会调用此方法。
/// <summary>
/// Creates or modifies the RegexValidationRule in the TextBox's
/// Text property binding to use the current values of the attached
/// properties exposed by this class.
/// </summary>
/// <param name="textBox">The TextBox being validated.</param>
static void VerifyRegexValidationRule( TextBox textBox )
{
RegexValidationRule regexRule = GetRegexValidationRuleForTextBox( textBox );
if( regexRule != null )
{
regexRule.RegexText =
textBox.GetValue( RegexValidator.RegexTextProperty ) as string;
regexRule.ErrorMessage =
textBox.GetValue( RegexValidator.ErrorMessageProperty ) as string;
}
}
此方法尝试检索一个 RegexValidationRule
,用于指定 TextBox
的 Text
属性绑定。如果它获得了对该对象的引用,则会将属性值传输到该对象。请注意,RegexValidationRule
属性的实际值由 TextBox
作为附加属性存储。在处理附加属性时,务必记住这些属性的值由它们所应用的对象存储,而不是由公开附加属性的类/对象存储。
此时,有必要检查 GetRegexValidationRuleForTextBox
方法的工作原理。此方法负责为给定的 TextBox
创建或检索 RegexValidationRule
对象。
/// <summary>
/// Returns a RegexValidationRule to be used for validating the specified
/// TextBox. If the TextBox is not yet initialized, this method returns null.
/// </summary>
static RegexValidationRule GetRegexValidationRuleForTextBox( TextBox textBox )
{
if( ! textBox.IsInitialized )
{
// If the TextBox.Text property is bound, but the TextBox is not yet
// initialized, the property's binding can be null. In that situation,
// hook its Initialized event and verify the validation rule again when
// that event fires. At that point in time, the Text property's binding
// will be non-null.
EventHandler callback = null;
callback = delegate
{
textBox.Initialized -= callback;
VerifyRegexValidationRule( textBox );
};
textBox.Initialized += callback;
return null;
}
// Get the binding expression associated with the TextBox's Text property.
BindingExpression expression =
textBox.GetBindingExpression( TextBox.TextProperty );
if( expression == null )
throw new InvalidOperationException(
"The TextBox's Text property must be bound for the RegexValidator to " +
"validate it." );
// Get the binding which owns the binding expression.
Binding binding = expression.ParentBinding;
if( binding == null )
throw new ApplicationException(
"Unexpected situation: the TextBox.Text binding expression has no " +
"parent binding." );
// Look for an existing instance of the RegexValidationRule class in the
// binding. If there is more than one instance in the ValidationRules
// then throw an exception because we don't know which one to modify.
RegexValidationRule regexRule = null;
foreach( ValidationRule rule in binding.ValidationRules )
{
if( rule is RegexValidationRule )
{
if( regexRule == null )
regexRule = rule as RegexValidationRule;
else
throw new InvalidOperationException(
"There should not be more than one RegexValidationRule in a Binding's " +
"ValidationRules." );
}
}
// If the TextBox.Text property's binding does not yet have an
// instance of RegexValidationRule in its ValidationRules,
// add an instance now and return it.
if( regexRule == null )
{
regexRule = new RegexValidationRule();
binding.ValidationRules.Add( regexRule );
}
return regexRule;
}
该方法获取与 TextBox
的 Text
属性关联的 BindingExpression
的引用。然后,它检索拥有该表达式的 Binding
的引用,并遍历 Binding
的 ValidationRules
集合以查找 RegexValidationRule
。如果找不到,它将创建一个新实例并将其添加到 ValidationRules
集合中。如果找到多个 RegexValidationRule
,它将抛出异常,因为无法知道应该返回哪一个。
结论
RegexValidationRule
和 RegexValidator
支持在 WPF 中使用正则表达式验证 TextBox
。
您可以创建 RegexValidationRule
并显式将其添加到 Text
属性的 Binding
中,或者直接使用 RegexValidator
的附加属性,它将为您处理其余工作。
如果您需要为 RegexValidationRule
指定单个 RegexOptions
值,您可以在 XAML 中使用该类的 RegexOptions
属性。
在 XAML 文件中创建正则表达式时,请记住 XAML 要求某些字符必须进行转义(例如和号和撇号)。
完整的源代码和演示应用程序可从本文顶部下载。
修订历史
- 2006 年 9 月 17 日 - 创建文章。
- 2006 年 11 月 2 日 - 最初,文章指出位操作不能在 XAML 中执行(即 XAML 解析器不支持它们)。我提到“事实”是关于组合
RegexOptions
枚举的多个值。事实证明我错了;通过用逗号分隔,可以组合带标志的枚举值。我从文章中删除了该错误信息。以下是揭示 XAML 中组合带标志枚举真相的 WPF 论坛帖子:Microsoft 论坛。