[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:

Copy
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:

Copy
<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 设置为焦点元素:

Copy
<StackPanel FocusManager.FocusedElement="{Binding ElementName=firstButton}"> <Button Name="firstButton" /> </StackPanel>

4. 使用属性控制焦点#

了解 FocusManager.FocusedElement 的使用方式以后,我们可以在 ViewModel 中定义一个 bool 类型属性 IsNameHasFocus,当调用 Submit 函数时更改这个属性值以控制 UI 焦点。

Copy
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 指定某个元素获得焦点:

Copy
<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 上添加这个样式:

Copy
<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 中可以不负责处理焦点,只负责验证数据:

Copy
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 附加属性

8. 源码#

https://github.com/DinoChan/Wpf_Focus_Demo

posted @   dino.c  阅读(1712)  评论(11编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 周边上新:园子的第一款马克杯温暖上架
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· 使用C#创建一个MCP客户端
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
点击右上角即可分享
微信分享提示
CONTENTS