[WPF] 让第一个数据验证出错(Validation.HasError)的控件自动获得焦点

时间:2021-08-05 16:54:40

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.AddErrorHandlertarget 添加事件处理程序,当数据验证出错时找到表单范围内第一个出错的元素,如果它还没有获得焦点就执行 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)的控件自动获得焦点的更多相关文章

  1. &lbrack;WPF&rsqb; 在 ViewModel 中让数据验证出错(Validation&period;HasError)的控件获得焦点

    1. 需求 在 MVVM 中 ViewModel 和 View 之间的交互通常都是靠 Icommand 和 INotifyPropertyChanged,不过有时候还会需要从 MVVM 中控制 Vie ...

  2. &lt&semi;转&gt&semi;ASP&period;NET学习笔记之MVC 3 数据验证 Model Validation 详解

    MVC 3 数据验证 Model Validation 详解  再附加一些比较好的验证详解:(以下均为引用) 1.asp.net mvc3 的数据验证(一) - zhangkai2237 - 博客园 ...

  3. C&num; WPF 低仿网易云音乐(PC)歌词控件

    原文:C# WPF 低仿网易云音乐(PC)歌词控件 提醒:本篇博客记录了修改的过程,废话比较多,需要项目源码和看演示效果的直接拉到文章最底部~ 网易云音乐获取歌词的api地址 http://music ...

  4. 实现虚拟模式的动态数据加载Windows窗体DataGridView控件 &period;net 4&period;5 (一)

    实现虚拟模式的即时数据加载Windows窗体DataGridView控件 .net 4.5 原文地址 :http://msdn.microsoft.com/en-us/library/ms171624 ...

  5. WPF Prism MVVM 中 弹出新窗体&period; 放入用户控件

    原文:WPF Prism MVVM 中 弹出新窗体. 放入用户控件 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/qq_37214567/artic ...

  6. &period;net dataGridView当鼠标经过时当前行背景色变色;然后【给GridView增加单击行事件,并获取单击行的数据填充到页面中的控件中】

    1.首先在前台dataGridview属性中增加onRowDataBound属性事件 2.然后在后台Observing_RowDataBound事件中增加代码 protected void Obser ...

  7. WPF加载Winform窗体时 报错:子控件不能为*窗体

    一.wpf项目中引用WindowsFormsIntegration和System.Windows.Forms 二.Form1.Designer.cs 的 partial class Form1 设置为 ...

  8. 五种情况下会刷新控件状态(刷新所有子FWinControls的显示)——从DFM读取数据时、新增加子控件时、重新创建当前控件的句柄时、设置父控件时、显示状态被改变时

    五种情况下会刷新控件状态(刷新控件状态才能刷新所有子FWinControls的显示): 在TWinControls.PaintControls中,对所有FWinControls只是重绘了边框,而没有整 ...

  9. 从数据池中捞取的存储过程控件使用完以后必须unprepare

    从数据池中捞取的存储过程控件使用完以后必须unprepare,否则会造成输入参数是仍是旧的BUG. 提示:动态创建的存储过程控件无此BUG.此BUG只限于从数据池中捞取的存储过程控件. functio ...

随机推荐

  1. POJ 1062 昂贵的聘礼 (最短路)

    昂贵的聘礼 题目链接: http://acm.hust.edu.cn/vjudge/contest/122685#problem/M Description 年轻的探险家来到了一个印第安部落里.在那里 ...

  2. Openfire服务器MySQL优化

    Openfire服务器MySQL优化: [root@iZ28g4ctd7tZ ~]# mysql -u root -p XXXXX mysql> show processlist; +----- ...

  3. Flip Game

    http://poj.org/problem?id=1753 #include<cstdio> #include<algorithm> #include<string.h ...

  4. 安卓开发之探秘蓝牙隐藏API(转)

    源:http://www.cnblogs.com/xiaochao1234/p/3793172.html 上次讲解Android的蓝牙基本用法,这次讲得深入些,探讨下蓝牙方面的隐藏API.用过Andr ...

  5. zabbix 批量生成聚合图形

    通过插入数据库的方式批量生成 zabbix 聚合图形 原型图形 聚合的 sql 批量操作 .在聚合图形创建好一个聚合图形A.找出图形A的ID (创建图形的时候记得填写好行数和列数) select sc ...

  6. ElasticSearch安装部署(Windows)

    测试版本:elasticsearch-5.1.1 1.解压elasticsearch-5.1.1.zip. 2.执行elasticsearch.bat启动服务,启动画面如下: 3.访问ElasticS ...

  7. iOS 网络请求中的空类型字符串转换

    创建一个工具类,   .h: #import <Foundation/Foundation.h> @interface MySetNullWithStrTool : NSObject +( ...

  8. vue源码-检查对象是否全相等

    /** * Check if two values are loosely equal - that is, * if they are plain objects, do they have the ...

  9. linux 设置git记住密码

    linux下: 1.在~/下, touch创建文件 .git-credentials, 用vim编辑此文件,输入: https://{username}:{password}@github.com 注 ...

  10. perl中的默认变量与Z&sol;map介绍

    use v6; =begin pod @*ARGS 命令行参数, 不含脚本名 $*PROGRAM-NAME:当前运行脚本的相对路径 $*PROGRAM:当前运行脚本的文件名称 $*CWD:当前工作路径 ...