理解和使用WPF 验证机制

原创 2013年06月20日 11:15:37
  • 7404

首先建立一个demo用以学习和实验WPF Data Validation机制。创建一个数据实体类:

public class Employee

{

public string Name { get; set; }

public int? Age { get; set; }

}

创建一个用户控件或者窗口,用以输入Name和Age,如下:

<Grid Width="400" Height="200">

<Grid.RowDefinitions>

<RowDefinition/>

<RowDefinition/>

<RowDefinition/>

</Grid.RowDefinitions>

<Grid.ColumnDefinitions>

<ColumnDefinition Width="80"/>

<ColumnDefinition/>

</Grid.ColumnDefinitions>

<TextBlock Text="User Name:"VerticalAlignment="Center"/>

<TextBox Grid.Column="1" x:Name="tb" Height="30">

<TextBox.Text>

<Binding Path="Name">

<Binding.ValidationRules>

<local:NotNullValidationRule/>

</Binding.ValidationRules>

</Binding>

</TextBox.Text>

</TextBox>

<TextBlock Text="Age" Grid.Row="1"VerticalAlignment="Center"/>

<TextBox Grid.Row="1"Grid.Column="1" Height="30">

<TextBox.Text>

<Binding Path="Age">

<Binding.ValidationRules>

<local:NotNullValidationRule/>

</Binding.ValidationRules>

</Binding>

</TextBox.Text>

</TextBox>

<Button Grid.Row="2"Grid.Column="1" Content="Save" Width="60" Height="23"/>

</Grid>

在后置代码中连接数据上下文,如下:

void MainWindow_Loaded(object sender, RoutedEventArgs e)

{

Employee p = new Employee();

DataContext = p;

}

要运行此demo还需要创建一NotNullValidationRule 类,数据验证的工作正是在此类中完成,此类的代码如下:

public class NotNullValidationRule : ValidationRule

{

public override ValidationResult Validate(object value, CultureInfo cultureInfo)

{

if (value == null || string.IsNullOrWhiteSpace(value as string))

{

return new ValidationResult(false, "value cannot benull");

}

return ValidationResult.ValidResult;

}

}

可以看到,此类必须从ValidationRule派生,然后重写Validate方法,参数value就是设置数据绑定的控件属性所表示的值。在我们的示例中,value就是TextBox.Text的值,也就是用户输入的文本。验证逻辑非常简单,不再赘述。验证完成后需要返回一个ValidationResult 对象表示验证结果,如果验证的数据无效,就需要为验证结果指定一个字符串作为错误信息反馈给用户。

好了,现在demo可以运行了,在表示Name的文本框中输入一些字符,然后删除所有刚才输入的字符,最后按下tab键让焦点离开改文本框。可以看见文本框出现了一个红色边框。显然,红色边框不是很美观,而且验证错误信息也没有通过Tooltip的方式呈现出来,记得以前是可以的,现在用的是.NetFramework 4.5,是没有Tooltip提示的。下面我们就自定义一下验证出错时的UI显示。

验证错误的显示样式是由Validation.ErrorTemplate来控制的,这是一个关联属性,类型是ControlTemplate,下面是一个验证错误控件模板的示例:

<ControlTemplate x:Key="ErrorTempalte">

<StackPanel Orientation="Horizontal">

<StackPanel.Triggers>

<EventTrigger RoutedEvent="FrameworkElement.Loaded" SourceName="bd">

<BeginStoryboard>

<Storyboard>

<DoubleAnimation Storyboard.TargetName="bd"Storyboard.TargetProperty="RenderTransform.ScaleX" From="0" To="1" Duration="0:0:0.2"/>

</Storyboard>

</BeginStoryboard>

</EventTrigger>

</StackPanel.Triggers>

<AdornedElementPlaceholder/>

<Border CornerRadius="3"BorderBrush="DarkMagenta" Background="#AAFF0000"BorderThickness="1" Padding="5 2" x:Name="bd">

<Border.RenderTransform>

<ScaleTransform/>

</Border.RenderTransform>

<TextBlock Foreground="White" VerticalAlignment="Center" Text="{BindingPath=/ErrorContent}"/>

</Border>

</StackPanel>

</ControlTemplate>

把以上模板应用到TextBox,

<TextBox Validation.ErrorTemplate="{StaticResource ErrorTempalte}">…

