WPF在用户输入时,提供了验证功能,通常验证使用以下两种方式来实现:
INotifyDataErrorInfo
或IDataErrorInfo
接口。只有来自目标的值正在被用于更新数据源时才会应用验证。
public class MyData
{
private string _value = "200";
public string Value
{
get { return _value; }
set
{
_value = value;
if (value == "123")
throw new System.Exception("报错了~~~[Exception]");
}
}
}
ExceptionValidationRule
。ExceptionValidationRule
是预先构建的验证规则,它向WPF发出所有的异常报告。它必须在
<TextBox x:Name="tb1">
<TextBox.Text>
<Binding Path="Value" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<ExceptionValidationRule />
Binding.ValidationRules>
Binding>
TextBox.Text>
TextBox>
ExceptionValidationRule
在绑定过程中发生的所有异常,包括编辑的值不能转为正确类型、属性设置器异常以及值转换器异常(float转为string)。当出现验证失败后,System.Windows.Controls.Validation
类的附加属性会记录下错误:
Validation.HasError
为True,同时会自动将控件的模板切换为Validation.ErrorTemplate
定义的模板。ValidationRule.Validate()
会返回ValidationError
,其中中包含错误细节Binding.NotifyOnValidationError
被设置为True,则会在绑定元素上引发Validation.Error
事件INotifyDataErrorInfo
和INotifyDataErrorInfo
都有类似作用,但是INotifyDataErrorInfo
界面更加丰富。与上面不同的是,实现INotifyDataErrorInfo
或IDataErrorInfo
接口时,允许用户修改为非法值,只不过给出错误提示。
使用INotifyDataErrorInfo的案例
//类实现了INotifyDataErrorInfo接口,该接口定义了HasErrors属性和GetErrors方法,以及ErrorsChanged事件
public class Data : INotifyDataErrorInfo,INotifyPropertyChanged
{
//key为属性名,value为错误信息列表
Dictionary<string, List<string>> errors = new();
void SetErrors(string propertyName, List<string> value)
{
errors.Remove(propertyName);
errors.Add(propertyName, value);
if (ErrorsChanged != null)
{
ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
}
}
void ClearErrors(string propertyName)
{
errors.Remove(propertyName);
if (ErrorsChanged != null)
{
ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
}
}
public bool HasErrors => errors.Count>0;
public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
public event PropertyChangedEventHandler? PropertyChanged;
public IEnumerable Errors => GetErrors("ModelNumber");
public IEnumerable GetErrors(string? propertyName)
{
if (propertyName is null or { Length: <= 0 })
{
return errors.Values;
}
else
{
if (errors.ContainsKey(propertyName))
{
return errors[propertyName];
}
else
{
return null;
}
}
}
private string modelNumber;
public string ModelNumber
{
get { return modelNumber; }
set { modelNumber = value;
bool valid = true;
foreach (char c in modelNumber)
{
if (!char.IsLetterOrDigit(c))
{
valid = false;
break;
}
}
if (!valid)
{
List<string> errors = new();
errors.Add("ModelNumber不能含有标点符号,空格等");
SetErrors("ModelNumber", errors);
}
else
{
ClearErrors("ModelNumber");
}
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("ModelNumber"));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("HasErrors"));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Errors"));
}
}
}
<Window ...>
<Window.DataContext>
<local:Data/>
Window.DataContext>
<StackPanel>
<TextBox Text="{Binding ModelNumber ,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged,ValidatesOnDataErrors=True}"/>
<TextBlock>
<Run Text="是否有错误"/>
<Run Text="{Binding HasErrors, Mode=OneWay}"/>
TextBlock>
<ListView ItemsSource="{Binding Errors}"/>
StackPanel>
Window>
自定义验证规则很像自定义转换器
public class ValueRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
if (value?.ToString() == "123") return new ValidationResult(false, "输入的值不在范围内");
return new ValidationResult(true, null);
}
}
<StackPanel>
<TextBox>
<TextBox.Text>
<Binding Path="Max" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay">
<Binding.ValidationRules>
<local:ValueRule/>
Binding.ValidationRules>
Binding>
TextBox.Text>
TextBox>
StackPanel>
可以看出,
下面可以放置多个验证规则,按顺序执行,当所有的验证规则都通过后,则调用转换器(如果存在),其中ExceptionValidationRule
比较特殊,当输入内容不能转换为其他规则所定义的转换时,也会触发。
首先只有设置了Binding.NotifyOnValidationError
为true时,才会引发Validation.Error
事件,当含有错误时,可以使用静态类Validation
中的附加属性Errors
和HasError
来获取信息。
通常出现错误时,边框显示未红色,也可以自行设置错误模板,错误模板位于装饰层,它位于普通窗口内容之上。
<TextBox Width="130">
<TextBox.Text>
<Binding
Mode="TwoWay"
Path="Max"
UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:ValueRule />
Binding.ValidationRules>
Binding>
TextBox.Text>
<Validation.ErrorTemplate>
<ControlTemplate>
<DockPanel LastChildFill="True">
<TextBlock
DockPanel.Dock="Right"
Foreground="Red"
Text="*" />
<Border BorderBrush="Green" BorderThickness="2">
<AdornedElementPlaceholder />
Border>
DockPanel>
ControlTemplate>
Validation.ErrorTemplate>
TextBox>
其中AdornedElementPlaceholder
代表控件本身,上面案例中是将*
放入了控件周围,如果想将*
重叠放到控件上面,可以使用Grid,放在同一窗格。
<Validation.ErrorTemplate>
<ControlTemplate>
<Grid>
<TextBlock
Margin="50,5,0,0"
DockPanel.Dock="Right"
Foreground="Red"
Text="*" />
<Border BorderBrush="Green" BorderThickness="2">
<AdornedElementPlaceholder />
Border>
Grid>
ControlTemplate>
Validation.ErrorTemplate>
但是这样显示不出错误信息,可以使用ToolTip来显示第一个错误内容
<Validation.ErrorTemplate>
<ControlTemplate>
<Grid>
<TextBlock
Margin="50,5,0,0"
DockPanel.Dock="Right"
Foreground="Red"
Text="*"
ToolTip="{Binding ElementName=adornerPlaceholder, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}" />
<Border BorderBrush="Green" BorderThickness="2">
<AdornedElementPlaceholder x:Name="adornerPlaceholder" />
Border>
Grid>
ControlTemplate>
Validation.ErrorTemplate>
上面模板中使用了AdornedElementPlaceholder
的AdornedElement
属性指向背后的元素。
这样只有悬浮在后面的*号时才会显示错误信息,如果想作为TextBox元素本身的ToolTip,可借助Validation.HasError
可以实现。
<TextBox Width="130">
<TextBox.Text>
<Binding
Mode="TwoWay"
Path="Max"
UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:ValueRule />
Binding.ValidationRules>
Binding>
TextBox.Text>
<Validation.ErrorTemplate>
<ControlTemplate>
<Grid>
<TextBlock
Margin="50,5,0,0"
DockPanel.Dock="Right"
Foreground="Red"
Text="*"
ToolTip="{Binding ElementName=adornerPlaceholder, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}" />
<Border BorderBrush="Green" BorderThickness="2">
<AdornedElementPlaceholder x:Name="adornerPlaceholder" />
Border>
Grid>
ControlTemplate>
Validation.ErrorTemplate>
<TextBox.Style>
<Style TargetType="TextBox">
"Validation.HasError" Value="True">
"ToolTip" Value="{Binding RelativeSource={RelativeSource Mode=Self}, Path=(Validation.Errors)[0].ErrorContent}" />
Style>
TextBox.Style>
TextBox>
很多时候需要动态验证多个绑定值,比如有两个属性,一个Max,一个Min,要求是用户输入Min必须小于Max,要实现这个功能可以使用绑定组来创建。
绑定组的原理很简单,同样是创建继承自ValidationRule的类,不同的是,不能将该规则绑定到单个绑定表达式,而是将其附加到包含所有绑定控件的容器上。
public class Data : INotifyDataErrorInfo,INotifyPropertyChanged
{
public int Max { set; get; } = 100;
public int Min { set; get; } = 1;
}
public class ValueRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
BindingGroup bindingGroup = (BindingGroup)value;
var d= (Data)bindingGroup.Items[0];
if (d.Min >= d.Max)
{
return new ValidationResult(false, "错误,最小值必须小于最大值");
}
return new ValidationResult(true, null);
}
}
<Grid Margin="60" TextBox.LostFocus="Grid_LostFocus">
<Grid.BindingGroup>
<BindingGroup x:Name="customGroup">
<BindingGroup.ValidationRules>
<local:ValueRule />
BindingGroup.ValidationRules>
BindingGroup>
Grid.BindingGroup>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
Grid.RowDefinitions>
<TextBox
x:Name="ddd"
Grid.Row="0"
Text="{Binding Path=Max, BindingGroupName=customGroup, UpdateSourceTrigger=PropertyChanged}" />
<TextBox Grid.Row="1" Text="{Binding Path=Min, BindingGroupName=customGroup, UpdateSourceTrigger=PropertyChanged}" />
Grid>
private void Grid_LostFocus(object sender, RoutedEventArgs e)
{
customGroup.CommitEdit();
}
注意:
Text="{Binding Path=Max, BindingGroupName=customGroup, UpdateSourceTrigger=PropertyChanged}" />
BindingGroup bindingGroup = (BindingGroup)value;
var d = (Data)bindingGroup.Items[0];
var newValue = bindingGroup.GetValue(d, "Min");