[WPF] 在 ViewModel 中让数据验证出错(Validation.HasError)的控件获得焦点
1. 需求
在 MVVM 中 ViewModel 和 View 之间的交互通常都是靠 Icommand 和 INotifyPropertyChanged,不过有时候还会需要从 MVVM 中控制 View 中的某个元素,让它获得焦点,例如这样:

上面的 gif 是我在另一篇文章 《自定义一个“传统”的 Validation.ErrorTemplate》 中的一个示例,在这个示例中我修改了 Validation.ErrorTemplate,这样在数据验证出错后,相关的控件会显示一个红色的框,获得焦点后用 Popup 弹出具体的错误信息。可是这个过程稍微不够流畅,我希望点击 Sign In 按钮后,数据验证错误的控件自动获得焦点,像下面这个 gif 那样:

这个需求在使用 CodeBehind 的场景很容易实现,但 MVVM 模式就有点难,因为 ViewModel 应该不能直接调用 View 上的任何元素的函数。 如果可以的话,最好通过 ViewModel 上的属性控制 UI 元素,让这个 UI 元素获得焦点。
这篇文章介绍了两种方式实现这个需求。
2. 环境
首先介绍这个例子使用到的 ViewModel 和 View。
首先在 Nuget 上安装 Prism.Core,然后实现一个简单的 ViewModel,这个 ViewModel 只有一个 Name 属性和一个 SubmitCommand:
public class ViewModel : ModelBase
{
public string Name { get; set; }
public ICommand SubmitCommand { get; }
public ViewModel()
{
SubmitCommand = new DelegateCommand(Submit);
}
private void Submit()
{
ErrorsContainer.ClearErrors();
if (string.IsNullOrEmpty(Name))
ErrorsContainer.SetErrors(nameof(Name), new List<string> { "请输入名称" });
}
}
public abstract class ModelBase : BindableBase, INotifyDataErrorInfo
{
private ErrorsContainer<string> _errorsContainer;
public bool HasErrors => ErrorsContainer.HasErrors;
public ErrorsContainer<string> ErrorsContainer
{
get
{
if (_errorsContainer == null)
{
_errorsContainer =
new ErrorsContainer<string>(pn => RaiseErrorsChanged(pn));
}
return _errorsContainer;
}
}
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public IEnumerable GetErrors(string propertyName)
{
return ErrorsContainer.GetErrors(propertyName);
}
protected void RaiseErrorsChanged(string propertyName)
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
}
}
View 上自定义一个 ErrorTemplate,还有一个绑定到 Name 的 TextBox,一个绑定到 SubmitCommand 的 Button:
<Grid HorizontalAlignment="Center" VerticalAlignment="Center" Width="300">
<Grid.Resources>
<ControlTemplate x:Key="ErrorTemplate">
<AdornedElementPlaceholder>
<kino:ValidationContent />
</AdornedElementPlaceholder>
</ControlTemplate>
<Style TargetType="Control">
<Setter Property="Margin" Value="5" />
<Setter Property="FontSize" Value="15" />
<Setter Property="Validation.ErrorTemplate" Value="{StaticResource ErrorTemplate}" />
</Style>
<Style TargetType="Button" BasedOn="{StaticResource {x:Type Control}}"/>
</Grid.Resources>
<StackPanel>
<TextBox x:Name="AddressTextBox"/>
<TextBox x:Name="NameTextBox" Text="{Binding Name,Mode=TwoWay}"/>
<Button Content="Submit" Margin="5" Command="{Binding SubmitCommand}"/>
</StackPanel>
</Grid>
3. FocusManager.FocusedElement 附加属性使用属性控制焦点
ViewModel 不能直接控制 UI 元素的行为,但它可以通过属性影响 UI 元素的某些属性,例如将 Control 的 IsEnabled 与 ViewModel 上的属性绑定。WPF 可用于控制焦点的属性是 FocusManager.FocusedElement 附加属性,这个属性用于获取和设置指定焦点范围内的聚焦元素。一般使用方法如下,这段代码将 Button 设置为焦点元素:
<StackPanel FocusManager.FocusedElement="{Binding ElementName=firstButton}">
<Button Name="firstButton" />
</StackPanel>
4. 使用属性控制焦点
了解 FocusManager.FocusedElement 的使用方式以后,我们可以在 ViewModel 中定义一个 bool 类型属性 IsNameHasFocus,当调用 Submit 函数时更改这个属性值以控制 UI 焦点。
private bool _isNameHasFocus;
public bool IsNameHasFocus
{
get => _isNameHasFocus;
set => SetProperty(ref _isNameHasFocus, value);
}
private void Submit()
{
IsNameHasFocus = false;
ErrorsContainer.ClearErrors();
if (string.IsNullOrEmpty(Name))
{
ErrorsContainer.SetErrors(nameof(Name), new List<string> { "请输入名称" });
IsNameHasFocus = true;
}
}
在 XAML 中定义一个 StackPanel 的样式并为它添加 DataTrigger,当 IsNameHasFocus 的值为 True 时,通过 FocusManager.FocusedElement 指定某个元素获得焦点:
<StackPanel.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding IsNameHasFocus}" Value="True">
<Setter Property="FocusManager.FocusedElement" Value="{Binding ElementName=NameTextBox}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
5. 自动获得焦点
上面的做法实现了我的需求,而且使用这种方案可以让 ViewModel 对 View 有更多的控制权,可以指定哪个 UI 元素在任何时间获得焦点,但坏处就是要写很多代码,而且属性越多耦合越多。
另一种做法是让 Validation.HasError 为 true 的控件自动获得焦点,可以在 View 上添加这个样式:
<Style TargetType="TextBox" BasedOn="{StaticResource {x:Type Control}}">
<Style.Triggers>
<DataTrigger Binding="{Binding (Validation.HasError),RelativeSource={RelativeSource Mode=Self}}" Value="True">
<Setter Property="FocusManager.FocusedElement" Value="{Binding RelativeSource={RelativeSource Mode=Self}}"/>
</DataTrigger>
</Style.Triggers>
</Style>
ViewModel 中可以不负责处理焦点,只负责验证数据:
private void Submit()
{
ErrorsContainer.ClearErrors();
if (string.IsNullOrEmpty(Name))
ErrorsContainer.SetErrors(nameof(Name), new List<string> { "请输入名称" });
}
这个全局 Style 让所有 TextBox 都添加一个绑定到 Validation.HasError 的 DataTrigger,当 Validation.HasError 为 True 时 TextBox 获得焦点。这种做法可以写少很多代码,但对具体业务来说可能不是很好用。
6. 最后
这篇文章只介绍了简单的解决方案,最后还是需要根据自己的业务需求进行修改或封装。View 和 ViewModel 交互可以是一个很庞大的话题,下次有机会再深入探讨。
7. 参考
FocusManager.FocusedElement 附加属性
[WPF] 在 ViewModel 中让数据验证出错(Validation.HasError)的控件获得焦点的更多相关文章
- [WPF] 让第一个数据验证出错(Validation.HasError)的控件自动获得焦点
1. 需求 在上一篇文章 <在 ViewModel 中让数据验证出错(Validation.HasError)的控件获得焦点>中介绍了如何让 Validation.HasError 的控件 ...
- WPF中的数据验证
数据验证 WPF的Binding使得数据能够在数据源和目标之间流通,在数据流通的中间,便能够对数据做一些处理. 数据转换和数据验证便是在数据从源到目标 or 从目标到源 的时候对数据的验证和转换. V ...
- Java中的数据验证
原文链接:https://www.cuba-platform.com/blog/2018-10-09/945 翻译:CUBA China CUBA-Platform 官网 : https://www. ...
- 在kettle中实现数据验证和检查
在kettle中实现数据验证和检查 在ETL项目,输入数据通常不能保证一致性.在kettle中有一些步骤能够实现数据验证或检查.验证步骤能够在一些计算的基础上验证行货字段:过滤步骤实现数据过滤:jav ...
- <转>ASP.NET学习笔记之MVC 3 数据验证 Model Validation 详解
MVC 3 数据验证 Model Validation 详解 再附加一些比较好的验证详解:(以下均为引用) 1.asp.net mvc3 的数据验证(一) - zhangkai2237 - 博客园 ...
- WPF中ContextMenu(右键菜单)使用Command在部分控件上默认为灰色的处理方法
原文:WPF中ContextMenu(右键菜单)使用Command在部分控件上默认为灰色的处理方法 问题描述 今天发现如果我想在一个TextBlock弄一个右键菜单,并且使用Command绑定,结果发 ...
- 实现虚拟模式的动态数据加载Windows窗体DataGridView控件 .net 4.5 (一)
实现虚拟模式的即时数据加载Windows窗体DataGridView控件 .net 4.5 原文地址 :http://msdn.microsoft.com/en-us/library/ms171624 ...
- 【WPF学习】第六十五章 创建无外观控件
用户控件的目标是提供增补控件模板的设计表面,提供一种定义控件的快速方法,代价是失去了将来的灵活性.如果喜欢用户控件的功能,但需要修改使其可视化外观,使用这种方法就有问题了.例如,设想希望使用相同的颜色 ...
- 五种情况下会刷新控件状态(刷新所有子FWinControls的显示)——从DFM读取数据时、新增加子控件时、重新创建当前控件的句柄时、设置父控件时、显示状态被改变时
五种情况下会刷新控件状态(刷新控件状态才能刷新所有子FWinControls的显示): 在TWinControls.PaintControls中,对所有FWinControls只是重绘了边框,而没有整 ...
随机推荐
- leetcode133. 克隆图
给定无向连通图中一个节点的引用,返回该图的深拷贝(克隆).图中的每个节点都包含它的值 val(Int) 和其邻居的列表(list[Node]).示例: 输入:{"$id":&quo ...
- LIS问题$n log_2 n$做法(二分优化)
#include<bits/stdc++.h> using namespace std; const int inf=1e9+5; const int maxn=1e6+5; int n, ...
- 技巧:如何区分dll程序集的编译目标平台(同样适用于查看程序集的其它依赖)
我们在进行net core迁移过程中,有时候需要区分一个dll是针对netstandard平台还是net framework. 本文提供一个技巧来快速区分:通过工具dnSpy打开目标dll,按照如下截 ...
- java编写规范及注意事项
java编写规范及注意事项 1.注释 常见注释有三种 // /**/ /****/ 如何才能写出漂亮的注释呢,注释的目的就是为了使你的代码让人更容易理解和维护,写一手好的注释是一个优秀码农的基本 ...
- LaTeX中的插图
插图代码及注释: 显示效果: 更多命令可以看相关文档,通过在命令行终端输入 texdoc graphicx 命令打开文档.
- 移动端和web端的性能指标
移动端的性能指标: 1.内存:80% 2.CPU 3.流量 4.电量 5.启动速度 6.滑动速度.界面切换速度 7.与服务器交互的网络速度 web端的性能指标: 1.CPU 2.内存 3.网络 4.I ...
- 自学linux——20.Samba服务器的搭建
Samba服务器的搭建 一.Samba的认识 1.Samba是一个能让Linux系统应用Microsoft网络通讯协议的软件 2.Samba不仅用于Linux与windows系统直接的文件共享和打印共 ...
- 第8.14节 Python类中内置方法__str__详解
一. object类内置方法__str__和函数str 类的内置方法__str__和内置函数str实际上实现的是同一功能,实际上str调用的就是__str__方法,只是调用方式不同,二者的调用语法如下 ...
- PyQt(Python+Qt)学习随笔:QTabWidget部件选项卡可用状态访问方法isTabEnabled、setTabEnabled
老猿Python博文目录 专栏:使用PyQt开发图形界面Python应用 老猿Python博客地址 QTabWidget的每个选项卡及页面可设置是否可用,如果选项卡不可用时,则不能通过操作手工切换到该 ...
- Pentaho Report Designer 报表系统 - 入门详解
目录 简介 安装与配置 环境要求 运行方式 使用教学 数据源配置与原始数据获取 报表布局设计与格式化 布局设计 模块结构 控件 示例 报表预览与发布 报表访问与获取 参考材料 简介 Pentaho ...