再次运行demo,当焦点离开文本框后,会有一个红色的错误提示显示在文本框的右边,而且还有一个X放心的放大动画,使界面变得有一些动感了。对于这个ControlTemplate,需要记住的是,模板根元素的DataContext是一个ValidationError对象的列表,而在上面的模板中,我们将列表中第一个对象的ErrorContent显示了出来(实际上,当一个绑定有多个ValidationRule的时候,WPF绑定引擎一旦发现有一个ValidationRule验证失败,那么后续的ValidationRule将不会被执行),ErrorContent属性就是我们在构造函数中指定的错误信息。另外一个需要注意的是,这个模板中的界面元素是显示在AdornerLayer中的;要将显示错误的界面元素显示在被验证控件的周围,需要AdornedElementPlaceholder元素的支持,该元素是一个占位符,跟被验证控件有着同样的位置和尺寸;而且还可以通过此类的AdornedElement属性来访问被验证控件。

解决了错误信息显示的问题,还有下一个问题等着我们解决;假设有一个添加用户的界面,而且所有字段都是必须的,用户在填写了部分信息后,点击了保存按钮;如果我们直接保存,那么大部分情况下会出错,因为还存在无效值。但是你可能会想,我们已经为每个绑定设置了ValidationRule,为什么这些ValidationRule没有起作用呢?这是因为对于Binding来说,如果Target值没有变化,那么是不会引发验证的;而且如果设置了绑定的UpdateSourceTrigger="LostFocus" 即使文本框的值变了,但是在文本框焦点离开之前,也是不会引发验证。更糟糕的是,用户根本就没有对文本框做任何输入,所以也就谈不上焦点离开。所以这就要求我们在用户点击保存按钮的时候,手动引发所有验证操作,如果存在任何严重错误,那么验证错误就会像之前那样滑动出来。所谓手动引发,指的是我们自己写代码去引发。可以想象,这不是一个轻松的工作,首先需要获取所有的BindingExpressionBase对象,然后对每个绑定对象调用:

BindingExpressionBase exp = tb.GetBindingExpression(TextBox.TextProperty);

exp.ValidateWithoutUpdate();

显然,这种方案是让人无法忍受的。VaildationRule.ValidateOnTargetUpdated属性或许会给我们带来解决问题的曙光。修改NotNullValidationRule类如下:

public NotNullValidationRule()

{

this.ValidatesOnTargetUpdated = true;

}

我们给NotNullValidationRule 添加了一个构造函数,其中把ValidatesOnTargetUpdated设置为true;意思是,当Target被更新的时候执行验证逻辑。当我们设置DataContext的时候,Target将会被更新。所以当我们做了这个修改后,重新运行demo,你会发现,窗口一出现,所有的验证错误就被立刻显示了出来。这显然不是我们想要的,我们希望的逻辑是,当用户第一次进入编辑界面,即使数据对象是无效的,也不要显示验证错误,只有当用户点击了保存按钮或者焦点离开了某个绑定了数据对象的控件的时候才显示验证错误。由此我们得到的结论是:这个属性也许在某个时候会有用处,但是现在对我们来说却是无用的。

另一个解决此问题的方案是使用BindingGroup类,改类型是在.NetFramework 3.5 SP1引入的。BindingGroup能够将一组绑定集合起来,整体更新。如下所示:

<Grid Width="400" Height="200">

<Grid.BindingGroup>

<BindingGroup x:Name="bg"/>

</Grid.BindingGroup>…

如果设置了Grid的BindingGroup属性,那么Grid里面控件的所有绑定都会属于同一个BindingGroup。实际上属于不属于同一个BindingGroup,主要是看Binding对象的BindingGroupName属性是否和BindingGroup的Name相同,如果都没有设置,自然就是相同的了。

添加了BindingGroup之后,我们需要在保存按钮的事件处理方法中,验证所有属于BindingGroup的绑定,如下:

private void Button_Click(object sender, RoutedEventArgs e)

{

bool isValid = bg.ValidateWithoutUpdate();

if (isValid)

{

//Saveyour employee

}

}

试着运行demo,你会发现,点击了保存按钮后,界面没有任何响应,而且查看isValid的值,居然为true。但是,假如尝试着在Name和Age的文本框里键入一些文本,再删除这些键入的文本,然后点击保存按钮,验证就会起作用。根本的原因是:BindingGroup总是认为初始值是有效的值,不需要再验证。这在编辑一个用户的时候是有用的,但是因为我们是在添加一个对象,我们的初始值都是无效的,所以BindingGroup这一特性使得我们无法完成验证任务。BindingGroup还有一个UpdateSources方法,可以将所有属于BindingGroup的BindingExpressionBase执行UpdateSource方法;但是这个方法有着同样的缺陷,只对那些改变过的属性进行更新。

