WPF中数据绑定验证深入讲解

news2024/11/25 8:22:55

WPF中数据绑定验证深入讲解

WPF在用户输入时,提供了验证功能,通常验证使用以下两种方式来实现:

  1. 在数据对象中引发错误。通常是在属性设置过程中抛出异常,或者在数据类中实现INotifyDataErrorInfoIDataErrorInfo接口。
  2. 在绑定级别定义验证。

只有来自目标的值正在被用于更新数据源时才会应用验证。

数据对象中设置验证

  1. 在属性中Set上抛出异常
public class MyData
{
    private string _value = "200";

    public string Value
    {
        get { return _value; }
        set
        {
            _value = value;

            if (value == "123")
                throw new System.Exception("报错了~~~[Exception]");
        }
    }
}
  1. 直接抛出异常,wpf经常会忽略,从而得不到异常的信息,此时需要借助ExceptionValidationRule

ExceptionValidationRule是预先构建的验证规则,它向WPF发出所有的异常报告。它必须在<Binding.ValidationRules>里面

<TextBox x:Name="tb1">
    <TextBox.Text>
        <Binding Path="Value" UpdateSourceTrigger="PropertyChanged">
            <Binding.ValidationRules>
                <ExceptionValidationRule />
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>

ExceptionValidationRule在绑定过程中发生的所有异常,包括编辑的值不能转为正确类型、属性设置器异常以及值转换器异常(float转为string)。当出现验证失败后,System.Windows.Controls.Validation类的附加属性会记录下错误:

  • 在绑定元素上,Validation.HasError为True,同时会自动将控件的模板切换为Validation.ErrorTemplate定义的模板。
  • ValidationRule.Validate()会返回ValidationError,其中中包含错误细节
  • 如果Binding.NotifyOnValidationError被设置为True,则会在绑定元素上引发Validation.Error事件

INotifyDataErrorInfo

INotifyDataErrorInfoINotifyDataErrorInfo都有类似作用,但是INotifyDataErrorInfo界面更加丰富。与上面不同的是,实现INotifyDataErrorInfoIDataErrorInfo接口时,允许用户修改为非法值,只不过给出错误提示。

image-20231108111152474

使用INotifyDataErrorInfo的案例

  1. 新建一个Data类
//类实现了INotifyDataErrorInfo接口,该接口定义了HasErrors属性和GetErrors方法,以及ErrorsChanged事件
public class Data : INotifyDataErrorInfo,INotifyPropertyChanged
{
    //key为属性名,value为错误信息列表
    Dictionary<string, List<string>> errors = new();

    void SetErrors(string propertyName, List<string> value)
    {
        errors.Remove(propertyName);
        errors.Add(propertyName, value);
        if (ErrorsChanged != null)
        {
            ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
        }
    }
    void ClearErrors(string propertyName)
    {
        errors.Remove(propertyName);
        if (ErrorsChanged != null)
        {
            ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
        }
    }
    public bool HasErrors => errors.Count>0;

    public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
    public event PropertyChangedEventHandler? PropertyChanged;

    public IEnumerable Errors => GetErrors("ModelNumber");
    public IEnumerable GetErrors(string? propertyName)
    {
        if (propertyName is null or { Length: <= 0 })
        {
            return errors.Values;
        }
        else
        {
            if (errors.ContainsKey(propertyName))
            {
                return errors[propertyName];
            }
            else
            {
                return null;
            }
        }
    }

    private string modelNumber;

    public string ModelNumber
    {
        get { return modelNumber; }
        set { modelNumber = value;
            bool valid = true;
            foreach (char c in modelNumber)
            {
                if (!char.IsLetterOrDigit(c))
                {
                    valid = false;  
                    break;
                }
            }
            if (!valid)
            {
                List<string> errors = new(); 
                errors.Add("ModelNumber不能含有标点符号,空格等");
                SetErrors("ModelNumber", errors);
            }
            else
            {
                ClearErrors("ModelNumber");
            }
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("ModelNumber"));
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("HasErrors"));
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Errors"));
        }
    }
}
  1. 做一个界面,绑定ModelNumber
