Data Validation (数据验证)
大部分应用程序获取用户输入需要验证逻辑去确保用户输入期望信息。验证能够基于类型、范围、格式或其他应用程序指定要求进行验证检查。
Binding 配置验证规则
ExceptionValidationRule 目标类型转换异常验证
使用ExceptionValidationRule
规则验证输入的内容转换到目标类型是否正确。
<TextBox Height="40" Width="300" VerticalContentAlignment="Center">
<TextBox.Text>
<Binding Path="Number" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<ExceptionValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
以上的Binding能够进行简化,例子如下:
<TextBox Height="40" Width="300" VerticalContentAlignment="Center">
<TextBox.Text>
<Binding Path="Number" UpdateSourceTrigger="PropertyChanged" ValidatesOnExceptions="True" />
</TextBox.Text>
</TextBox>
TIP: 验证规则一般只有从Target to source 才会进行验证。
DataErrorValidation 数据错误验证
DataErrorValidation 数据验证,针对实现IDataErrorInfo接口的绑定源对象, 当Target传递输入到Source的时候,将触发IDataErrorInfo
索引器触发验证数据项逻辑, Binding系统将验证的结果保存到(Validation.Errors)[0].ErrorContent
字段中。
public string this[string columnName]
{
get
{
if(columnName == nameof(Number))
{
if (Number == 0) return "Number is zero";
return $"Number is {Number}";
}
if(columnName == nameof(Email))
{
if (string.IsNullOrEmpty(Email))
{
return "Email is emtpty.";
}
else
{
return (RegexUtility.IsEmail(Email) ? "Valid Email." : "Invalid Email.");
}
}
return "";
}
}
public string Error => throw new NotImplementedException();
自定义验证规则
根据上面两节中为WPF系统内置的验证规则,在生产环境中需要用到各种各样的数据验证,自定义验证规则的需求自然就来了. 实现一个自定义验证规则只需要重写一个Validation
方法就可以了。Validation方法根据输入的内容,能够支持验证格式、类型,如果验证结果失败,可以返回一个错误信息给UI层,WPF会将验证后的错误信息保存到(Validation.Errors)[0].ErrorContent
中。
一个验证DateTime
格式的例子:
public class DateTimeValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
if (DateTime.TryParse(value.ToString(), out DateTime dateTime))
{
return ValidationResult.ValidResult;
}
else
{
// Date is not a valid date, fail
return new ValidationResult(false, "Value is not a valid date.");
}
}
}
以及如何使用验证规则:
<TextBox x:Name="tbDateTime" Height="40" Width="300" VerticalContentAlignment="Center"
ToolTip="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}">
<TextBox.Text>
<Binding Path="DateTime" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:DateTimeValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
定制验证错误显示模板
默认情况下,错误模板(ErrorTemplate
)会用一个红色的边框框住输入对象TextBox
,看上去会不太好看。
定义一个验证模板:
<ControlTemplate x:Key="validationTemplate">
<Border BorderThickness="1"
BorderBrush="Blue"
UseLayoutRounding="True">
<DockPanel>
<TextBlock DockPanel.Dock="Right"
Foreground="Red"
FontSize="20"
VerticalAlignment="Center"
Text="!" />
<!--Hold住元素的装饰器-->
<AdornedElementPlaceholder/>
</DockPanel>
</Border>
</ControlTemplate>
使用验证错误模板:
<TextBox Validation.ErrorTemplate="{StaticResource validationTemplate}"
ToolTip="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}">
<TextBox.Text>
<Binding Path="DateTime" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:DateTimeValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
验证过程(Validation process)
验证通常发生在目标值被传输到Binding Source
属性的时候。传输会在Binding为TwoWay
或OneWayToSource
中发送。 对于接收操作,什么会引起一个目标源更新依赖于UpdateSourceTrigger
属性。
以下步骤描述验证的过程,如果有一个验证错误或者其他类型错误在以下任何步骤发生,过程将会被终止。
-
绑定引擎检查Binding中是否有任何
ValidationStep
属性是RawProposedValue
的ValidationRule
的验证规则。在此例子中将依次调用每一个ValidationRule
的Validate
方法是否有发生错误或者全部通过。 -
如果存在
Converter
,绑定引擎将调用转换器。 -
如果转换器转换成功,绑定引擎检查Binding中所有
ValidationStep
属性为ConvertedProposedValue
的验证规则。在此例子中将依次调用每一个ValidationRule
的Validate
方法是否有发生错误或者全部通过。 -
绑定引擎将设置
Source
属性 -
绑定引擎检查Binding中是否有任何
ValidationStep
属性是UpdateValue
的ValidationRule
的验证规则。在此例子中将依次调用每一个ValidationRule
的Validate
方法是否有发生错误或者全部通过。如果在一个绑定中设置了一个DataErroValidationRule
并且ValidationStep
默认为UpdateValue
的值时,数据错误验证规则将会进行检查。 -
绑定引擎检查Binding中是否有任何
ValidationStep
属性是CommittedValue
的ValidationRule
的验证规则。在此例子中将依次调用每一个ValidationRule
的Validate
方法是否有发生错误或者全部通过。
如果在验证过程中有一个ValidationRule
没有通过,则绑定引擎会创建一个ValidationError
对象并添加到绑定元素的Validation.Errors
集合中。 在绑定引擎运行任何ValidationRule
之前,它将移除目标元素所有添加在Validation.Errors
附加属性的集合中的所有ValidationError
。对于例子,如果任何ValidationStep
为UpdateValue
验证失败,在下一次验证过程中,如果任何ValidationStep
为UpdateValue
执行,绑定引擎将立即移除目标元素的ValidationError
。
如果Validation.Errors
不为空,则目标元素的属性Validation.HasError
将设置为True
,并且,如果绑定的NotifyOnValidationError
属性设置为True
,则绑定引擎将触发目标元素的路由事件Validation.Error
。
并且注意当一个有效的值传送到目标或者目标传送到源,都将清除Validation.HasError
附加属性。
如果绑定中设置了ExceptionValidationRule
验证规则或者设置VlidationsOnException
属性为True
,并且当目标值设置到源中发送了一个异常时,绑定引擎将检查是否存在UpdateSourceExceptionFilter
异常过滤器并过滤,如果你设置了UpdateSourceExceptionFilter
回调。 如果绑定没有指定UpdateSourceExceptionFilter
,则绑定引擎将创建一个ValidationError
并附带一个Exception
添加到目标元素的Validation.Errors
集合中。
调试机制(Debugging mechanism)
你可以设置一个目标相关元素的PresentationTraceSources.TraceLevel
属性,让其能够接收有关于特定绑定的相关状态。
实践提示
当验证错误时设置明显标记
当在验证错误时,需要对一个验证错误显示的模板,并将错误同步到目标元素的Tooltip 供用户了解错误内容。
ViewModel输入验证,实现IDataErrorInfo
public string this[string columnName]
{
get
{
if(columnName == nameof(Number))
{
if (Number == 0) return "Number is zero";
return $"Number is {Number}";
}
if(columnName == nameof(Email))
{
if (string.IsNullOrEmpty(Email))
{
return "Email is emtpty.";
}
else
{
return (RegexUtility.IsEmail(Email) ? "Valid Email." : "Invalid Email.");
}
}
return null;
}
}
public string Error => null; // 这里只有在使用BindingGroup在有用
在Code-behind中进行数据验证
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace SDKSample
{
public partial class MarginsDialogBox : Window
{
void okButton_Click(object sender, RoutedEventArgs e)
{
// Don't accept the dialog box if there is invalid data
if (!IsValid(this)) return;
}
// Validate all dependency objects in a window
bool IsValid(DependencyObject node)
{
// Check if dependency object was passed
if (node != null)
{
// Check if dependency object is valid.
// NOTE: Validation.GetHasError works for controls that have validation rules attached
bool isValid = !Validation.GetHasError(node);
if (!isValid)
{
// If the dependency object is invalid, and it can receive the focus,
// set the focus
if (node is IInputElement) Keyboard.Focus((IInputElement)node);
return false;
}
}
// If this dependency object is valid, check all child dependency objects
foreach (object subnode in LogicalTreeHelper.GetChildren(node))
{
if (subnode is DependencyObject)
{
// If a child dependency object is invalid, return false immediately,
// otherwise keep checking
if (IsValid((DependencyObject)subnode) == false) return false;
}
}
// All dependency objects are valid
return true;
}
}
}