而且BindingGroup还有另外一个特性也非常令人生厌,当你应用了BindingGroup后,它会改变属于BindingGroup的Binding的UpdateSourceTrigger属性的默认值为Explicit,这就意味着,当焦点离开的时候,验证将不会触发;除非显式调用每个BindingExpressionBase的UpdateSource方法,而这也是我们要使用的方法。但是显示指定每个Binding的UpdateSourceTrigger会覆盖这个行为,BindingGroup会尊重你的设定。所以这基本上完成了我们的任务,改动的代码如下:

<Grid Width="400" Height="200" x:Name="grid">

<Grid.BindingGroup>

<BindingGroup x:Name="bg"/>…

<Binding Path="Name"UpdateSourceTrigger="LostFocus">…

<Binding Path="Age"UpdateSourceTrigger="LostFocus">…

public MainWindow()

{

InitializeComponent();

Employee p = new Employee();

DataContext = p;

}

保存按钮事件处理方法:

private void Button_Click(object sender, RoutedEventArgs e)

{

foreach (var item in bg.BindingExpressions)

{

item.UpdateSource();

}

}

可以看到,问题的解决方案还是很简单的。问题基本上解决了,但是对于仔细查看demo就会发现,当界面显示以后,将焦点转移到Name文本框上,然后再离开,这时候没有发生验证。这是可以理解的,因为文本框里的数据没有发生变化,既然之前没有显示验证错误,那么现在也不应该显示。如果一定要实现这个需求,就应该做出如下变化:

public MainWindow()

{

InitializeComponent();

Employee p = new Employee();

DataContext = p;

Loaded += MainWindow_Loaded;

LostFocus += MainWindow_LostFocus;

}

void MainWindow_LostFocus(object sender, RoutedEventArgs e)

{

foreach (var item in bg.BindingExpressions)

{

if (item.Target == e.Source)

{

item.UpdateSource();

break;

}

}

}

因为LostFocus是一个路由事件,所以在主窗口中的LostFocus事件处理方法能够处理所有包含控件的LostFocus事件。这方法里面,我们获取了当前失去焦点的控件的绑定对象,对其进行手动更新。注意到,我们显式调用的UpdateSource,所以在Xaml中就不需要再设定UpdateSourceTrigger="LostFocus"了。

如果把上面的代码封装到一个关联属性里,用起来会更方便,如下:

public static class FEExtension

{

public static bool GetValidateOnLostFocus(DependencyObject obj)

{

return (bool)obj.GetValue(ValidateOnLostFocusProperty);

}

public static void SetValidateOnLostFocus(DependencyObject obj, bool value)

{

obj.SetValue(ValidateOnLostFocusProperty, value);

}

public static readonly DependencyProperty ValidateOnLostFocusProperty =

DependencyProperty.RegisterAttached("ValidateOnLostFocus", typeof(bool), typeof(FEExtension),

new FrameworkPropertyMetadata(false, OnValidateOnLostFocusChanged));

private static void OnValidateOnLostFocusChanged(object sender,DependencyPropertyChangedEventArgs e)

{

var fe = sender as FrameworkElement;

if (e.NewValue.Equals(true))

{

fe.LostFocus += fe_LostFocus;

}

else

{

fe.LostFocus -= fe_LostFocus;

}

}

static void fe_LostFocus(object sender, RoutedEventArgs e)

{

var fe = sender as FrameworkElement;

foreach (var item in fe.BindingGroup.BindingExpressions)

{

if (item.Target == e.Source)

{

item.UpdateSource();

break;

}

}

}

}

使用方法如下:

<Grid Width="400" Height="200" x:Name="grid" local:FEExtension.ValidateOnLostFocus="true">

深入理解ValidationRule

假设我们有一个毕业时间属性,如下:

public class Employee

{

public string Name { get; set; }

public int? Age { get; set; }

public DateTime GraduationDate { get; set; }

}

该属性不能大于当前时间,所以需要创建一个验证类如下:

class PassedDateTimeValidationRule : ValidationRule

{

public PassedDateTimeValidationRule()

{

ValidationStep= ValidationStep.ConvertedProposedValue;

}

public override ValidationResult Validate(object value, CultureInfo cultureInfo)

{

DateTime dt = (DateTime)value;

if (dt < DateTime.Now)

{

return ValidationResult.ValidResult;

}

return new ValidationResult(false, "Cannot select afuture date");

}

}

