[WPF] 让第一个数据验证出错(Validation.HasError)的控件自动获得焦点
1. 需求
在上一篇文章 《在 ViewModel 中让数据验证出错(Validation.HasError)的控件获得焦点》中介绍了如何让 Validation.HasError 的控件自动获得焦点,之后引申了另一个问题:如果有多个 HasError 的控件,如何只让第一个自动获得焦点。
这需求比较常见,所以我试着解决这个问题,最终完成了一个 Demo,XAML 如下:
<StackPanel local:ValidationService.IsValidationScope="True">
<StackPanel.Resources>
<Style BasedOn="{StaticResource {x:Type TextBox}}"
TargetType="TextBox">
<Setter Property="local:ValidationService.AutoFocusWhenValidationError"
Value="True" />
</Style>
</StackPanel.Resources>
<TextBox Text="{Binding Name, Mode=TwoWay, NotifyOnValidationError=True}" />
<TextBox Text="{Binding Name, Mode=TwoWay, NotifyOnValidationError=True}" />
<TextBox Text="{Binding Name, Mode=TwoWay, NotifyOnValidationError=True}" />
<Button Margin="5"
Command="{Binding SubmitCommand}"
Content="Submit" />
</StackPanel>
为了实现这个功能用到了几个入门知识,这篇文章讲解如何组合这几个入门知识实现需求:
2. Validation.Error 附加事件
为了实现自动获得焦点这个需求,我们首先需要一个和数据验证错误相关的事件通知。Validation 类 提供了很多支持数据验证的方法和附加属性,其中这次用到的是 Validation.Error 附加事件,它在绑定元素遇到验证错误时触发。使用方式如下:
Validation.AddErrorHandler(target, (s, e) =>
{
//some code
});
注意,为了使用这个事件,数据绑定中的 NotifyOnValidationError 必须设置为 true:
Text="{Binding Name, Mode=TwoWay, NotifyOnValidationError=True}"
3. WPF 中的树
使用 VisualTreeHelper 遍历 VisualTree,再通过 Validation.GetHasError 判断元素是否具有 ValidationError,这样就可以找出所有数据验证错误的元素。我在以前的文章中提供了一个用于遍历 VisualTree 的扩展方法类 VisualTreeExtensions,这次我直接使用它找出第一次数据验证出错的元素:
var root = Window.GetWindow(target).Content as UIElement;
var errorElement = root.GetVisualDescendants().OfType<UIElement>().FirstOrDefault(u => Validation.GetHasError(u));
4. 附加属性
附加属性是由 XAML 定义的概念。 附加属性旨在用作可在任何对象上设置的一类全局属性。通常来说附加属性有两种用法:纯粹作为属性值,或者在属性值改变的回调函数里执行代码。而这次我两种方式都有用到。
在上面的代码中,我先获得要获得焦点的控件的根节点元素,然后再找到第一次数据验证出错的元素。如果在结构复杂的 UI 中这个操作稍微有点耗时,而且说不定找到的是别的表单中的控件。这篇文章提到的“让第一个 HasError 的元素获得焦点”这个需求,通常还有一个隐含的条件:同一个表单以内。一般业务来说,同一个表单里的输入控件并不会太多,起码 VisualTree 会比一整个 Window 的 VisualTree 简单很多。所以需要用一个附加属性,将表单的根节点标记出来。在这里我参考 Grid.IsSharedSizeScope 附加属性 自定义了一个 IsValidationScope 属性作为标识:
public static bool GetIsValidationScope(DependencyObject obj) => (bool)obj.GetValue(IsValidationScopeProperty);
public static void SetIsValidationScope(DependencyObject obj, bool value) => obj.SetValue(IsValidationScopeProperty, value);
public static readonly DependencyProperty IsValidationScopeProperty =
DependencyProperty.RegisterAttached("IsValidationScope", typeof(bool), typeof(ValidationService), new PropertyMetadata(default(bool)));
在 XAML 中,将 StackPanel 标识为 ValidationScope:
<StackPanel local:ValidationService.IsValidationScope="True">
然后查找表单根节点的代码修改成这样:
var root = target.GetVisualAncestors().OfType<UIElement>().FirstOrDefault(d => GetIsValidationScope(d));
if (root == null)
root = Window.GetWindow(target).Content as UIElement;
IsValidationScope 是纯粹作为属性值的附加属性,我还需要定义另一个暑假属性, 并在它的属性值改变的回调函数中执行上面的逻辑。完整代码如下:
public static bool GetAutoFocusWhenValidationError(DependencyObject obj) => (bool)obj.GetValue(AutoFocusWhenValidationErrorProperty);
public static void SetAutoFocusWhenValidationError(DependencyObject obj, bool value) => obj.SetValue(AutoFocusWhenValidationErrorProperty, value);
public static readonly DependencyProperty AutoFocusWhenValidationErrorProperty =
DependencyProperty.RegisterAttached("AutoFocusWhenValidationError", typeof(bool), typeof(ValidationService), new PropertyMetadata(default(bool), OnAutoFocusWhenValidationErrorChanged));
private static void OnAutoFocusWhenValidationErrorChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
var oldValue = (bool)args.OldValue;
var newValue = (bool)args.NewValue;
if (newValue == oldValue || newValue == false)
return;
var target = obj as UIElement;
Validation.AddErrorHandler(target, (s, e) =>
{
var root = target.GetVisualAncestors().OfType<UIElement>().FirstOrDefault(d => GetIsValidationScope(d));
if (root == null)
root = Window.GetWindow(target).Content as UIElement;
var errorElement = root.GetVisualDescendants().OfType<UIElement>().FirstOrDefault(u => Validation.GetHasError(u));
if (errorElement != null && errorElement.IsKeyboardFocused == false)
errorElement.Focus();
});
}
在 OnAutoFocusWhenValidationErrorChanged 这个回调函数里面,我们可以拿到被 “附加”的元素 target,以及附加属性的值。如果这个值为 true (在这种用法里通常都是 true,类似一个简单的 Behavior),则通过 Validation.AddErrorHandler 为 target 添加事件处理程序,当数据验证出错时找到表单范围内第一个出错的元素,如果它还没有获得焦点就执行 Focus 函数。
在 XAML 中,为了让表单中所有元素都附加上这个行为,可以通过全局样式:
<StackPanel.Resources>
<Style BasedOn="{StaticResource {x:Type TextBox}}"
TargetType="TextBox">
<Setter Property="local:ValidationService.AutoFocusWhenValidationError"
Value="True" />
</Style>
</StackPanel.Resources>
5. 最后
这种做法需要每个数据绑定中的 NotifyOnValidationError 必须设置为 true,在实际业务中比较麻烦。还有一种方法是主动遍历所有元素并使用 Validation.GetHasError 找到目标元素,这样做法简单很多,但不够自动,而且和本文的方法大同小异,就不另外写出来了。
6. 源码
https://github.com/DinoChan/Wpf_Focus_Demo
[WPF] 让第一个数据验证出错(Validation.HasError)的控件自动获得焦点的更多相关文章
- [WPF] 在 ViewModel 中让数据验证出错(Validation.HasError)的控件获得焦点
1. 需求 在 MVVM 中 ViewModel 和 View 之间的交互通常都是靠 Icommand 和 INotifyPropertyChanged,不过有时候还会需要从 MVVM 中控制 Vie ...
- <转>ASP.NET学习笔记之MVC 3 数据验证 Model Validation 详解
MVC 3 数据验证 Model Validation 详解 再附加一些比较好的验证详解:(以下均为引用) 1.asp.net mvc3 的数据验证(一) - zhangkai2237 - 博客园 ...
- C# WPF 低仿网易云音乐(PC)歌词控件
原文:C# WPF 低仿网易云音乐(PC)歌词控件 提醒:本篇博客记录了修改的过程,废话比较多,需要项目源码和看演示效果的直接拉到文章最底部~ 网易云音乐获取歌词的api地址 http://music ...
- 实现虚拟模式的动态数据加载Windows窗体DataGridView控件 .net 4.5 (一)
实现虚拟模式的即时数据加载Windows窗体DataGridView控件 .net 4.5 原文地址 :http://msdn.microsoft.com/en-us/library/ms171624 ...
- WPF Prism MVVM 中 弹出新窗体. 放入用户控件
原文:WPF Prism MVVM 中 弹出新窗体. 放入用户控件 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/qq_37214567/artic ...
- .net dataGridView当鼠标经过时当前行背景色变色;然后【给GridView增加单击行事件,并获取单击行的数据填充到页面中的控件中】
1.首先在前台dataGridview属性中增加onRowDataBound属性事件 2.然后在后台Observing_RowDataBound事件中增加代码 protected void Obser ...
- WPF加载Winform窗体时 报错:子控件不能为顶级窗体
一.wpf项目中引用WindowsFormsIntegration和System.Windows.Forms 二.Form1.Designer.cs 的 partial class Form1 设置为 ...
- 五种情况下会刷新控件状态(刷新所有子FWinControls的显示)——从DFM读取数据时、新增加子控件时、重新创建当前控件的句柄时、设置父控件时、显示状态被改变时
五种情况下会刷新控件状态(刷新控件状态才能刷新所有子FWinControls的显示): 在TWinControls.PaintControls中,对所有FWinControls只是重绘了边框,而没有整 ...
- 从数据池中捞取的存储过程控件使用完以后必须unprepare
从数据池中捞取的存储过程控件使用完以后必须unprepare,否则会造成输入参数是仍是旧的BUG. 提示:动态创建的存储过程控件无此BUG.此BUG只限于从数据池中捞取的存储过程控件. functio ...
随机推荐
- 编程小白必备——主流语言C语言知识点
对于编程语言来说,经常看到有因为各自支持的语言阵营而互怼的,其实根本没那个必要,都只是一种工具而已.当多数主流语言都会使用时也许你就不会有偏见了,本质不过都是用来描述计算机的一个任务,只是每门语言设计 ...
- 基于混沌Logistic加密算法的图片加密与还原
摘要 一种基于混沌Logistic加密算法的图片加密与还原的方法,并利用Lena图和Baboon图来验证这种加密算法的加密效果.为了能够体现该算法在图片信息加密的效果,本文还采用了普通行列置乱加密算法 ...
- idea2020安装破解教程
申明:本教程 IntelliJ IDEA 破解补丁.激活码均收集于网络,请勿商用,仅供个人学习使用 不花钱 的方式 IDEA 2020.2 激活到 2089 年 idea官网下载安装包:https:/ ...
- 【mq读书笔记】消息消费队列和索引文件的更新
ConsumeQueue,IndexFile需要及时更新,否则无法及时被消费,根据消息属性查找消息也会出现较大延迟. mq通过开启一个线程ReputMessageService来准时转发commitL ...
- loading爬坑--跳出思维误区
最近在摸loading这个登录的loading动画,爬了一些坑. 第一坑--百度坑 我们爬的坑,前人都已经已经爬过了.并且把路都放在度娘了.--鲁迅 我最开始是不知道这个直接叫loading的,最开始 ...
- bugkuctf web区 sql2
来了!终于做出来(虽然是在大佬帮助下,提前感谢大佬) 在看wp之后发现这是一道典型的.DS_Store源码泄露,其他类型的web源码泄露:https://www.secpulse.com/archiv ...
- 【Dotnet9-01】从0开始搭建开源项目-lqclass.com
行文目录 一. 前言 1.1 我的现有网站 1.2 想法:新开发一个网站 1.3 目前开发计划 二. 行动了 2.1 Github创建项目 2.2 使用 WTM 搭建后台框架 2.3 项目演示 2.4 ...
- Python(二) 安装PIL
1. 在使用PIL之前我们需先安装PIL. 在cmd中使用 pip 指令,竟报错,没有这个指令 2. 我就给环境变量加上这个指令,找到本机上安装python的位置,找到scrips文件夹, 看到里面的 ...
- 题解 CF1437G Death DBMS
这题感觉不是很难,但是既然放在 \(\texttt{EDU}\) 的 \(\texttt{G}\) 题,那么还是写写题解吧. \(\texttt{Solution}\) 首先看到 "子串&q ...
- 《图解TCP/IP》笔记
OSI参考模型 协议分层 为什么需要分层? 简化网络协议. 每一层只需要衔接上下层的服务. 利于模块化开发. 解耦. 分层的问题 过分模块化.提高数据处理的开销. OSI参考模型 作用及意义 将复杂的 ...