WPF提供了能与数据绑定系统紧密协作的验证功能。提供了两种方法用于捕获非法值:
1、可在数据对象中引发错误。
可以在设置属性时抛出异常,通常WPF会忽略所有在设置属性时抛出的异常,但可以进行配置,从而显示更有帮助的可视化指示。另一种选择是在自定义的数据类中实现 INotifyDataErrorInfo 或 IDataErrorInfo 接口,从而可得到指示错误的功能而不会抛出异常。
2、可在绑定级别上定义验证。
这种方法可获得使用相同验证的灵活性,而不必考虑使用的是哪个控件。更好的是,因为是在不同类中定义验证,可以很容易的在存储类似数据类型的多个绑定中重用验证。
错误模板
错误模板使用的是装饰层,装饰层是位于普通窗口内容之上的绘图层。使用装饰层,可添加可视化装饰来指示错误,而不用替换控件背后的控件模板或改变窗口的布局。文本框的标准错误模板通过在相应文本框的上面添加红色的Border元素来指示发生了错误。可使用错误模板添加其他细节。
cs
<Style TargetType="{x:Type TextBox}">
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<DockPanel LastChildFill="True">
<TextBlock DockPanel.Dock="Right" Foreground="Red" FontSize="14" FontWeight="Bold"
ToolTip="{Binding ElementName=adornerPlaceholder, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}">*</TextBlock>
<Border BorderBrush="Green" BorderThickness="1">
<AdornedElementPlaceholder Name="adornerPlaceholder"></AdornedElementPlaceholder>
</Border>
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
<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>
在数据对象中引发错误
我这里分别尝试了 IDataErrorInfo 与 INotifyDataErrorInfo 接口,这俩需要分别对应使用 <DataErrorValidationRule/> 与 <NotifyDataErrorValidationRule/> 。
IDataErrorInfo 接口定义了Error字段,这个字段在结构有多个字段时,难以映射,于是在[]下标下实现逻辑。
INotifyDataErrorInfo 接口需要实现 HasError,GetErrors 两个函数,并需要在属性的set 访问器内实现校验逻辑,并管理Error信息。
总体来说,在数据对象中引发错误不是一个好选择,验证逻辑硬编码在数据对象中,不利于复用。
在绑定级别上定义验证
在绑定级别上定义验证是针对于数据类型自定义验证规则,可以对同一种数据类型进行复用。自定义验证规则需要继承自 ValidationRule 类,需要实现 Validate 函数,在其中完成自定义验证内容。
cs
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 来实现,将需要校验的多个控件放置于同一个容器中,在容器级别应用验证规则,需要通过事件主动触发验证,通常是子组件失去焦点时。
cs
<Grid Grid.Row="3" Grid.ColumnSpan="2" DataContext="{Binding Path=Person}" TextBoxBase.LostFocus="Grid_LostFocus">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.BindingGroup>
<BindingGroup x:Name="personValidation">
<BindingGroup.ValidationRules>
<local:PersonRule/>
</BindingGroup.ValidationRules>
</BindingGroup>
</Grid.BindingGroup>
<TextBlock Grid.Row="0" Grid.Column="0">Person ID:</TextBlock>
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Path=ID}" />
<TextBlock Grid.Row="1" Grid.Column="0">Person Name:</TextBlock>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Path=Name}"/>
</Grid>
cs
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
cs
<Window x:Class="TestValidation.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:TestValidation"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Border Padding="7" Margin="7" Background="LightSteelBlue">
<Grid x:Name="myGrid">
<Grid.Resources>
<Style TargetType="{x:Type TextBox}">
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<DockPanel LastChildFill="True">
<TextBlock DockPanel.Dock="Right" Foreground="Red" FontSize="14" FontWeight="Bold"
ToolTip="{Binding ElementName=adornerPlaceholder, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}">*</TextBlock>
<Border BorderBrush="Green" BorderThickness="1">
<AdornedElementPlaceholder Name="adornerPlaceholder"></AdornedElementPlaceholder>
</Border>
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
<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>
</Grid.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="2*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0">PersonAge:</TextBlock>
<TextBox Grid.Row="0" Grid.Column="1" DataContext="{Binding Path=PersonAge}">
<TextBox.Text>
<Binding Path="Age" NotifyOnValidationError="true">
<Binding.ValidationRules>
<DataErrorValidationRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<TextBlock Grid.Row="1" Grid.Column="0">PersonName:</TextBlock>
<TextBox Grid.Row="1" Grid.Column="1" DataContext="{Binding Path=PersonName}">
<TextBox.Text>
<Binding Path="Name" NotifyOnValidationError="true">
<Binding.ValidationRules>
<NotifyDataErrorValidationRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<TextBlock Grid.Row="2" Grid.Column="0">PersonPrice:</TextBlock>
<TextBox Grid.Row="2" Grid.Column="1" DataContext="{Binding Path=PersonPrice}">
<TextBox.Text>
<Binding Path="Price" NotifyOnValidationError="true">
<Binding.ValidationRules>
<local:PriceRule Min="0"/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Grid Grid.Row="3" Grid.ColumnSpan="2" DataContext="{Binding Path=Person}" TextBoxBase.LostFocus="Grid_LostFocus">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.BindingGroup>
<BindingGroup x:Name="personValidation">
<BindingGroup.ValidationRules>
<local:PersonRule/>
</BindingGroup.ValidationRules>
</BindingGroup>
</Grid.BindingGroup>
<TextBlock Grid.Row="0" Grid.Column="0">Person ID:</TextBlock>
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Path=ID}" />
<TextBlock Grid.Row="1" Grid.Column="0">Person Name:</TextBlock>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Path=Name}"/>
</Grid>
</Grid>
</Border>
</Window>
MainWindow.xaml.cs
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<T>(ref T member, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.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<string> errors = new List<string> ();
errors.Add("The Name can only contain letters and numbers.");
SetErrors("Name", errors);
}
else
{
ClearErrors("Name");
}
SetProperty(ref name, value);
}
}
private Dictionary<string, List<string>> errors = new Dictionary<string, List<string>>();
private void SetErrors(string propertyName, List<string> 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<DataErrorsChangedEventArgs>? ErrorsChanged;
public IEnumerable GetErrors(string? propertyName)
{
if(!string.IsNullOrEmpty(propertyName) && errors.ContainsKey(propertyName))
{
return errors[propertyName];
}
return new List<string>();
}
}
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();
}
}