验证逻辑非常简单,需要注意的是构造函数中把ValidationStep设置为ValidationStep.ConvertedProposedValue,这表示我们得到的参数value的值已经被Converter转换过了,如果没有Converter,WPF绑定引擎至少会帮你做一个类型转换。所以在上例中,我们的value参数已经不是字符串了,而是一个DateTime对象。

ValidationStep是一个枚举,每个值的解释如下:

public enum ValidationStep

{

// 使用原始值,对于TextBox来说,就是Text属性表示的字符串

RawProposedValue = 0,

//

// 使用转换后的值,将原始值经过类型转换或者经过Binding的Converter转换过的值,这个值

// 还没有被更新到我们的数据对象里面。

ConvertedProposedValue = 1,

//

// 使用更新过的值,也就是说,我们的数据对象的属性值已经被更新了,

// 然后用这个更新的值再做数据验证

UpdatedValue = 2,

//

// 使用提交后的值,验证将会发生在调用了BindingGroup.CommitEdit之后。

//大部分情况下我们都不会再数据提交了再做验证,所以使用该值的情况应该非常少见。

CommittedValue = 3,

}

对于ValidationStep.UpdatedValue,value参数会有所不同,value参数实际上是包含当前ValidationRule对象的BindingExpressionBase对象。说包含有些不恰当,因为是Binding对象包含了ValidationRule。但是BindingExpressionBase和Binding之间是通过public属性相互引用的。如果当前的ValidationRule是属于BindingGroup的,那么value参数就是BindingGroup对象,你可以对其进行转换,这样就可以方法BindingGroup公开的任何方法和属性了。

深入理解BindingGroup

首先来看看BindingGroup提供了那些功能:

public class BindingGroup : DependencyObject

{

//得到所有属于当前BindingGroup的BindingExpressionBase对象

public Collection<BindingExpressionBase> BindingExpressions { get; }

//获取当前BindingGroup的所有数据上下文对象,在本文中只有一个,就是Employee对象

public IList Items { get; }

//该集合将在UpdateSources,ValidateWithoutUpdate,CommitEdit被调用的时候进行验证调用

public Collection<ValidationRule> ValidationRules { get; }

//下面这三个方法对应IEditableObject的方法,调用下面的方法后,如果你的对象实现了IEditableObject接口,那么你的方法将会被调用

public void BeginEdit();

public void CancelEdit();

public bool CommitEdit();

//下面两个方法前面已经介绍过了

public bool UpdateSources();

public boolValidateWithoutUpdate();

}

BindingGroup提供了很多功能,但是确有一个缺陷。这就是我前面提到的,如果文本框里的值没有变化,即使是无效的,它也不会再次验证,也就是说,他会假定初始数据都是有效的,在添加一个新的实体的时候,就无法完成验证数据的功能。

前面说到,我们可以单独调用每个binding对象的Update方法,但是,当你这么做了之后,BindingGroup本身的ValidationRules就得不到执行了。

一个比较Hack的方法是,将每个绑定的NeedsVlidation属性设置为true,如下:

foreach (var item in bg.BindingExpressions)

{

typeof(BindingExpressionBase)

.GetProperty("NeedsValidation",System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)

.SetValue(item, true);

}

bg.ValidateWithoutUpdate();

如此,ValidateWithoutUpdate方法就显的正常了。因为用反射更改了BindingExpressionBase的内部属性,所以说这个方法有点Hack。

