程序地带

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


8. 源码

https://github.com/DinoChan/Wpf_Focus_Demo


版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/dino623/p/focus_controls_in_ViewModel.html

随机推荐

Rasa原文--接触用户

Rasa原文--接触用户

目录ReachingOuttotheUserReachingoutfirst#1.Updatetheconfiguration2.Addarule#3.Addaresponsetemplate#Ext...

郭洪源 阅读(716)

区块链知识系列 - 比特币的UTXO交易模型

区块链知识系列 - 比特币的UTXO交易模型

UTXOBitcoin采用了UTXO模型作为其底层存储的数据结构,其全称为UnspentTransactionoutput,也就是未被使用的交易输出。比特币客户端会在每次接...

搬砖魁首 阅读(208)

bpython缺少termios_Python中缺少数据点

没问题,您是编码和python的新手!您需要添加参数parse_dates=True为read_csv才能将索引首先转换为DatetimIndex,然后再添加reindex-从1...

weixin_39582737 阅读(875)

码海的个人网站上线了!

码海的个人网站上线了!

一直有不少读者吐嘈说手机上看文章很不方便,尤其是看源码类的解析文章,在手机上看体验太糟糕了,搜索历史文章也很不方便。我去对比了下各个博客平台,发...

公众号:码海 阅读(860)

Java基础知识回顾一:类与对象

什么是类?什么是对象?类与对象之间是什么关系?在java中,讲究万事万物皆为对象,说白了,java为我们构造了一个虚...

都是az 阅读(474)

bpython安装使用_windows bpython 的安装流程

刚开始学习python的时候使用的ipython解释器,挺好用的,后来发现bpython功能更强大,linux系统中安装基本没啥问题,不过在wi...

weixin_39844901 阅读(267)