<Window ...>
    <Window.DataContext>
        <local:Data/>
    </Window.DataContext>
    <StackPanel>
        <TextBox Text="{Binding ModelNumber ,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged,ValidatesOnDataErrors=True}"/>
        <TextBlock>
            <Run Text="是否有错误"/>
            <Run Text="{Binding HasErrors, Mode=OneWay}"/>
        </TextBlock>
        <ListView ItemsSource="{Binding Errors}"/>
    </StackPanel>
</Window>

动图

自定义验证规则

自定义验证规则很像自定义转换器

  1. 针对某个属性自定义验证规则
public class ValueRule : ValidationRule
{

    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        if (value?.ToString() == "123") return new ValidationResult(false, "输入的值不在范围内");
        return new ValidationResult(true, null);
    }
}
  1. 界面上使用验证规则
<StackPanel>
    <TextBox>
        <TextBox.Text>
            <Binding Path="Max" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay">
                <Binding.ValidationRules>
                    <local:ValueRule/>
                </Binding.ValidationRules>
            </Binding>
        </TextBox.Text>
    </TextBox>
</StackPanel>

可以看出,<Binding.ValidationRules>下面可以放置多个验证规则,按顺序执行,当所有的验证规则都通过后,则调用转换器(如果存在),其中ExceptionValidationRule比较特殊,当输入内容不能转换为其他规则所定义的转换时,也会触发。

错误显示

首先只有设置了Binding.NotifyOnValidationError为true时,才会引发Validation.Error事件,当含有错误时,可以使用静态类Validation中的附加属性ErrorsHasError来获取信息。

通常出现错误时,边框显示未红色,也可以自行设置错误模板,错误模板位于装饰层,它位于普通窗口内容之上。

<TextBox Width="130">
    <TextBox.Text>
        <Binding
            Mode="TwoWay"
            Path="Max"
            UpdateSourceTrigger="PropertyChanged">
            <Binding.ValidationRules>
                <local:ValueRule />
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
    <Validation.ErrorTemplate>
        <ControlTemplate>
            <DockPanel LastChildFill="True">
                <TextBlock
                    DockPanel.Dock="Right"
                    Foreground="Red"
                    Text="*" />
                <Border BorderBrush="Green" BorderThickness="2">
                    <AdornedElementPlaceholder />
                </Border>
            </DockPanel>
        </ControlTemplate>
    </Validation.ErrorTemplate>
</TextBox>

image-20231108140348940

其中AdornedElementPlaceholder代表控件本身,上面案例中是将*放入了控件周围,如果想将*重叠放到控件上面,可以使用Grid,放在同一窗格。

<Validation.ErrorTemplate>
    <ControlTemplate>
        <Grid>
            <TextBlock
                Margin="50,5,0,0"
                DockPanel.Dock="Right"
                Foreground="Red"
                Text="*" />
            <Border BorderBrush="Green" BorderThickness="2">
                <AdornedElementPlaceholder />
            </Border>
        </Grid>
    </ControlTemplate>
</Validation.ErrorTemplate>

image-20231108141002670

但是这样显示不出错误信息,可以使用ToolTip来显示第一个错误内容

<Validation.ErrorTemplate>
    <ControlTemplate>
        <Grid>
            <TextBlock
                Margin="50,5,0,0"
                DockPanel.Dock="Right"
                Foreground="Red"
                Text="*"
                ToolTip="{Binding ElementName=adornerPlaceholder, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}" />
            <Border BorderBrush="Green" BorderThickness="2">
                <AdornedElementPlaceholder x:Name="adornerPlaceholder" />
            </Border>
        </Grid>
    </ControlTemplate>
</Validation.ErrorTemplate>

上面模板中使用了AdornedElementPlaceholderAdornedElement属性指向背后的元素。

动图

