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. [WPF] 在 ViewModel 中让数据验证出错(Validation.HasError)的控件获得焦点

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

  2. <转>ASP.NET学习笔记之MVC 3 数据验证 Model Validation 详解

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

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

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

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

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

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

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

  6. .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. Beta冲刺随笔——Day_Seven

    这个作业属于哪个课程 软件工程 (福州大学至诚学院 - 计算机工程系) 这个作业要求在哪里 Beta 冲刺 这个作业的目标 团队进行Beta冲刺 作业正文 正文 其他参考文献 无 今日事今日毕 林涛: ...

  2. Kubernetes中Service的使用

    目录 简介 1. Service资源定义 1.1 Service Type ClusterIP 无头service NodePort sessionAffinity实现源地址session绑定 简介 ...

  3. Fist—— 团队展示

    作业要求 软件工程1班 团队名称 Fist 这个作业的目标 团队合作开发项目,加强团队合作,进一步了解相应岗位. 作业正文 https://www.cnblogs.com/team4/p/137730 ...

  4. 去除openwrite.cn博客验证码限制

    相信有的小伙伴肯定遇到过如下这种情况,但是作为老白嫖党肯定是 「下次一定」 了,所以今天我们来看看如何不关注公众号实现 「阅读原文」. 如何解决呢? 1.通过 F12 打开控制台,切换至 Elemen ...

  5. 关于java链接装载的思考

    遇到个bug,noClassFoundEx,很常见. 但是问题来了. 比如我的服务器目录是 /opt/tomcat/webapps/ROOT/WEB-INF/classes/cn/controller ...

  6. 转:http协议学习系列(响应头---Response Headers)

    HTTP最常见的响应头如下所示: ·Allow:服务器支持哪些请求方法(如GET.POST等): ·Content-Encoding:文档的编码(Encode)方法.只有在解码之后才可以得到Conte ...

  7. PHP中双引号引起的命令执行漏洞(Kuwebs代码审计 )

    在代码审计一书中提到Kuwebs的配置文件中可以利用PHP可变变量的特性执行代码 在PHP语言中,单引号和双引号都可以表示一个字符串,但是对于双引号来说,可能会对引号内的内容进行二次解释,这就可能会出 ...

  8. js 几种跨域解决方法

    同源策略: JS只能与同一个域中的页面进行通讯,必须是协议.域名.端口都相同,相同域下才能相互通信,这可以被认为是一种通信原则,叫同源策略. 跨域: js跨域是指通过js在不同的域之间进行数据传输或通 ...

  9. 手把手教你写DI_3_小白徒手支持 `Singleton` 和 `Scoped` 生命周期

    手把手教你写DI_3_小白徒手支持 Singleton 和 Scoped 生命周期 在上一节:手把手教你写DI_2_小白徒手撸构造函数注入 浑身绷带的小白同学:我们继续开展我们的工作,大家都知道 Si ...

  10. 题解 CF611H 【New Year and Forgotten Tree】

    Solution 提供一种新思路. 首先考虑如何判断一个状态是否合法. 考虑把所有十进制长度一样的数缩成一个点. 这样的点的个数 \(\le 5\). 蒟蒻猜了一个结论:只要满足对于所有缩出来的点的子 ...