1. 什么是Validaion.ErrorTemplate

数据绑定模型允许您将与您Binding的对象相关联ValidationRules。 如果用户输入的值无效,你可能希望在应用程序 用户界面 (UI) 上提供一些有关错误的反馈。 提供此类反馈的一种方法是设置Validation.ErrorTemplate附加到自定义ControlTemplate的属性。

有关验证的详细讨论, 请参阅数据绑定概述中的 "数据验证" 一节。

如果没有设置Validation.ErrorTemplate,当控件包含无效数据时,WPF 将在无效控件周围显示如下图所示的红色边框,:

这样用户就能清楚这是一个无效的数据,直到用户输入有效的值这个红色的边框才会消失。可是只有一个红色边框,用户并不清楚具体有什么错误,通常需要用其它手段来通知用户具体的错误信息(例如弹出MessageBox)。

2. 如何自定义Validaion.ErrorTemplate

一种更好的方式是通过自定义Validaion.ErrorTemplate显示更多的信息。Validaion.ErrorTemplate的类型是ControlTemplate,它的默认值如下:

<ControlTemplate>
<Border BorderThickness="1"
BorderBrush="Red">
<AdornedElementPlaceholder />
</Border>
</ControlTemplate>

当控件绑定数据无效时默认显示这个ControlTemplate,其中的AdornedElementPlaceholder专门用于Validaion.ErrorTemplate,它用于提供AdornedElement关联的错误控件的定位和尺寸。

通常我会给项目中每一个输入控件都设置Validaion.ErrorTemplate用于方便地显示错误信息,而这个Validaion.ErrorTemplate的样式来自10年前的Silverlight。从Silverlight开始,很多控件库都使用了类似的Validaion.ErrorTemplate样式,所以才说它是个“传统”的Validaion.ErrorTemplate。具体效果如下:

控件的数据出错时显示红色边框,当控件获得焦点通过Tooltip显示具体的错误信息,当空间失去焦点关闭Tooltip。本来这个Tooltip的边框是圆角的,因为我喜欢直角,所以将它改为直角了,其它外观和行为基本和以前Silverlight的版本一样。为了方便调用,我把这个ErrorTempalte的主要内容封装进一个自定义控件ValidationContent,然后具体调用方式如下:

<ControlTemplate x:Key="ErrorTemplate">
<AdornedElementPlaceholder>
<kino:ValidationContent />
</AdornedElementPlaceholder>
</ControlTemplate> <Style TargetType="TextBox">
<Setter Property="Validation.ErrorTemplate"
Value="{StaticResource ErrorTemplate}" />
</Style>

ValidationContent是个没有逻辑代码的控件,它直接继承Control:

public class ValidationContent : Control
{
public ValidationContent()
{
DefaultStyleKey = typeof(ValidationContent);
}
}

ControlTemplate的部分,使用了一个红色边框,右上角的一点装饰,还有一个用于显示据图错误信息的Tooltip:

<Border  BorderBrush="#FFDB000C"
BorderThickness="1"
x:Name="root">
<ToolTipService.ToolTip>
<ToolTip x:Name="validationTooltip"
Placement="Right"
PlacementTarget="{Binding RelativeSource={RelativeSource TemplatedParent}}"
Template="{StaticResource ValidationToolTipTemplate}" />
</ToolTipService.ToolTip>
<Grid Background="Transparent"
HorizontalAlignment="Right"
Height="12"
Width="12"
Margin="1,-4,-4,0"
VerticalAlignment="Top">
<Path Data="M 1,0 L6,0 A 2,2 90 0 1 8,2 L8,7 z"
Fill="#FFDC000C"
Margin="1,3,0,0" />
<Path Data="M 0,0 L2,0 L 8,6 L8,8"
Fill="#ffffff"
Margin="1,3,0,0" />
</Grid>
</Border>

然后在Trigger中通过FindAncestor绑定到祖先元素中的AdornedElementPlaceholder的AdornedElement,判断它是否出错并获得键盘焦点,如果是则打开Tooltip:

<ControlTemplate.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type AdornedElementPlaceholder}}, Path= AdornedElement.IsKeyboardFocusWithin, Mode=OneWay}"
Value="True" />
<Condition Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type AdornedElementPlaceholder}}, Path= AdornedElement.(Validation.HasError), Mode=OneWay}"
Value="True" />
</MultiDataTrigger.Conditions>
<Setter TargetName="validationTooltip"
Property="IsOpen"
Value="True" />
</MultiDataTrigger>
</ControlTemplate.Triggers>

