用于用户输入全面验证的框架
使Windows.Forms、WPF、控制台应用程序或任何其他用途的输入验证尽可能简单
内容
2 背景
2.1 在 Windows.Forms 中验证用户输入
2.2 在 WPF 中验证用户输入
2.3 结论
3 库
3.1 验证器
3.2 链接
3.2.1 WPF
3.2.2 Windows.Forms
3.2.3 MVVM
3.3 自定义
3.3.1 验证器
3.3.2 链接
3.4 已知问题
1 引言
如果在 .NET 中处理对话框时,所有用户输入的数据都必须经过验证,这会导致许多麻烦和精力。有许多标准方法可以可靠地进行验证。所有这些方法的共同点是,它们必须为每个对话框重新实现。
我的目标是尽量减少因对话框式输入验证的持续重新实现而造成的精力消耗和成本。为了实现这一目标,有必要找到一种抽象和集中验证代码的方法,使对话框本身免于验证输入数据。此外,我想及时验证用户的输入,以拒绝或调整输入字符。
我将在本文中介绍的这些库已经发展了多年。附件文件包含处理输入验证的部分。
2 背景
在本章中,我将解释 .NET 中自定义输入验证的一些基础知识及其所有困难和问题。如果已经精通此主题并已经经历过自定义输入验证的头痛,可以跳过本章,继续阅读第 3 章中的解决方案。
2.1 在 Windows.Forms 中验证用户输入
名称最显著的事件是 Control.Validating
。当控件需要验证其内容时,会触发此事件。这通常发生在控件失去输入焦点时。但当包含控件的对话框即将关闭时,也可能触发此事件。
Validating
事件似乎是验证用户输入的“那个”事件。但仔细观察后会发现令人失望:
- 无法确定事件的原因。此外,如果事件是因为对话框即将关闭而触发,我们无法万无一失地检测关闭原因。这会导致几个问题:
- 如果某些情况需要与默认处理不同的处理,控件无法明确地保持(或失去)输入焦点。在无效输入时更改输入焦点要么总是允许,要么总是拒绝。
- 如果任何控件包含无效输入,对话框不能通过 [Esc] 或“取消”按钮关闭。当对话框的某些属性和相关控件巧妙设置时,可以通过“取消”按钮关闭对话框,但在对话框最终关闭之前,总是会显示一条关于无效输入的消息。该消息必须由实现者显示,因为无法检测到对话框即将关闭的原因。这是因为
Validating
事件在对话框关闭时触发的其他事件之前触发。该消息可以通过在“取消”按钮的Click
处理程序中设置的标志来抑制,但当用户点击对话框框架内的关闭按钮时,它不能轻易地被抑制。 - 需要为每个需要验证的控件处理
Validating
事件。这总是在对话框的代码隐藏文件中完成。因此,代码不能分离或转移。 - 输入无法“实时”验证。这意味着当用户输入字符时,不会触发 Validating 事件。只有当用户关闭对话框或选择另一个控件时,它才会触发。因此,不可能单独验证每个输入字符。由此可见,该事件不能用于检查输入字符是否为数字。它也不能用于修改输入,例如将小写字母更改为大写字母。
要验证用户当前输入到控件中的字符,Control.TextChanged
事件似乎是正确的地方。但在 TextChanged
事件中修改输入字符会导致无限递归(除了我们不知道文本中的哪个字符是当前输入的)。为了避免 TextChanged
事件的递归,必须定义一个标志来指示事件的原因。必须在事件处理程序中查询和设置此标志。对于每个需要以这种方式验证输入的控件,都必须单独完成此操作。
比 TextChanged
事件更好的即时验证解决方案是 KeyPress
事件。此事件在输入字符显示在输入行之前触发。抑制或更改输入字符不会导致无限递归。
但 TextChanged
事件也有一个很大的缺点:在许多情况下,需要根据之前已经输入的整个文本的上下文来验证输入字符。这意味着必须手动将输入字符插入到现有文本的正确位置。为此,有必要从控件的内容构建一个内部缓冲区,将输入字符插入其中(以验证整个文本)。就其本身而言,此解决方案成本高昂且容易出错(尤其是在与 Control.KeyDown
事件结合使用时,在某些情况下必须覆盖该事件以处理从未传递给 KeyPress
事件的 [Del] 键)。
最后但并非最不重要,Control.Leave
事件可以用于根据当前对话框状态验证输入文本。Leave
事件使程序员能够检测到将获得输入焦点的下一个控件。在某些情况下,即使当前文本无效,也可能允许移动到该控件。在其他情况下,如果输入文本无效,则可能不允许失去焦点。Leave
事件可以用于阻止焦点更改。
如果不需要即时验证每个输入字符,并且在无效输入时不需要拒绝失去输入焦点,那么最简单的方法是处理对话框的 Closing
事件。然而,即使是这个简单的解决方案也涉及到每个对话框的昂贵实现。最重要的是,Closing 事件允许检测关闭的原因并进行相应处理
protected override void OnClosing( CancelEventArgs e )
{
if( DialogResult != DialogResult.Cancel )
{
// Validate the input of all controls in user written
// method ValidateAllControls (this may display a message
// on invalid input):
//
if( !ValidateAllControls() )
e.Cancel = true;
}
base.OnClosing(e);
}
2.2 在 WPF 中验证用户输入
WPF 应用程序中用户输入验证的基本原理与 Windows.Forms 相同,尽管有更多方法可以将代码与 UI 分离。一些事件的名称不同(例如 TextInput
而不是 KeyPress
),但自定义验证的原理几乎相同,只是数据绑定提供了一个强大的选项来在 UI 之外进行验证。
WPF 的默认功能无法实现诸如即时抑制无效字符或自动填充部分完成的输入等花哨功能。实现这些优点会导致与 Windows.Forms 相同的昂贵工作。
即使在使用数据绑定时将验证代码外包,也无法集中实现。这必须为每个对话框(或者每个视图模型)重新完成。输入的验证可以收集在整个应用程序的单独模块中,但下一个应用程序需要新的实现。
2.3 结论
用户输入的验证并非易事,并且经常导致实现错误。当存在超出规范的更舒适输入行为的需求时,这一点尤其明显。这包括抑制无效字符、自动填充部分输入以及自动在大小写之间转换等功能。
通常采用的方法是实现继承自现有控件的新控件,以提高舒适性和安全性。结果是,一个单独的控件接受指定范围内的整数,另一个控件接受指定范围内的十进制小数,另一个控件允许在指定范围内输入日期或时间,另一个控件允许定义输入掩码等等。
这导致了许多问题。其中一些在此列举为例
- 控件必须为每个目标框架(WPF、Windows.Forms 等)重新实现。
- 控件必须为每个使用的第三方库控件重新实现。
- 许多控件无法重新实现,例如网格中的输入框(它们可能可继承,但网格不会使用它们)。
3 库
我将在这里介绍的验证框架以最小的努力解决了上述所有问题。我几年前开始用 C++ 开发该解决方案,并将其移植到 .NET。最近的扩展是为了支持 WPF(无论是否使用 MVVM 模式)。
3.1 验证器
基本的类层次结构最初受到了 Borland Pascal Turbo Vision 和 Borland C++ OWL 框架的验证类的启发。但这些框架的类有一些主要缺点:
- 它们不能在其开发的框架之外使用,并且
- 它们无法充分配置以涵盖除简单输入文本验证之外的更多用例。
我的验证框架的基本验证器(类 Validator
)支持以下由派生类专门化的功能:
- 部分输入文本的验证(这包括即时修改的可能性)
- 完全输入文本的验证
- 自定义错误消息的定义
- 允许输入的最小和最大字符数的合理性
Ganzer.dll 程序集中定义的验证器层次结构如下图所示:
Validator
类已经测试了以下属性:
- 如果指定了
ValidatorOptions.NeedsInput
(Validator
类的默认值),则空文本无效 - 如果未指定
ValidatorOptions.BlanksValid
(所有验证类的默认值),则仅包含空格的文本无效 - 如果文本长度小于
Validator.MinLength
或大于Validator.MaxLength
,则文本无效
对于所有其他特性,更专业的验证器派生自 Validator。框架中已包含的派生验证器涵盖了大多数应用程序的大多数需求。您将在下表中找到更详细的描述。
验证器的使用非常简单。验证器不一定用于用户输入的字符串。它也可以用于验证从文件或数据库中读取的字符串。为了解释验证器的主要用法,我将从一个控制台应用程序开始。
最重要的方法是 Validator.Validate()
,它必须用于验证字符串。这是一个示例:
string ReadNumberFromConsole()
{
String input = Console.ReadLine();
// Allow an integer from -100 to +100:
//
Validator validator = new NumberValidator(-100, 100);
validator.Validate(input);
return input;
}
上面列表中使用的 Validator.Validate()
的重载在输入无效时抛出 ValidatorException
类型的异常。如果这不适用于某些目的,则可以使用接受第二个 ValidatorException
类型参数的重载
string ReadNumberFromConsole()
{
String input = Console.ReadLine();
Validator validator = new NumberValidator(-100, 100);
ValidatorException x;
if( validator.Validate(input, out x) )
{
// Input is valid - return input:
//
return input;
}
// Input is not valid - display a message and return null:
//
Console.WriteLine(x.Message);
return null;
}
本文随附的 ConsoleValidatorTutorial 项目中展示了一种更常见的、使用验证器的输入方法。
private static string ReadString( string prompt, Validator validator )
{
Debug.Assert(!string.IsNullOrEmpty(prompt));
//
// This is left only if the input is valid:
//
for( ; ; )
{
try
{
Console.Write(prompt + ": ");
string input = Console.ReadLine();
if( validator != null )
validator.Validate(input);
return input;
}
catch( ValidatorException x )
{
//
// Using exception handling for controlling a
// workflow is not the "clean" way but it is
// much more readable for training purposes ;-)
//
Console.WriteLine(x.Message);
}
}
}
此方法用于输入和验证不同的数据。以下代码展示了 ConsoleValidatorTutorial 项目的另一部分(项目中还有更多使用其他验证器的示例):
private static void ReadAndDisplay( string prompt, Validator validator )
{
Console.WriteLine(Resources.StrTheInputText, ReadString(prompt, validator));
}
private static void DemonstrateValidator()
{
//
// The default constructor sets the option NeedsInput:
//
Validator validator = new Validator();
//
// Any input is valid that contains at least one character other
// than whitespace:
//
ReadAndDisplay(Resources.StrInputAnything, validator);
//
// Any input is valid that contains at least three characters:
//
validator.MinLength = 3;
ReadAndDisplay(Resources.StrInputAtLeastThreeChars, validator);
//
// Any input is valid that contains exactly five characters:
//
validator.MinLength = 5;
validator.MaxLength = 5;
ReadAndDisplay(Resources.StrInputExactlyFiveChars, validator);
//
// Any input is valid that contains exactly zero or five characters:
//
validator.Options = ValidatorOptions.None;
ReadAndDisplay(Resources.StrInputExactlyFiveCharsOrNothing, validator);
}
上面的代码解释了 Validator
类的一些基本功能。正如您在上面的图中看到的,有许多类扩展了验证行为。我将简要描述验证类及其用途
在 Ganzer.Wpf.dll 和 Ganzer.Windows.Forms.dll 程序集中,还有一些派生自 CompareValidator
的验证器。这些验证器引用来自其他控件的文本,并且专门用于 WPF 和 Windows.Forms。因此,它们不属于公共 Ganzer.dll 程序集。
也许在阅读下一章之前,您想了解更多关于各种验证类的可能性。附件文件中包含一个名为 ValidatorTests 的项目。您可以启动它并尝试各种输入和选项。
3.2 链接
为了方便地使用验证器,与其编写新的控件(派生自 TextBox
、ComboBox
等),不如使用一小组类,这些类只是将验证器链接到现有控件。我注意到大多数支持文本输入的控件(尤其是第三方库的控件)内部都使用简单的 TextBox
控件。通常很容易获取此控件,例如从 Infragistics 的 UltraGrid
等网格中获取。从现有链接继承以支持网格中的文本框比编写使用派生文本框的新网格要容易得多。
本章分为几个部分:WPF、Windows.Forms 和 MVVM。即使您对 WPF 不感兴趣,也应该学习本节,因为此处显示的所有基础知识在 Windows.Forms 中都是相同的,以后不会重复。
如果您只对 MVVM 感兴趣,可以跳过“WPF”和“Windows.Forms”部分。MVVM 中验证器与控件的链接确实与 WPF 相同,但由于数据绑定,这在 XAML 中通过其他类进行了封装。
3.2.1 WPF
第一个例子非常简单。一个带有单个输入行的对话框,提示用户输入客户编号:
该数字是一种虚构格式,旨在展示一些即时技巧,例如从小写字母转换为大写字母或自动填充所需文本。支持此功能的验证器是 PxPicValidator
。该名称是“Paradox Picture”的缩写。Paradox 是 Borland 公司的一款现已过时的数据库。但该数据库具有基于图片掩码的非常出色的输入功能。PxPicValidator
模拟了这一点。
图片字符的含义如下
字符 | 含义 |
& | 只允许大写字母(考虑当前文化设置) |
# | 只允许十进制数字 |
其他 | 原样 |
还有更多特殊字符可用。它们在 PxPicValidator.Picture
属性的代码文档中进行了描述。
回到对话框:传统上,程序员会在用户单击对话框的“确定”按钮时编写代码来验证输入。但是,如果您想实现自动填充总是相等的所需文本(所有不是 & 或 # 的字符),或者如果您想在输入时将小写字符更改为大写字符,您将有很多工作要做。
使用验证框架,我们只需将合适的验证器与输入框链接起来即可:
public partial class SimpleInputDialog : Window
{
private ValidatorLink _customerNumberLink;
public SimpleInputDialog()
{
// ... other code here
// Link the input box to the validator:
//
_customerNumberLink = new TextBoxLink(
/* A */ _edtCustomerNumber,
/* B */ new PxPicValidator("&&-DE/N-55.###/###", ValidatorOptions.NeedsInput | ValidatorOptions.AutoFill),
/* C */ LinkOptions.None);
}
// ... other methods here
}
解释
行 A:要链接验证器的控件名称(在设计器中指定)。
行 B:要链接控件的验证器。
行 C:在这里我们可以设置一些影响链接行为的选项。在这种情况下,我们不设置任何选项。
这就是验证输入所需做的一切。试试看,您会看到以下内容
- 首先,您只能输入字母。
- 小写字母在输入前两位时会自动转换为大写。
- 输入第二个字母后,直到点为止的所有后续文本都会自动填充。
- 在点后面,您只能输入十进制数字。
- 输入第三个数字后,斜杠会自动填充。
上面的代码中的验证器定义了 ValidatorOptions.NeedsInput
选项。这意味着空文本是无效的。用户必须输入与完整图片掩码匹配的文本。为了确保当用户单击“确定”按钮时进行验证,唯一需要做的就是处理按钮的 Click
事件或窗口的 Closing
事件:
private void _btnOK_Click( object sender, RoutedEventArgs e )
{
//
// Check whether the input is valid (a message box
// is shown automatically on invalid input):
//
if( _customerNumberLink.Validate() )
DialogResult = true;
}
在教程项目中,使用窗口的 Closing
事件来验证所有内容。在这两种情况下,结果都是相同的:如果输入无效,用户不能通过“确定”按钮关闭对话框。
就这样!没什么好做的了!需要验证多少个控件并不重要。每个要验证的控件都可以一次只用一行代码链接到合适的验证器。
当需要验证许多控件时,单独为每个链接调用 Validate()
可能很费力。为避免这种情况,可以使用 ValidatorLinkCollection
类。将每个链接插入此集合,然后验证集合而不是单独验证每个链接
public partial class SimpleInputDialog : Window
{
private ValidatorLinkCollection _validatorLinks = new ValidatorLinkCollection();
public SimpleInputDialog()
{
// ... other code here
// Link the controls to the validators:
//
_validatorLinks.Add(new TextBoxLink(
_edtCustomerNumber,
new PxPicValidator("&&-DE/N-55.###/###", ValidatorOptions.NeedsInput | ValidatorOptions.AutoFill),
LinkOptions.None));
// ... more controls here
}
// ... other methods here
private void _btnOK_Click( object sender, RoutedEventArgs e )
{
//
// Check whether all the input is valid (a message box
// is shown automatically on invalid input):
//
if( _validatorLinks.ValidateAll() )
DialogResult = true;
}
}
例如,教程项目包含一个更复杂的对话框,如下图所示:
所有数据都是虚构的,可能无法满足员工的实际要求,但这些数据集合展示了验证器和链接的一些优点
- 必须输入姓名。更重要的是:只要输入了姓名,用户就不能离开文本框(例外:如果“取消”按钮的属性
IsCancel
为true
,则始终允许移动到“取消”按钮)。 - 数字必须以“#/###-&&”格式输入。数字可以留空。
- 工资必须显示货币符号。我住在德国,因此图中符号是欧元(系统默认),但这是完全可配置的。当用户进入此输入行时,文本应不带货币符号显示,以便于编辑。当用户离开此输入行时,必须再次显示货币符号。输入不能为空,并且必须是负数(不能输入减号)。与“姓名”相同,此文本框在输入无效时不能离开。
- Code 是任意十六进制数。与“Name”相同,此文本框在输入无效时不能离开。
在设计器中构建对话框之后,剩下的就只有这些了:
public partial class ComplexInputDialog : Form
{
private ValidatorLinkCollection _validatorLinks = new ValidatorLinkCollection();
public ComplexInputDialog()
{
// ... other code
_validatorLinks.Add(new TextBoxLink(
_edtName,
new Validator(),
/* A */ LinkOptions.ForceValid));
_validatorLinks.Add(new TextBoxLink(
_edtNumber,
new PxPicValidator("#/###-&&"),
LinkOptions.ForceValid));
_validatorLinks.Add(new TextBoxLink(
_edtSalary,
/* B */ new NumberValidator(0m, decimal.MaxValue, "C", "G", ValidatorOptions.NeedsInput),
/* C */ _edtCode,
/* D */ LinkOptions.ForceValid | LinkOptions.DoFormat | LinkOptions.TextChangeFormats));
_validatorLinks.Add(new TextBoxLink(
_edtCode,
new FilterValidator("0-9a-fA-F", ValidatorOptions.NeedsInput),
LinkOptions.ForceValid));
}
private void _btnOK_Click( object sender, RoutedEventArgs e )
{
if( !_validatorLinks.ValidateAll() )
DialogResult = true;
}
}
解释
行 A:
ForceValid
选项强制用户进行有效输入。这意味着,只要输入无效,就不能离开控件。行 B:工资将根据输入焦点的状态进行格式化。可选的第三和第四个参数指定此验证器的显示和编辑格式。
行 C:我解释过用户总是可以移动到“取消”按钮。当按钮被标记为
IsCancel
时,这会隐式地得到保证。这里我们希望用户即使在输入工资无效时也可以移动到“代码”行(这与上述要求相悖,但它将演示链接的另一个特性)。为了实现这一点,可以指定一个或多个控件。这里是“代码”文本框。与“取消”按钮一样,用户可以在输入无效时将焦点从“工资”移动到“代码”。好的——我们为什么要这样做?嗯,可能有些情况需要这种行为。例如,用户可以在其中输入文件名的一行输入。输入的文件名必须有效且不能为空。输入行附近的一个按钮会打开一个对话框,用户可以在其中选择文件名。即使输入文件名无效,用户也应该能够移动到该按钮。
行 D:工资的格式将取决于控件是否获得输入焦点。
DoFormat
选项强制链接在控件的输入焦点更改时格式化文本。TextChangeFormats
选项会导致在文本因用户输入以外的其他操作(例如,用户粘贴剪贴板内容时)而更改时进行格式化。这两个选项对于DateTimeValidator
和NumberValidator
的实例都很有意义。
本章到此为止。您可能会认为上面复杂的对话框实在太简单了,不能称之为“复杂”。嗯,您说得对。但由于本文的篇幅,我决定用一个更小的示例来实现目标。附件中包含一个名为 ValidatorTests 的项目,您可以在其中找到一个真正复杂的 UI,其中包含多个选项卡页面和许多需要验证的控件。
3.2.2 Windows.Forms
Windows.Forms 的链接类与 WPF 的链接类工作方式相同。它们的命名甚至相同。差异微乎其微
- Windows.Forms 不支持
LinkOptions.ShowMessageOnBinding
选项,因为绑定在这里不支持所需的错误处理类型。因此,Windows.Forms 的链接不支持直接绑定。相反,可以使用ErrorProvider
实例来通知错误,而无需显示消息框。 LinkOptions.HandleValidating
选项仅适用于 Windows.Forms,因为 WPF 中不提供Control.Validating
事件。- 为了允许在输入无效时将输入焦点移动到“取消”按钮,在 Windows.Forms 中必须将
CausesValidation
属性设置为false
(没有IsCancel
属性)。这可以为每个不应阻止在无效输入时更改焦点的控件完成。 - Windows.Forms 中没有验证规则。因此,Windows.Forms 中不存在与
ValidatorValidationRule
类(尚未解释)等效的类。 - 命名空间是
Ganzer.Windows.Forms.Validation
,而不是Ganzer.Wpf.Validation
。
Windows.Forms 处理“确定”按钮的方式与 WPF 不同。在 WPF 中,需要处理“确定”按钮的 Click
事件来设置窗口的 DialogResult
属性。在 Windows.Forms 中,如果按钮的 DialogResult
属性设置为 DialogResult.OK
,这会自动完成。因此,最好不要创建 Click
处理程序,并在 Form.Closing
事件中进行最终验证:
protected override void OnClosing( CancelEventArgs e )
{
if( DialogResult == DialogResult.Cancel )
base.OnClosing(e);
else
{
//
// Check whether the input is valid (a message box
// is shown automatically on invalid input):
//
if( _customerNumberLink.Validate() )
{
base.OnClosing(e);
if( !e.Cancel )
TransferData(TransferDirection.ToData);
}
else
{
DialogResult = 0;
e.Cancel = true;
}
}
}
所有其他都与 WPF 相同。因此,在此处重复源代码是多余的。FormsValidatorTutorial 项目包含 Windows.Forms 的所有源代码。它与 WPF 的代码相同(就验证代码而言)。
3.2.3 MVVM
在本节中,我将解释如何在实现 MVVM 模式的应用程序中使用验证类。
重要的是您要知道,有很多方法可以使用验证类和链接而不会破坏模式。我在这里解释的方法只是我最喜欢的方法。
3.2.3.1 基础知识
我们想构建与上面“WPF”部分中使用的相同简单示例对话框:
源代码可以在 WpfValidatorTutorial 项目中找到。这是包含上面“WPF”部分示例对话框的相同项目。
窗口只包含按钮和一个类型为 ContentControl
的内容呈现器。它的 Content
属性绑定到窗口的当前绑定(在 XAML 中):
<ContentControl Name="_content" Content="{Binding}" Focusable="False" />
窗口的资源包含以下模板(在 XAML 文件中):
<DataTemplate DataType="{x:Type mvvm:SimpleDialogViewModel}">
<mvvm:SimpleDialogView />
</DataTemplate>
SimpleDialogView
是一个自定义控件,包含带标签和输入行的组框。我们必须将窗口的 DataContext
属性设置为 SimpleDialogViewModel
的实例
private void _btnShowSimpleMvvmDialog_Click( object sender, RoutedEventArgs e )
{
GenericDialog dialog = new GenericDialog();
dialog.Owner = this;
dialog.DataContext = new SimpleDialogViewModel();
dialog.ShowDialog();
}
现在,我们来看看视图模型
public class SimpleDialogViewModel : BasicViewModel
{
private string _customerNumber;
public string CustomerNumber
{
get
{
return _customerNumber;
}
set
{
//
// Instead of using the following lines:
//
// if( value == null )
// throw new ArgumentNullException("value");
// if( value.Length == 0 )
// throw new ArgumentException("Invalid Length.", "value");
// ... and further more...
//
// we does simply use this:
//
_customerNumberValidator.Validate(value);
if( _customerNumber == value )
return;
_customerNumber = value;
OnPropertyChanged("CustomerNumber");
}
}
private static Validator _customerNumberValidator =
new PxPicValidator("&&-DE/N-55.###/###",
ValidatorOptions.NeedsInput | ValidatorOptions.AutoFill);
public static Validator CustomerNumberValidator
{
get
{
return _customerNumberValidator;
}
}
public SimpleDialogViewModel()
: base(Resources.StrSimpleDialogViewModelName)
{
}
}
这是一个非常小的视图模型,因为它只包含数据 CustomerNumber
。通过属性设置器中的注释,您可以看到可以使用合适的验证器来检查要设置的数字是否有效。如果数字无效,_customerNumberValidator.Validate(value)
将抛出带有适当消息的异常。验证器可以是静态的,因为所有实例都必须以相同的方式验证其客户编号。
到目前为止一切顺利,但是视图如何将验证器绑定到其输入框呢?
好的,这是通过静态类 ValidationService
完成的。它定义了一些附加属性,用于将验证器绑定到控件。当验证器绑定时,会隐式创建 ValidatorLink
对象。感兴趣的行是定义 TextBox
的行
<UserControl x:Class="WpfValidatorTutorial.Mvvm.SimpleDialogView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:gwv="clr-namespace:Ganzer.Wpf.Validation;assembly=Ganzer.Wpf"
mc:Ignorable="d">
<Grid>
<GroupBox Header="Customer Number"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Label Grid.Row="0"
Content="_Input the Number in the Format &&-DE/N-55.###/###:"
Target="{Binding ElementName=_edtCustomerNumber}"
Margin="3,3,3,0"
Padding="0" />
<TextBox Name="_edtCustomerNumber"
Text="{Binding CustomerNumber}"
gwv:ValidationService.Validator="{Binding CustomerNumberValidator}"
gwv:ValidationService.Options="ShowMessageOnBinding"
Grid.Row="1"
MinWidth="250"
Margin="3" />
</Grid>
</GroupBox>
</Grid>
</UserControl>
选项设置为 LinkOptions.ShowMessageOnBinding
。这对于显示消息框是必要的。
简单提一下:如果未设置此选项(并且控件已绑定),则链接将把绑定标记为无效,并在控件周围绘制红色边框以通知用户错误。要显示错误消息,我们可以使用 WPF 集成标准——例如这样设置工具提示:
<Style TargetType="TextBox">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
在这种情况下,我们既不需要消息框,也不需要 ShowMessageOnBinding
选项。
回到主题:上面的示例展示了如何在视图模型中定义验证器。这样做的好处是,属性和输入的验证总是相同的,并且在算法更改时不会意外地有所不同。另一方面,也可以在 XAML 文件的资源中定义验证器。在这一点上,您完全可以自由选择。
正如您在 XAML 文件中看到的,一行代码就足以完成带有默认绑定的窗口的完整验证。但这仅适用于需要及时更新绑定视图模型的非模型窗口。模态对话框必须以另一种方式处理向绑定属性的传输。当用户取消模态对话框时,绑定视图模型的任何属性都不能更改。只有当用户单击“确定”按钮时,完整的更改数据才必须提交。这种行为可以通过使用验证组来实现。
3.2.3.2 验证组
在上述所有 WPF 和 Windows.Forms 练习中,都使用 ValidatorLinkCollection
来验证所有输入,然后将它们传输到底层数据的属性中。使用 MVVM,我们必须做其他事情。首先,必须阻止自动提交。这只需通过绑定组即可完成。教程通用对话框的 XAML 文件包含以下代码:
<Window.BindingGroup>
<BindingGroup />
</Window.BindingGroup>
OK 按钮的 Click
事件在对话框的代码隐藏文件中处理
private void _btnOK_Click( object sender, RoutedEventArgs e )
{
//
// Check whether the input is valid:
//
if( ValidationService.ValidateGroup(_content) )
{
BindingGroup.CommitEdit();
DialogResult = true;
}
}
ValidationService
类定义了静态方法 ValidateGroup()
。第一个参数定义了要开始验证的控件。在这种情况下,它是名为“_content”的内容呈现器。上面的代码验证了所有是内容呈现器的子控件并链接到默认验证组中的验证器的控件。
可以通过附加属性 ValidationService.Group
为链接控件定义一个验证组。在上面的示例中,我们没有设置任何组。因此,对于绑定的控件,使用了默认组。
ValidateGroup()
的另一个重载允许将组指定为第二个参数。这可以用于验证特定组的控件。验证组的显式定义对于复杂的 UI 很有意义,其中一些控件必须不时地独立于其他控件进行验证。
3.2.3.3 更多复杂性
教程项目还包含“WPF”部分中描述的更复杂的对话框
插入到通用对话框中的视图的 XAML 代码包含一个样式,该样式将无效输入行的工具提示设置为错误消息(这由链接设置)。因此,无需使用消息框来显示错误消息。为了演示这一点,选项不包含 ForceValid
,并且始终允许移动输入焦点。所有其他行为与“WPF”部分中的示例相同。
以下代码显示了 XAML 文件。完整的验证是通过在文本框中设置 ValidationService
类的附加属性的行完成的:
<UserControl x:Class="WpfValidatorTutorial.Mvvm.ComplexDialogView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:gwv="clr-namespace:Ganzer.Wpf.Validation;assembly=Ganzer.Wpf"
mc:Ignorable="d">
<UserControl.Resources>
<Style TargetType="TextBox">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
<Grid>
<GroupBox Header="Employee"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label Grid.Column="0"
Grid.Row="0"
Content="_Name:"
Target="{Binding ElementName=_edtName}"
Margin="3,5,3,0"
Padding="0" />
<TextBox Grid.Column="1"
Grid.Row="0"
Name="_edtName"
Text="{Binding Name}"
gwv:ValidationService.Validator="{Binding NameValidator}"
Width="250"
Margin="3" />
<Label Grid.Column="0"
Grid.Row="1"
Content="N_umber:"
Target="{Binding ElementName=_edtNumber}"
Margin="3,5,3,0"
Padding="0" />
<TextBox Grid.Column="1"
Grid.Row="1"
Name="_edtNumber"
Text="{Binding Number}"
gwv:ValidationService.Validator="{Binding NumberValidator}"
Width="250"
Margin="3" />
<Label Grid.Column="0"
Grid.Row="2"
Content="_Salary:"
Target="{Binding ElementName=_edtSalary}"
Margin="3,5,3,0"
Padding="0" />
<TextBox Grid.Column="1"
Grid.Row="2"
Name="_edtSalary"
Text="{Binding Salary}"
gwv:ValidationService.Validator="{Binding SalaryValidator}"
gwv:ValidationService.Options="DoFormat,TextChangeFormats"
TextAlignment="Right"
Width="250"
Margin="3" />
<Label Grid.Column="0"
Grid.Row="3"
Content="_Code:"
Target="{Binding ElementName=_edtCode}"
Margin="3,5,3,0"
Padding="0" />
<TextBox Grid.Column="1"
Grid.Row="3"
Name="_edtCode"
Text="{Binding HexCode}"
gwv:ValidationService.Validator="{Binding HexCodeValidator}"
Width="250"
Margin="3" />
</Grid>
</GroupBox>
</Grid>
</UserControl>
当然,验证器必须在视图模型(或资源中)创建,但我认为“更简单”几乎是不可能的。
3.2.3.4 验证规则
您也可以将验证器与验证规则一起使用。启用验证器使用的规则是 ValidatorValidationRule
,它派生自 ValidationRule
。
在此解释中,视图模型类可以命名为 NumberViewModel
,并且可以包含属性 Counter
以及静态属性 CounterValidator
。由于验证规则无法实现依赖属性,因此有必要在视图模型中将属性 CounterValidator
定义为静态(或在资源中创建验证器)。XAML 代码可能如下所示:
<TextBox gwv:ValidationService.Validator="{Binding.CounterValidator}"
Name="_edtCounter"
TextAlignment="Right"
Margin="0,3,0,0">
<Binding Path="Counter" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<gwv:ValidatorValidationRule Validator="{x:Static local:NumberViewModel.CounterValidator}" />
</Binding.ValidationRules>
</Binding>
</TextBox>
3.3 自定义
验证器和链接都可以通过许多选项进行自定义。我认为在此处描述所有可能性并不明智,因为代码已妥善注释——我希望 。相反,我将概述如何实现自己的验证器和链接。
3.3.1 验证器
实现一个新的验证器并不困难。如果需要,有三种方法可以被覆盖
DoValidation()
:这是主要的验证方法,由所有Validator.Validate()
重载调用。请注意,您不应在此处在无效输入时 抛出 异常。相反,将 out 参数设置为描述错误的异常。DoInputValidation()
:此方法由Validator.IsValidInput()
调用,用于部分输入。给定文本包含当前输入,如果需要,可以修改。这方面的示例可以在FilterValidator
、PxPicValidator
或ListValidator
等许多类中找到。DoFormatText()
:当文本需要格式化以用于显示或编辑目的时,此方法由Validator.FormatText()
调用。这方面的示例可以在DateTimeValidator
或NumberValidator
类中找到。
3.3.2 链接
创建新链接比创建新验证器复杂得多。而且 WPF 和 Windows.Forms 的工作方式非常不同。因此,很难给您一些通用的指导。最好的解决方案可能是探索现有链接的代码以了解它们的工作原理。
我试图找到最大的共同点,但是从控件获取/设置文本的方式差异极大(比较 TextBoxLink
、ComboBoxLink
、NumericUpDownLink
和 DateTimePickerLink
的实现)。因此,基本 ValidatorLink
类中有许多抽象方法和属性必须由继承者覆盖。在 WPF 中,还必须考虑属性可以绑定。如果忽略这一点,绑定将导致不可用的结果。
因此,我邀请您研究源代码及其文档。
如果您真诚地有兴趣了解如何为 Infragistics 的 UltraGrid
(Windows.Forms) 实现一个功能齐全的验证器链接,我可以提供代码。我之前在一个项目上已经这样做了。它是用 Visual Basic 实现的,注释是德语的,但我认为这无关紧要。您可以在您的项目中使用它,或者您可以简单地研究它以了解如何链接此类复杂的控件。最终它比第一眼看起来更容易。
3.4 已知问题
- 这些库已经发展了好几年。因此,它们可能包含一些不同的编码和文档风格。
RegExValidator
运行良好,但没有像PxPicValidator
那样实现自动填充。这是因为正则表达式比 Paradox 图片掩码更难解析,而且我还没有找到足够的时间来实现这一点。(如果您中有人已经知道如何解析正则表达式,并且能够实现DoInputValidation()
方法中的自动化,我将不胜感激。这在实践中如何实现,可以在PxPicValidator
类中查找。)- Windows.Forms 中的链接不考虑数据绑定。我目前没有理由实现它。在一些实际项目中,我发现这并不重要。也许您会发现某些情况下,当控件在 Windows.Forms 中绑定时,链接无法正常工作。
- 链接类尚未完全重构。这意味着:存在一些重复的代码片段,并且可能还有更好的继承方式。坦率地说,我对当前的解决方案不满意。我还没有更改它,因为它运行正常
。
- 链接不支持 ASP。您可以在代码隐藏文件中直接使用验证器类来验证输入,但我不是 Web 开发人员,只是不知道如何为 ASP 编写功能链接。也许没有必要,因为 ASP 已集成的验证行为运行良好。
- 我从未测试过带有 Silverlight 的 WPF 验证链接。我不知道它是否有效。
最后但并非最不重要:如果您认为这些库很有用,并且有人寻求协作,我将很高兴分享这个项目(也许在 CodePlex 上)。我对此还没有经验。因此,我需要一些反馈和帮助。