这样只有悬浮在后面的*号时才会显示错误信息,如果想作为TextBox元素本身的ToolTip,可借助Validation.HasError可以实现。

<TextBox Width="130">
    <TextBox.Text>
        <Binding
            Mode="TwoWay"
            Path="Max"
            UpdateSourceTrigger="PropertyChanged">
            <Binding.ValidationRules>
                <local:ValueRule />
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
    <Validation.ErrorTemplate>
        <ControlTemplate>
            <Grid>
                <TextBlock
                    Margin="50,5,0,0"
                    DockPanel.Dock="Right"
                    Foreground="Red"
                    Text="*"
                    ToolTip="{Binding ElementName=adornerPlaceholder, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}" />
                <Border BorderBrush="Green" BorderThickness="2">
                    <AdornedElementPlaceholder x:Name="adornerPlaceholder" />
                </Border>
            </Grid>
        </ControlTemplate>
    </Validation.ErrorTemplate>
    <TextBox.Style>
        <Style TargetType="TextBox">
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="True">
                    <Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Mode=Self}, Path=(Validation.Errors)[0].ErrorContent}" />
                </Trigger>
            </Style.Triggers>
        </Style>
    </TextBox.Style>
</TextBox>

动图

验证多个值

很多时候需要动态验证多个绑定值,比如有两个属性,一个Max,一个Min,要求是用户输入Min必须小于Max,要实现这个功能可以使用绑定组来创建。

绑定组的原理很简单,同样是创建继承自ValidationRule的类,不同的是,不能将该规则绑定到单个绑定表达式,而是将其附加到包含所有绑定控件的容器上。

  1. ViewModel中有两个属性
public class Data : INotifyDataErrorInfo,INotifyPropertyChanged
{
    public int Max { set; get; } = 100;
    public int Min { set; get; } = 1;
}
  1. 创建验证规则
public class ValueRule : ValidationRule
{
    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        BindingGroup bindingGroup = (BindingGroup)value;
        var d= (Data)bindingGroup.Items[0];
        if (d.Min >= d.Max)
        {
            return new ValidationResult(false, "错误,最小值必须小于最大值");
        }
        return new ValidationResult(true, null);
    }
}
  1. UI上绑定,注意,此处要在Grid中添加绑定组
<Grid Margin="60" TextBox.LostFocus="Grid_LostFocus">
    <Grid.BindingGroup>
        <BindingGroup x:Name="customGroup">
            <BindingGroup.ValidationRules>
                <local:ValueRule />
            </BindingGroup.ValidationRules>
        </BindingGroup>
    </Grid.BindingGroup>
    <Grid.RowDefinitions>
        <RowDefinition />
        <RowDefinition />
    </Grid.RowDefinitions>
    <TextBox
        x:Name="ddd"
        Grid.Row="0"
        Text="{Binding Path=Max, BindingGroupName=customGroup, UpdateSourceTrigger=PropertyChanged}" />
    <TextBox Grid.Row="1" Text="{Binding Path=Min, BindingGroupName=customGroup, UpdateSourceTrigger=PropertyChanged}" />
</Grid>
  1. 此时并不会验证,绑定组使用了事务处理编辑系统,只有正式提交后才会进行验证,所以在Grid上增加事件,当TextBox失去焦点时触发
private void Grid_LostFocus(object sender, RoutedEventArgs e)
{
    customGroup.CommitEdit();
}
  1. 如果验证失败,则整个Grid会认为是无效的。

    动图

注意:

  1. 当存在多个绑定组时,要为BindingGroup设置Name,这样可以在具体绑定时设置绑定组Text="{Binding Path=Max, BindingGroupName=customGroup, UpdateSourceTrigger=PropertyChanged}" />
  2. 默认情况时,Validate方法中接收到的数据是原始对象,而不是新修改的值,所以为了验证新值,可以使用GetValue方法
BindingGroup bindingGroup = (BindingGroup)value;
var d = (Data)bindingGroup.Items[0];
var newValue = bindingGroup.GetValue(d, "Min");

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1191196.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【docker容器 redis密码没有生效解决办法】