最后是处理Tooltip的Template,它使用Binding [0].ErrorContent显示Validation中Errors附加属性(是一个ReadOnlyObservableCollection类型的集合)中第一条内容(也可以做成一个显示所有错误的ItemsControl,看个人喜好吧)。接下来再在OpenClosed两个VisualState中处理一下动画,就大功告成了。

<ControlTemplate x:Key="ValidationToolTipTemplate">
<Border x:Name="Root"
Margin="5,0,0,0"
Opacity="0"
Padding="0,0,20,20"
RenderTransformOrigin="0,0">
<Border.RenderTransform>
<TranslateTransform x:Name="xform"
X="-25" />
</Border.RenderTransform>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="OpenStates">
<VisualStateGroup.Transitions>
<VisualTransition GeneratedDuration="0" />
<VisualTransition GeneratedDuration="0:0:0.2"
To="Open">
<Storyboard>
<DoubleAnimation Duration="0:0:0.2"
To="0"
Storyboard.TargetProperty="X"
Storyboard.TargetName="xform">
<DoubleAnimation.EasingFunction>
<BackEase Amplitude=".3"
EasingMode="EaseOut" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation Duration="0:0:0.2"
To="1"
Storyboard.TargetProperty="Opacity"
Storyboard.TargetName="Root" />
</Storyboard>
</VisualTransition>
</VisualStateGroup.Transitions>
<VisualState x:Name="Closed">
<Storyboard>
<DoubleAnimation Duration="0"
To="0"
Storyboard.TargetProperty="Opacity"
Storyboard.TargetName="Root" />
</Storyboard>
</VisualState>
<VisualState x:Name="Open">
<Storyboard>
<DoubleAnimation Duration="0"
To="0"
Storyboard.TargetProperty="X"
Storyboard.TargetName="xform" />
<DoubleAnimation Duration="0"
To="1"
Storyboard.TargetProperty="Opacity"
Storyboard.TargetName="Root" />
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<FrameworkElement.Effect>
<DropShadowEffect BlurRadius="11"
ShadowDepth="6"
Opacity="0.4" />
</FrameworkElement.Effect>
<Border Background="#FFDC000C"
BorderThickness="1"
BorderBrush="#FFBC000C">
<TextBlock Foreground="White"
MaxWidth="250"
Margin="8,4,8,4"
TextWrapping="Wrap"
Text="{Binding [0].ErrorContent}"
UseLayoutRounding="false" />
</Border>
</Border>
</ControlTemplate>

3. 其它样式的Validation.ErrorTempalte

现在常见的显示错误信息的手段通常是在输入控件下预留足够显示一行错误信息的空间,例如这样:

或者是索性不预留空间,有错误再占用这些空间:

与它们相比,这篇文章介绍的ErrorTempalte最明显的好处是节省空间。由于我常常都在WPF上做所谓的“信息密集型”软件,所以多年来一直都是用Silverlight的这个ErrorTemplate,没机会跟风修改它的样式。这篇文章已经讲解了如何自定义Validation.ErrorTemplate,有需要的话可以自定义一个合适自己的样式。

4. 结语

