zoukankan      html  css  js  c++  java
  • [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

  • 相关阅读:
    poj 2411 Mondriaan's Dream 骨牌铺放 状压dp
    zoj 3471 Most Powerful (有向图)最大生成树 状压dp
    poj 2280 Islands and Bridges 哈密尔顿路 状压dp
    hdu 3001 Travelling 经过所有点(最多两次)的最短路径 三进制状压dp
    poj 3311 Hie with the Pie 经过所有点(可重)的最短路径 floyd + 状压dp
    poj 1185 炮兵阵地 状压dp
    poj 3254 Corn Fields 状压dp入门
    loj 6278 6279 数列分块入门 2 3
    VIM记事——大小写转换
    DKIM支持样本上传做检测的网站
  • 原文地址:https://www.cnblogs.com/dino623/p/focus_controls_in_ViewModel.html
Copyright © 2011-2022 走看看