WPF数据校验

在WPF中,有许多数据校验方法,ValidationRule、IDataErrorInfo和DataAnnotations是三种最常用的方式,本文对其用法做一个简单介绍。

在实际项目开发中,推荐使用IDataErrorInfo + DataAnnotations的方案!

方法1:ValidationRule

这种方法适合在XAML中指定数据检验规则,但需要在后台代码中编写检验规则(继承自ValidationRule的子类)。

  • MainWindow.xaml
    • 如果在代码中修改属性,默认不做数据校验,添加ValidatesOnTargetUpdated="True"可以开启。
    • 数据非法时默认不会激发事件,给Binding添加NotifyOnValidationError="True"可以激发事件,事件在TextBox中指定Validation.Error="TextBox_Error"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="WPF数据校验" Width="300" Height="250">
<StackPanel Orientation="Vertical">
<TextBox Margin="5" Validation.Error="TextBox_Error">
<TextBox.Text>
<Binding Path="Score" UpdateSourceTrigger="PropertyChanged" NotifyOnValidationError="True">
<Binding.ValidationRules>
<local:ScoreValidateRule ValidatesOnTargetUpdated="True"/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Button Margin="5" x:Name="btnShowAge" Click="btnShowAge_Click">显示分数</Button>
<Button Margin="5" x:Name="btnChangeAge" Click="btnChangeAge_Click">修改分数</Button>
</StackPanel>
</Window>
  • MainWindow.xaml.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System.Windows;
namespace WpfApp1 {
public partial class MainWindow : Window {
private ViewModel vm = null;
public MainWindow() {
InitializeComponent();
vm = new ViewModel() { Name = "小明", Score = 25 };
DataContext = vm;
}
private void btnShowAge_Click(object sender, RoutedEventArgs e) {
MessageBox.Show(vm.Score.ToString());
}
// 在代码中修改属性值
private void btnChangeAge_Click(object sender, RoutedEventArgs e) {
vm.Score = 120;
}
// 数据校验失败时的事件处理器
private void TextBox_Error(object sender, System.Windows.Controls.ValidationErrorEventArgs e) {
MessageBox.Show("Score非法!");
}
}
}
  • ViewModel.cs
    • [AddINotifyPropertyChangedInterface]来自NuGet包PropertyChanged.Fody,可以为属性自动添加变化通知,非常实用!
    • 实现自定义校验规则时,需要实现其Validate()方法,参数value表示待校验的数据,传回是否成功的标识符和失败时的错误信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using PropertyChanged;
using System;
using System.Globalization;
using System.Windows.Controls;
namespace WpfApp1 {
[AddINotifyPropertyChangedInterface]
internal class ViewModel {
public String Name { get; set; }
public int Score { get; set; }
}

public class ScoreValidateRule : ValidationRule {
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
int score = 0;
if (int.TryParse(value.ToString(), out score))
if (score < 0 || score > 100)
return new ValidationResult(false, "分数非法");
return new ValidationResult(true, null);
}
}
}

方法2:IDataErrorInfo

  • MainWindow.xaml
    • 当输入数据非法时,如果要显示红色边框,需要给Binding设置ValidatesOnDataErrors=True
    • 同方法1一样,设置NotifyOnValidationError=True可以激发数据校验失败事件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="WPF数据校验" Width="300" Height="250">
<StackPanel Orientation="Vertical">
<TextBox Margin="5" Validation.Error="TextBox_Error"
Text="{Binding Score, UpdateSourceTrigger=PropertyChanged,
ValidatesOnDataErrors=True, NotifyOnValidationError=True}">
</TextBox>
<Button Margin="5" x:Name="btnShowAge" Click="btnShowAge_Click">显示分数</Button>
<Button Margin="5" x:Name="btnChangeAge" Click="btnChangeAge_Click">修改分数</Button>
</StackPanel>
</Window>
  • MainWindow.xaml.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System.Windows;
namespace WpfApp1 {
public partial class MainWindow : Window {
private ViewModel vm = null;
public MainWindow() {
InitializeComponent();
vm = new ViewModel() { Name = "小明", Score = 25 };
DataContext = vm;
}
private void btnShowAge_Click(object sender, RoutedEventArgs e) {
MessageBox.Show(vm.Score.ToString());
}
private void btnChangeAge_Click(object sender, RoutedEventArgs e) {
vm.Score = 120;
}
private void TextBox_Error(object sender, System.Windows.Controls.ValidationErrorEventArgs e) {
MessageBox.Show(vm["Score"]);
}
}
}
  • ViewModel.cs
    • 当一个ViewModel需要进行数据校验时,它必须实现IDataErrorInfo接口,该接口包含一个属性Error和一个[]符号重载。
    • []符号重载代码中,需要对各属性进行合法性校验,随着属性数量的增多,该代码将非常繁琐,这也是该方法的一个弊端,方法3可以解决该问题。
    • 如果上述[]符号重载代码最终返回null,则表示该属性值校验成功;否则校验失败,会在控件边缘显示红色边框。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using PropertyChanged;