Validation.Error没有办法一次性为所有控件统一设置,只能在全局样式中为所有控件都分别设置一次,例如上面出现的``TextBox`的Style,这会很麻烦,毕竟WPF的控件还不少。

除了我的实现方式,MahApps.Metro的实现更加优秀,有兴趣的话也可以参考它的源码:

MahApps.Metro_ValidationErrorTemplate.xaml

5. 参考

Validation.ErrorTemplate 附加属性 (System.Windows.Controls) _ Microsoft Docs

Data binding overview - WPF _ Microsoft Docs

对话框概述 - WPF _ Microsoft Docs

AdornedElementPlaceholder 类 (System.Windows.Controls) _ Microsoft Docs

6. 源码

Kino.Toolkit.Wpf_Validation at master

[WPF 自定义控件]自定义一个“传统”的 Validation.ErrorTemplate的更多相关文章

  1. [WPF 自定义控件]开始一个自定义控件库项目

    1. 目标 我实现了一个自定义控件库,并且打算用这个控件库作例子写一些博客.这个控件库主要目标是用于教学,希望通过这些博客初学者可以学会为自己或公司创建自定义控件,并且对WPF有更深入的了解. 控件库 ...

  2. WPF整理-自定义一个扩展标记(custom markup extension)

    "Markup extensions are used to extend the capabilities of XAML, by providing declarativeoperati ...

  3. WPF 如何自定义一个弹框

    ------------吾亦无他,唯手熟尔,谦卑若愚,好学若饥------------- 简述: 手工以原生Grid的方式,自定义了一个仿弹窗效果,优点可以自定义,缺点需要自己实现以及维护整个弹窗的效 ...

  4. WPF自定义控件与样式(2)-自定义按钮FButton

    一.前言.效果图 申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的等,若有不明白的地方可以参考本系列前面的文章,文末附有部分文章链接. 还是先看看效果 ...

  5. WPF自定义控件与样式(4)-CheckBox/RadioButton自定义样式

    一.前言 申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的等,若有不明白的地方可以参考本系列前面的文章,文末附有部分文章链接. 本文主要内容: Che ...

  6. WPF自定义控件与样式(5)-Calendar/DatePicker日期控件自定义样式及扩展

    一.前言 申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的等,若有不明白的地方可以参考本系列前面的文章,文末附有部分文章链接. 本文主要内容: 日历控 ...

  7. WPF自定义控件与样式(6)-ScrollViewer与ListBox自定义样式

    一.前言 申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的等,若有不明白的地方可以参考本系列前面的文章,文末附有部分文章链接. 本文主要内容: Scr ...

  8. WPF自定义控件与样式(7)-列表控件DataGrid与ListView自定义样式

    一.前言 申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的等,若有不明白的地方可以参考本系列前面的文章,文末附有部分文章链接. 本文主要内容: Dat ...

  9. WPF自定义控件与样式(8)-ComboBox与自定义多选控件MultComboBox

    一.前言 申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的等,若有不明白的地方可以参考本系列前面的文章,文末附有部分文章链接. 本文主要内容: 下拉选 ...

随机推荐

  1. 深入理解Java虚拟机-类加载连接和初始化解析

    不管学习什么,我一直追求的是知其然,还要知其所以然,对真理的追求可以体现在方方面面.人生短短数十载,匆匆一世似烟云,我认为,既然来了,就应该留下一些有意义的东西.本系列文章是结合张龙老师的<深入 ...

  2. 基于Java+HttpClient+TestNG的接口自动化测试框架(四)-------参数存取处理

    在真正开始接口测试之前,我们需要对参数的处理进行梳理.这里所说的“参数”,既包含之前在xml中的配置(我们称之为全局参数),也包含在每一条用例中书写的param.全局参数为固定不变的,而根据接口相应获 ...

  3. [apue] 一个查看当前终端标志位设置的小工具

    话不多说,先看运行效果: >./term input flag 0x00000500 BRKINT not in ICRNL IGNBRK not in IGNCR not in IGNPAR ...

  4. 认识一下 RabbitMQ

    分布式系统中,如何在各个应用之间高效的进行通信,是系统设计中的一个关键. 使用 消息代理(message broker) 是一个优雅的解决方案. RabbitMQ 就是一个被广泛应用的消息代理,遵循 ...

  5. Dart语言学习(六) Dart 列表List数组

    创建List : var list = [1,2,3,"Dart",true]; 创建不可变List : var list = const [1,2,3,"Dart&qu ...

  6. 第二阶段冲刺个人任务——two

    今日任务: 优化作业查询结果,按学号排列. 昨日成果: 修改注册界面.

  7. 为什么RChain区块链上一定要有REV?

    RChain区块链网络上一定要有REV吗?它的作用是什么?在这篇文章里,RChain创始人Mr. Greg Meredith做了详细解读,便于业界和社区对RChain网络有更深入的认识. 作者:Gre ...

  8. 【置顶】入驻百家号【九哥聊IT】和【九哥九嫂小日子】,欢迎关注

    欢迎大家关注. 1.关注百家号[九哥聊IT],每天专注讲解互联网最新资讯和知识分享.2.关注百家号[九哥九嫂小日子],带你看下班之外的九哥.

  9. Docker扩展内容之容器开机自启

    前言 部署项目服务器时,为了应对停电等情况影响正常web项目的访问,会把Docker容器设置为开机自动启动. 在使用docker run启动容器时,使用--restart参数来设置,具体参数如下详解 ...

  10. MGR监控报警

    一.报警思路 m.conf文件记录配置信息,只需要修改这个文件的内容即可(需要将mysql_stat.sh里面的信息写到这里,进行中) mysql_stat.sh文件作为MGR状态监测脚本,加入定时任 ...