遇到的问题&#xff1a;启动docker版本redis认证失败&#xff0c;导致web端启动失败 报错内容如下 redis.exceptions.ResponseError: AUTH called without any password configured for the default user. Are you sure your configuration is correct? 做的一系列操作&…

野火霸天虎 STM32F407 学习笔记_4 构建库函数尝试;使用固件库点亮 LED 灯

构建库函数 创建一个通用的模板&#xff0c;后面写程序直接使用这个模板。 $ ls Mode LastWriteTime Length Name ---- ------------- ------ ---- d----- 2023/11/8 23:27 Libraries d----- …

Django文件配置、request对象、连接MySQL、ORM

文章目录 Django静态文件及相关配置静态文件前言静态文件相关配置 form表单request对象request请求结果GET请求POST请求 pycharm连接数据库Django连接MySQLDjango ORM简介 Django静态文件及相关配置 在此篇博客我将以一个用户登录页面来引入相关知识 首先我们先编写一个html页面…

【计算机网络笔记】网络层服务模型——数据报网络

系列文章目录 什么是计算机网络&#xff1f; 什么是网络协议&#xff1f; 计算机网络的结构 数据交换之电路交换 数据交换之报文交换和分组交换 分组交换 vs 电路交换 计算机网络性能&#xff08;1&#xff09;——速率、带宽、延迟 计算机网络性能&#xff08;2&#xff09;…

不可忽视的国外服务器地址IP选择指南

​  在如今互联网高速发展的时代&#xff0c;海外服务器扮演着重要的角色。选择合适的国外服务器IP地址却是一项复杂而又关键的任务。本文将为您介绍一些不可忽视的国外服务器地址IP选择指南。 私有IP地址&#xff1a; 私有IP地址是指在局域网内使用的IP地址&#xff0c;用于…

vue Sts认证后直传图片到阿里云OSS

后端进行sts认证生成临时身份凭证&#xff0c;前端通过凭证直传图片等文件到OSS中 一 OSS配置 增加用户和角色&#xff0c;创建OSS bucket 1.1 添加用户 登录阿里云管理控制台&#xff0c;右侧头像&#xff0c;进入访问控制 点击左侧导航栏的身份管理的用户&#xff0c;点击…

python- time模块

3种时间格式之间的转换 &#xff1a; 1、时间戳->格式化时间 time.localtime(timestamp)&#xff1a;北京时间 time.gmtime(timestamp) &#xff1a;伦敦时间 2、格式化时间->时间戳时间

PHP网站源码 知识付费分站代理自助下单系统 自带多款模板

源码测评&#xff1a;功能很齐全&#xff0c;有可以对接的总站&#xff0c;应该是对接好就可以推广赚钱了&#xff0c;但是这种感觉能赚钱的就那么几个人&#xff0c;见仁见智吧&#xff01; 截图演示&#xff1a; 转载自 https://www.qnziyw.cn/cmsmb/qtcms/3952.html

【JAVA学习笔记】67 - 坦克大战1.5 - 1.6,防止重叠,记录成绩,选择是否开新游戏或上局游戏,播放游戏音乐

项目代码 https://github.com/yinhai1114/Java_Learning_Code/tree/main/IDEA_Chapter20/src 增加功能 1.防止敌人坦克重叠运动 2.记录玩家的成绩&#xff0c;存盘退出 3.记录当时的敌人坦克坐标&#xff0c;存盘退出 4.玩游戏时&#xff0c;可以选择是开新游戏还是继续上局…

说说对React Hooks的理解?解决了什么问题?

一、是什么 Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性 至于为什么引入hook&#xff0c;官方给出的动机是解决长时间使用和维护react过程中常遇到的问题&#xff0c;例如&#xff1a; 难以重用和共享组件中的与状态…

ChatGPT:something went wrong

