WPF提供了能与数据绑定系统紧密协作的验证功能。提供了两种方法用于捕获非法值:
1、可在数据对象中引发错误。
可以在设置属性时抛出异常,通常WPF会忽略所有在设置属性时抛出的异常,但可以进行配置,从而显示更有帮助的可视化指示。另一种选择是在自定义的数据类中实现 INotifyDataErrorInfo 或 IDataErrorInfo 接口,从而可得到指示错误的功能而不会抛出异常。
2、可在绑定级别上定义验证。
这种方法可获得使用相同验证的灵活性,而不必考虑使用的是哪个控件。更好的是,因为是在不同类中定义验证,可以很容易的在存储类似数据类型的多个绑定中重用验证。
错误模板
错误模板使用的是装饰层,装饰层是位于普通窗口内容之上的绘图层。使用装饰层,可添加可视化装饰来指示错误,而不用替换控件背后的控件模板或改变窗口的布局。文本框的标准错误模板通过在相应文本框的上面添加红色的Border元素来指示发生了错误。可使用错误模板添加其他细节。
在数据对象中引发错误
我这里分别尝试了 IDataErrorInfo 与 INotifyDataErrorInfo 接口,这俩需要分别对应使用
IDataErrorInfo 接口定义了Error字段,这个字段在结构有多个字段时,难以映射,于是在[]下标下实现逻辑。
INotifyDataErrorInfo 接口需要实现 HasError,GetErrors 两个函数,并需要在属性的set 访问器内实现校验逻辑,并管理Error信息。
总体来说,在数据对象中引发错误不是一个好选择,验证逻辑硬编码在数据对象中,不利于复用。
在绑定级别上定义验证
在绑定级别上定义验证是针对于数据类型自定义验证规则,可以对同一种数据类型进行复用。自定义验证规则需要继承自 ValidationRule 类,需要实现 Validate 函数,在其中完成自定义验证内容。
public class PriceRule : ValidationRule
{
private decimal min = 0;
private decimal max = decimal.MaxValue;
public decimal Min
{
get => min;
set => min = value;
}
public decimal Max
{
get => max;
set => max = value;
}
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
decimal price = 0;
try
{
if (((string)value).Length > 0)
{
price = Decimal.Parse((string)value, NumberStyles.Any, cultureInfo);
}
}
catch
{
return new ValidationResult(false, "Illegal characters.");
}
if (price < min || price > max)
{
return new ValidationResult(false, "Not in the range " + Min + " to " + Max + ".");
}
else
{
return new ValidationResult(true, null);
}
}
}
验证多个值
如果需要执行对两个或更多个绑定值的验证,可以通过 BindingGroup 来实现,将需要校验的多个控件放置于同一个容器中,在容器级别应用验证规则,需要通过事件主动触发验证,通常是子组件失去焦点时。
Person ID:
Person Name:
public class Person : ViewModelBase
{
private string name = string.Empty;
public string Name { get=>name; set=> SetProperty(ref name, value); }
private string id = string.Empty;
public string ID { get => id; set => SetProperty(ref id, value); }
}
public class PersonRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
BindingGroup bindingGroup = (BindingGroup)value;
Person? person = bindingGroup.Items[0] as Person;
string name = (string)bindingGroup.GetValue(person, "Name");
string id = (string)bindingGroup.GetValue(person, "ID");
if ((name == "") && (id == ""))
{
return new ValidationResult(false, "A Person requires a ID or Name.");
}
else
{
return new ValidationResult(true, null);
}
}
}
下面贴出完整的测试代码:
MainWindow.xaml
PersonAge:
PersonName:
PersonPrice:
Person ID:
Person Name:
MainWindow.xaml.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace TestValidation;
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual bool SetProperty(ref T member, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer.Default.Equals(member, value))
{
return false;
}
member = value;
OnPropertyChanged(propertyName);
return true;
}
}
public class PersonAge : ViewModelBase, IDataErrorInfo
{
public string this[string columnName]
{
get
{
if(columnName == "Age")
{
if(Age < 0 || Age > 150)
{
return "Age must not be less than 0 or greater than 150.";
}
}
return null;
}
}
private int age;
public int Age { get => age; set => SetProperty(ref age, value); }
public string Error => null;
}
public class PersonName : ViewModelBase, INotifyDataErrorInfo
{
private string name = string.Empty;
public string Name
{
get => name;
set
{
bool valid = true;
foreach (char c in value)
{
if (!char.IsLetterOrDigit(c))
{
valid = false;
break;
}
}
if(!valid)
{
List errors = new List ();
errors.Add("The Name can only contain letters and numbers.");
SetErrors("Name", errors);
}
else
{
ClearErrors("Name");
}
SetProperty(ref name, value);
}
}
private Dictionary> errors = new Dictionary>();
private void SetErrors(string propertyName, List propertyErrors)
{
errors.Remove(propertyName);
errors.Add(propertyName, propertyErrors);
if (ErrorsChanged != null)
ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
}
private void ClearErrors(string propertyName)
{
errors.Remove(propertyName);
if(ErrorsChanged != null)
ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
}
public bool HasErrors
{
get { return errors.Count > 0; }
}
public event EventHandler? ErrorsChanged;
public IEnumerable GetErrors(string? propertyName)
{
if(!string.IsNullOrEmpty(propertyName) && errors.ContainsKey(propertyName))
{
return errors[propertyName];
}
return new List();
}
}
public class PersonPrice : ViewModelBase, INotifyPropertyChanged
{
private decimal price = 0;
public decimal Price
{
get => price;
set
{
SetProperty(ref price, value);
}
}
}
public class PriceRule : ValidationRule
{
private decimal min = 0;
private decimal max = decimal.MaxValue;
public decimal Min
{
get => min;
set => min = value;
}
public decimal Max
{
get => max;
set => max = value;
}
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
decimal price = 0;
try
{
if (((string)value).Length > 0)
{
price = Decimal.Parse((string)value, NumberStyles.Any, cultureInfo);
}
}
catch
{
return new ValidationResult(false, "Illegal characters.");
}
if (price < min || price > max)
{
return new ValidationResult(false, "Not in the range " + Min + " to " + Max + ".");
}
else
{
return new ValidationResult(true, null);
}
}
}
public class Person : ViewModelBase
{
private string name = string.Empty;
public string Name { get=>name; set=> SetProperty(ref name, value); }
private string id = string.Empty;
public string ID { get => id; set => SetProperty(ref id, value); }
}
public class PersonRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
BindingGroup bindingGroup = (BindingGroup)value;
Person? person = bindingGroup.Items[0] as Person;
string name = (string)bindingGroup.GetValue(person, "Name");
string id = (string)bindingGroup.GetValue(person, "ID");
if ((name == "") && (id == ""))
{
return new ValidationResult(false, "A Person requires a ID or Name.");
}
else
{
return new ValidationResult(true, null);
}
}
}
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
myGrid.DataContext = this;
}
public PersonAge PersonAge { get; set; } = new PersonAge();
public PersonName PersonName { get; set; } = new PersonName();
public PersonPrice PersonPrice { get; set; } = new PersonPrice();
public Person Person { get; set; } = new Person();
private void Grid_LostFocus(object sender, RoutedEventArgs e)
{
personValidation.CommitEdit();
}
}