理解和使用WPF 验证机制的更多相关文章

  1. 理解和使用WPF 验证机制(值得推荐)

    首先建立一个demo用以学习和实验WPF Data Validation机制.创建一个数据实体类: public class Employee { public string Name { get;  ...

  2. ASP.NET 身份验证机制

    ASP.NET提供了3种认证方式:windows身份验证:IIS根据应用程序的设置执行身份验证.要使用这种验证方式,在IIS中必须禁用匿名访问.Forms验证          :用Cookie来保存 ...

  3. 逆向project第005篇:跨越CM4验证机制的鸿沟(下)

    一.前言 本文是逆向分析CM4系列的最后一篇,我会将该游戏的序列号验证机制分析完成,进而编写出注冊码生成器. 二.分析第二个验证循环 延续上一篇文章的内容,来到例如以下代码处: 图1 上述代码并没有特 ...

  4. 深入理解和探究Java类加载机制

    深入理解和探究Java类加载机制---- 1.java.lang.ClassLoader类介绍 java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字 ...

  5. 逆向project第003篇:跨越CM4验证机制的鸿沟(上)

    一.前言 <冠军足球经理>系列作为一款拟真度极高的足球经营类游戏.赢得过无数赞誉,而CM4可以说是这个传奇的起点. 可是在游戏安装过程中.当用户输入完序列号之后.程序并不会对用户的输入进行 ...

  6. 深入理解android的UI更新机制

    深入理解android的UI更新机制 由问题开始: 如何更新android UI? 可以通过如下方法: 在主线程里直接操作UI控件. handler.post(Runnable) runOnUiThr ...

  7. WPF源代码分析系列一:剖析WPF模板机制的内部实现(一)

    众所周知,在WPF框架中,Visual类是可以提供渲染(render)支持的最顶层的类,所有可视化元素(包括UIElement.FrameworkElment.Control等)都直接或间接继承自Vi ...

  8. 逆向工程第003篇:跨越CM4验证机制的鸿沟(上)

    一.前言 <冠军足球经理>系列作为一款拟真度极高的足球经营类游戏,赢得过无数赞誉,而CM4可以说是这个传奇的起点.但是在游戏安装过程中,当用户输入完序列号之后,程序并不会对用户的输入进行真 ...

  9. 逆向工程第005篇:跨越CM4验证机制的鸿沟(下)

    一.前言 本文是逆向分析CM4系列的最后一篇,我会将该游戏的序列号验证机制分析完毕,进而编写出注册码生成器. 二.分析第二个验证循环 延续上一篇文章的内容,来到如下代码处: 图1 上述代码并没有特别需 ...

随机推荐

  1. c++ 一个cpp文件如何调用另一个cpp文件已经定义的类?我不想重复定义

    文件test1.cpp有类class A;文件test2.cpp有类class B.如在test2.cpp中想用A:#include "test1.cpp" 当然一般的做法是将类的 ...

  2. x86保护模式 任务状态段和控制门

    x86保护模式    任务状态段和控制门 每个任务都有一个任务状态段TSS     用于保存任务的有关信息     在任务内权变和任务切换时  需要用到这些信息    任务内权变的转移和任务切换  一 ...

  3. 九度oj 题目1159:坠落的蚂蚁

    题目描述: 一根长度为1米的木棒上有若干只蚂蚁在爬动.它们的速度为每秒一厘米或静止不动,方向只有两种,向左或者向右.如果两只蚂蚁碰头,则它们立即交换速度并继续爬动.三只蚂蚁碰头,则两边的蚂蚁交换速度, ...

  4. ajax原生post请求

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  5. 凌乱的桌子和与 Web 的设计理念说明

    Python是一门脚本语言,因为能将其他各种编程语言写的模块粘接在一起,也被称作胶水语言.强大的包容性.强悍的功能和应用的广泛性使其受到越来越多的关注,想起一句老话:你若盛开,蝴蝶自来. 如果你感觉学 ...

  6. 写给新员工的十点SQL开发建议

    1.建立自己的知识体系 摘抄一句话你所拥有的知识并不取决于你记得多少,而在于它们能否在恰当的时候被回忆起来: 做笔记: 把笔记放在可以随时被找到的地方.个人的笔记可以放在印象笔记之类工具上,单位上的笔 ...

  7. KVM 存储虚拟化

    KVM 的存储虚拟化是通过存储池(Storage Pool)和卷(Volume)来管理的. Storage Pool 是宿主机上可以看到的一片存储空间,可以是多种类型,后面会详细讨论.Volume 是 ...

  8. 51nod1026 矩阵中不重复的元素 V2

    $n \leq 500000,m \leq 500000$的矩阵,第一行第一列是$a^b,2 \leq a,b \leq 500000$,如果一个数是$i^j$那他右边是$i^{j+1}$,下面是${ ...

  9. BB FlashBack pro导出AVI格式的配置参数

    文件-->导出,选择AVI格式在选择AVI解码器中选择Cinepak Codec by Radius压缩质量:建议6~12%点击确定在AVI导出选项中选择帧速率:4帧每秒音频选择格式 11.02 ...

  10. Ionic 学习笔记

    本文为原创,转载请注明出处: cnzt       文章:cnzt-p http://www.cnblogs.com/zt-blog/p/7831153.html 注: 本篇学习笔记基于Ionic 3 ...