今天下午不知什么原因&#xff0c;ChatGPT无法使用。我原来在使用ChatGPT for chrome&#xff0c;返回了一个答案&#xff0c;后来在网页端无法使用&#xff0c;以为是这个chrome插件泄露API KEY导致的。注销账号&#xff0c;删除API KEY后&#xff0c;wrong问题仍然存在。 我…

读程序员的制胜技笔记08_死磕优化(上)

1. 过早的优化是万恶之源 1.1. 著名的计算机科学家高德纳(Donald Knuth)的一句名言 1.2. 原话是&#xff1a;“对于约97%的微小优化点&#xff0c;我们应该忽略它们&#xff1a;过早的优化是万恶之源。而对于剩下的关键的3%&#xff0c;我们则不能放弃优化的机会。” 2. 过早…

12 # 手写 findIndex 方法

findIndex 的使用 findIndex() 方法返回数组中满足提供的测试函数的第一个元素的索引。若没有找到对应元素则返回 -1。 <script>var arr [1, 3, 5, 7, 8];var result arr.findIndex(function (ele, index, array) {console.log("ele----->", ele);conso…

【Java】SPI在Java中的实现与应用

一、SPI的概念 1.1、什么是API&#xff1f; API在我们日常开发工作中是比较直观可以看到的&#xff0c;比如在 Spring 项目中&#xff0c;我们通常习惯在写 service 层代码前&#xff0c;添加一个接口层&#xff0c;对于 service 的调用一般也都是基于接口操作&#xff0c;通…

已解决:rm: 无法删除“/opt/module/zookeeper-3.4.10/zkData/zookeeper_server.pid“: 权限不够

解决&#xff1a; ZooKeeper JMX enabled by default Using config: /opt/module/zookeeper-3.4.10/bin/../conf/zoo.cfg Stopping zookeeper ... /opt/module/zookeeper-3.4.10/bin/zkServer.sh: 第 182 行:kill: (4149) - 不允许的操作 rm: 无法删除"/opt/module/zooke…

开发知识点-Python

Python从小白到入土 python渗透测试安全工具开发锦集Python安全工具编程基础第一章 Python在网络安全中的应用第一节 Python黑客领域的现状第二节 我们可以用Python做什么第三节 第一章课程内容总结 第二章 python安全应用编程入门第一节 Python正则表达式第二节 Python Web编程…

C++二分查找算法:阶乘函数后 K 个零

涉及知识点 二分查找 数学 题目 f(x) 是 x! 末尾是 0 的数量。回想一下 x! 1 * 2 * 3 * … * x&#xff0c;且 0! 1 。 例如&#xff0c; f(3) 0 &#xff0c;因为 3! 6 的末尾没有 0 &#xff1b;而 f(11) 2 &#xff0c;因为 11! 39916800 末端有 2 个 0 。 给定 k&a…

Python--列表及其应用场景

1.为什么需要列表 思考&#xff1a;有一个人的姓名(laowang)怎么书写存储程序&#xff1f; 用 变量。如&#xff1a;name laowang 但是&#xff0c;如果要记录很多人的名字&#xff0c;怎么办&#xff1f; 思考&#xff1a; 如果一个班级100位学生&#xff0c;每个人的…

17 Linux 中断

一、Linux 中断简介 1. Linux 中断 API 函数 ① 中断号 每个中断都有一个中断号&#xff0c;通过中断号可以区分出不同的中断。在 Linux 内核中使用一个 int 变量表示中断号。 ② request_irq 函数 在 Linux 中想要使用某个中断是需要申请的&#xff0c;request_irq 函数就是…

【Unity ShaderGraph】| 如何快速制作一个炫酷的 全息投影效果

前言 【Unity ShaderGraph】| 如何快速制作一个炫酷的 全息投影效果一、效果展示二、 全息投影效果 前言 本文将使用ShaderGraph制作一个 炫酷的 全息投影效果 &#xff0c;可以直接拿到项目中使用。对ShaderGraph还不了解的小伙伴可以参考这篇文章&#xff1a;【Unity Shader…