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

最新文章

  1. ASP.NET MVC+EF框架+EasyUI实现权限管理系列(22)-为用户设置角色
  2. HTML div 滚动条样式设计
  3. 解决Python2.7的UnicodeEncodeError: &#39;ascii&#39; codec can’t encode异常错误
  4. 3D建模与处理软件简介
  5. JavaScript实现存储HTML字符串
  6. LC_ALL=C
  7. [转]后缀自动机(SAM)
  8. [转载]C#获取本机IPv4地址
  9. POJ1505 Copying Books(二分法)
  10. android控制之 adb shell (已完成,不定期增加内容)
  11. Ucenter整合Thinkphp 双向同步登录退出
  12. linux命令学习02-通过tomcat学习ps和netstat
  13. curl错误码说明
  14. 深入解析Java反射-invoke方法
  15. redis_字符串对象
  16. [转帖]Windows平台卸载Oracle的办法
  17. 第三十四天 UDP协议 并发编程
  18. MySQL分组查询,前3条数据
  19. js验证银行卡号 luhn校验规则
  20. linux查找某段时间修改的文件的总大小

热门文章

  1. C语言编程学习者问答第一期,看看这些问题你出现过吗?
  2. java数组作为函数返回值
  3. php项目使用git的webhooks实现自动部署
  4. 深度学习基础 Probabilistic Graphical Models | Statistical and Algorithmic Foundations of Deep Learning
  5. 编写测试用例 QQ账号6--10位自然数 某城市电话号码 126邮箱注册功能
  6. ubantu+nginx+uwsgi+django部署
  7. 第1.2节 Python学习环境的使用
  8. 第12.4节 Python伪随机数数生成器random模块导览
  9. PyQt(Python+Qt)学习随笔:QAbstractItemView的alternatingRowColors属性
  10. RocketMQ开发者指南