using System;
using System.ComponentModel;
namespace WpfApp1 {
[AddINotifyPropertyChangedInterface]
public class ViewModel : IDataErrorInfo {
public String Name { get; set; }
public int Score { get; set; }
#region 数据校验
public string Error => "";
public string this[string columnName] {
get {
if (columnName == "Score") {
if (Score < 0 || Score > 100) {
return "Score必须在1~100之间!";
}
}
return string.Empty;
}
}
#endregion
}
}

方法3:DataAnnotations

该方法最大的特点是:在ViewModel的各属性上通过标注添加各种校验规则。

通过Validator.TryValidateObject()方法进行属性的数据校验,可以返回错误信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
using PropertyChanged;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;

namespace ConsoleApp1 {
class Program {
static void Main(string[] args) {
Person person = new Person() {
Name = "",
Email = "aaaa",
Age = 222,
Phone = "1111",
};
var result = ValidatetionHelper.IsValid(person);
if (!result.IsVaild) {
foreach (ErrorMember errormember in result.ErrorMembers) {
Console.WriteLine($"{errormember.ErrorMemberName}:{errormember.ErrorMessage}");
}
}
Console.ReadLine();
}
}

//实现一个Person类,里面包含几个简单的属性,然后指定几个Attribute
[AddINotifyPropertyChangedInterface]
public class Person {
[Required(ErrorMessage = "{0}必须填写,不能为空")]
[DisplayName("姓名")]
public string Name { get; set; }

[Required(ErrorMessage = "{0}必须填写,不能为空")]
[RegularExpression(@"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}", ErrorMessage = "{0}邮件格式不正确")]
public string Email { get; set; }

[Required(ErrorMessage = "{0}必须填写,不能为空")]
[Range(18, 100, ErrorMessage = "{0}年满18岁小于100岁方可申请")]
public int Age { get; set; }

[Required(ErrorMessage = "{0}手机号不能为空")]
[StringLength(11, MinimumLength = 11, ErrorMessage = "{0}请输入正确的手机号")]
public string Phone { get; set; }
}
}

以下是进行ViewModel数据校验的工具类,核心是使用Validator.TryValidateObject()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class ValidatetionHelper {
public static ValidResult IsValid(object value) {
ValidResult result = new ValidResult();
try {
var validationContext = new ValidationContext(value);
var results = new List<ValidationResult>();
var isValid = Validator.TryValidateObject(value, validationContext, results, true);

if (!isValid) {
result.IsVaild = false;
result.ErrorMembers = new List<ErrorMember>();
foreach (var item in results) {
result.ErrorMembers.Add(new ErrorMember() {
ErrorMessage = item.ErrorMessage,
ErrorMemberName = item.MemberNames.FirstOrDefault()
});
}
}
else {
result.IsVaild = true;
}
}
catch (Exception ex) {
result.IsVaild = false;
result.ErrorMembers = new List<ErrorMember>();
result.ErrorMembers.Add(new ErrorMember() {
ErrorMessage = ex.Message,
ErrorMemberName = "Internal error"
});

}
return result;
}
}
public class ValidResult {
public List<ErrorMember> ErrorMembers { get; set; }
public bool IsVaild { get; set; }
}
public class ErrorMember {
public string ErrorMessage { get; set; }
public string ErrorMemberName { get; set; }
}

方法4:IDataErrorInfo + DataAnnotations

在本方法中,MainWindow.xamlMainWindow.xaml.cs同方法3的代码。

相比方法2,本方法的唯一差别体现在ViewModel.cs[]符号重载中,这里使用DataAnnotations的数据校验方式,替换了方法2中的手动编写数据校验逻辑!领会这一点很关键。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using PropertyChanged;
using System;
using System.ComponentModel.DataAnnotations;

namespace WpfApp1 {
[AddINotifyPropertyChangedInterface]
public class ViewModel : PropertyValidateModel {
public String Name { get; set; }

[Range(0, 100, ErrorMessage = "Score必须在1~100之间!")]
public int Score { get; set; }
}

public abstract class PropertyValidateModel : IDataErrorInfo {
public string Error { get { return null; } }
public string this[string columnName] {
get {
var validationResults = new List<ValidationResult>();
if (Validator.TryValidateProperty(
GetType().GetProperty(columnName).GetValue(this),
new ValidationContext(this) { MemberName = columnName },
validationResults))
return null;
return validationResults.First().ErrorMessage;
}
}
}
}

方法5:CommunityToolkit.MVVM框架中的数据校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ViewModel : ObservableValidator {
private string name = "小明";
[MaxLength(10, ErrorMessage = "名字长度不能超过10")]
public string Name {
get => name;
set => SetProperty(ref name, value, true);
}

private RelayCommand<object> _checkDataCommand;
public RelayCommand<object> CheckDataCommand => _checkDataCommand ?? (_checkDataCommand = new RelayCommand<object>(CheckData));

public void CheckData(object parameter) {
ValidateAllProperties();
if (HasErrors) {
string res = string.Join(Environment.NewLine, GetErrors());
MessageBox.Show(res);
}
}
  • ValidateAllProperties()可以校验所有属性,如果有属性校验失败,HasErrors会被置为false,通过GetErrors()方法可以拿到所有的错误信息。
  • 如果希望属性发生变化时就校验,可以向SetProperty()的第三个参数传true

评论