WPF 数据验证

WPF提供了能与数据绑定系统紧密协作的验证功能。提供了两种方法用于捕获非法值:

1、可在数据对象中引发错误。

可以在设置属性时抛出异常,通常WPF会忽略所有在设置属性时抛出的异常,但可以进行配置,从而显示更有帮助的可视化指示。另一种选择是在自定义的数据类中实现 INotifyDataErrorInfo 或 IDataErrorInfo 接口,从而可得到指示错误的功能而不会抛出异常。

2、可在绑定级别上定义验证。

这种方法可获得使用相同验证的灵活性,而不必考虑使用的是哪个控件。更好的是,因为是在不同类中定义验证,可以很容易的在存储类似数据类型的多个绑定中重用验证。

错误模板

错误模板使用的是装饰层,装饰层是位于普通窗口内容之上的绘图层。使用装饰层,可添加可视化装饰来指示错误,而不用替换控件背后的控件模板或改变窗口的布局。文本框的标准错误模板通过在相应文本框的上面添加红色的Border元素来指示发生了错误。可使用错误模板添加其他细节。

cs 复制代码
<Style TargetType="{x:Type TextBox}">
    <Setter Property="Validation.ErrorTemplate">
        <Setter.Value>
            <ControlTemplate>
                <DockPanel LastChildFill="True">
                    <TextBlock DockPanel.Dock="Right" Foreground="Red" FontSize="14" FontWeight="Bold" 
                                    ToolTip="{Binding ElementName=adornerPlaceholder, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}">*</TextBlock>
                    <Border BorderBrush="Green" BorderThickness="1">
                        <AdornedElementPlaceholder Name="adornerPlaceholder"></AdornedElementPlaceholder>
                    </Border>
                </DockPanel>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
    <Style.Triggers>
        <Trigger Property="Validation.HasError" Value="true">
            <Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"/>
        </Trigger>
    </Style.Triggers>
</Style>

在数据对象中引发错误

我这里分别尝试了 IDataErrorInfo 与 INotifyDataErrorInfo 接口,这俩需要分别对应使用 <DataErrorValidationRule/> 与 <NotifyDataErrorValidationRule/> 。

IDataErrorInfo 接口定义了Error字段,这个字段在结构有多个字段时,难以映射,于是在[]下标下实现逻辑。

INotifyDataErrorInfo 接口需要实现 HasError,GetErrors 两个函数,并需要在属性的set 访问器内实现校验逻辑,并管理Error信息。

总体来说,在数据对象中引发错误不是一个好选择,验证逻辑硬编码在数据对象中,不利于复用。

在绑定级别上定义验证

在绑定级别上定义验证是针对于数据类型自定义验证规则,可以对同一种数据类型进行复用。自定义验证规则需要继承自 ValidationRule 类,需要实现 Validate 函数,在其中完成自定义验证内容。

cs 复制代码
public class PriceRule : ValidationRule
{
    private decimal min = 0;
    private decimal max = decimal.MaxValue;
    public decimal Min
    {
        get => min;
        set => min = value;
    }
    public decimal Max
    {
        get => max;
        set => max = value;
    }
    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        decimal price = 0;
        try
        {
            if (((string)value).Length > 0)
            {
                price = Decimal.Parse((string)value, NumberStyles.Any, cultureInfo);
            }
        }
        catch
        {
            return new ValidationResult(false, "Illegal characters.");
        }
        if (price < min || price > max)
        {
            return new ValidationResult(false, "Not in the range " + Min + " to " + Max + ".");
        }
        else
        {
            return new ValidationResult(true, null);
        }
    }
}

验证多个值

如果需要执行对两个或更多个绑定值的验证,可以通过 BindingGroup 来实现,将需要校验的多个控件放置于同一个容器中,在容器级别应用验证规则,需要通过事件主动触发验证,通常是子组件失去焦点时。

cs 复制代码
<Grid Grid.Row="3" Grid.ColumnSpan="2" DataContext="{Binding Path=Person}" TextBoxBase.LostFocus="Grid_LostFocus">
    <Grid.ColumnDefinitions>
        <ColumnDefinition/>
        <ColumnDefinition/>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition/>
        <RowDefinition/>
    </Grid.RowDefinitions>
    <Grid.BindingGroup>
        <BindingGroup x:Name="personValidation">
            <BindingGroup.ValidationRules>
                <local:PersonRule/>
            </BindingGroup.ValidationRules>
        </BindingGroup>
    </Grid.BindingGroup>

    <TextBlock Grid.Row="0" Grid.Column="0">Person ID:</TextBlock>
    <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Path=ID}" />

    <TextBlock Grid.Row="1" Grid.Column="0">Person Name:</TextBlock>
    <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Path=Name}"/>
</Grid>
cs 复制代码
public class Person : ViewModelBase
{
    private string name = string.Empty;
    public string Name { get=>name; set=> SetProperty(ref name, value); }
    
    private string id = string.Empty;
    public string ID { get => id; set => SetProperty(ref id, value); }
}
public class PersonRule : ValidationRule
{
    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        BindingGroup bindingGroup = (BindingGroup)value;
        Person? person = bindingGroup.Items[0] as Person;

        string name = (string)bindingGroup.GetValue(person, "Name");
        string id = (string)bindingGroup.GetValue(person, "ID");
        if ((name == "") && (id == ""))
        {
            return new ValidationResult(false, "A Person requires a ID or Name.");
        }
        else
        {
            return new ValidationResult(true, null);
        }
    }
}

下面贴出完整的测试代码:

MainWindow.xaml

cs 复制代码
<Window x:Class="TestValidation.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:TestValidation"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Border Padding="7" Margin="7" Background="LightSteelBlue">
        <Grid x:Name="myGrid">
            <Grid.Resources>
                <Style TargetType="{x:Type TextBox}">
                    <Setter Property="Validation.ErrorTemplate">
                        <Setter.Value>
                            <ControlTemplate>
                                <DockPanel LastChildFill="True">
                                    <TextBlock DockPanel.Dock="Right" Foreground="Red" FontSize="14" FontWeight="Bold" 
                                                   ToolTip="{Binding ElementName=adornerPlaceholder, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}">*</TextBlock>
                                    <Border BorderBrush="Green" BorderThickness="1">
                                        <AdornedElementPlaceholder Name="adornerPlaceholder"></AdornedElementPlaceholder>
                                    </Border>
                                </DockPanel>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                    <Style.Triggers>
                        <Trigger Property="Validation.HasError" Value="true">
                            <Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"/>
                        </Trigger>
                    </Style.Triggers>
                </Style>
            </Grid.Resources>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="*"/>
                <RowDefinition Height="*"/>
                <RowDefinition Height="*"/>
                <RowDefinition Height="2*"/>
            </Grid.RowDefinitions>
           
            <TextBlock Grid.Row="0" Grid.Column="0">PersonAge:</TextBlock>
            <TextBox Grid.Row="0" Grid.Column="1" DataContext="{Binding Path=PersonAge}">
                <TextBox.Text>
                    <Binding Path="Age" NotifyOnValidationError="true">
                        <Binding.ValidationRules>
                            <DataErrorValidationRule/>
                        </Binding.ValidationRules>
                    </Binding>
                </TextBox.Text>
            </TextBox>

            <TextBlock Grid.Row="1" Grid.Column="0">PersonName:</TextBlock>
            <TextBox Grid.Row="1" Grid.Column="1" DataContext="{Binding Path=PersonName}">
                <TextBox.Text>
                    <Binding Path="Name" NotifyOnValidationError="true">
                        <Binding.ValidationRules>
                            <NotifyDataErrorValidationRule/>
                        </Binding.ValidationRules>
                    </Binding>
                </TextBox.Text>
            </TextBox>

            <TextBlock Grid.Row="2" Grid.Column="0">PersonPrice:</TextBlock>
            <TextBox Grid.Row="2" Grid.Column="1" DataContext="{Binding Path=PersonPrice}">
                <TextBox.Text>
                    <Binding Path="Price" NotifyOnValidationError="true">
                        <Binding.ValidationRules>
                            <local:PriceRule Min="0"/>
                        </Binding.ValidationRules>
                    </Binding>
                </TextBox.Text>
            </TextBox>

            <Grid Grid.Row="3" Grid.ColumnSpan="2" DataContext="{Binding Path=Person}" TextBoxBase.LostFocus="Grid_LostFocus">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition/>
                    <ColumnDefinition/>
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition/>
                    <RowDefinition/>
                </Grid.RowDefinitions>
                <Grid.BindingGroup>
                    <BindingGroup x:Name="personValidation">
                        <BindingGroup.ValidationRules>
                            <local:PersonRule/>
                        </BindingGroup.ValidationRules>
                    </BindingGroup>
                </Grid.BindingGroup>

                <TextBlock Grid.Row="0" Grid.Column="0">Person ID:</TextBlock>
                <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Path=ID}" />

                <TextBlock Grid.Row="1" Grid.Column="0">Person Name:</TextBlock>
                <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Path=Name}"/>
            </Grid>
        </Grid>
    </Border>
</Window>

MainWindow.xaml.cs

cs 复制代码
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace TestValidation;


public class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;
    protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    protected virtual bool SetProperty<T>(ref T member, T value, [CallerMemberName] string? propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(member, value))
        {
            return false;
        }
        member = value;
        OnPropertyChanged(propertyName);
        return true;
    }
}
public class PersonAge : ViewModelBase, IDataErrorInfo
{
    public string this[string columnName]
    {
        get
        {
            if(columnName == "Age")
            {
                if(Age < 0 || Age > 150)
                {
                    return "Age must not be less than 0 or greater than 150.";
                }
            }
            return null;
        }
    }
    private int age;
    public int Age { get => age; set => SetProperty(ref age, value); }
    public string Error => null;
}
public class PersonName : ViewModelBase, INotifyDataErrorInfo
{
    private string name = string.Empty;
    public string Name 
    {
        get => name;
        set
        {
            bool valid = true;
            foreach (char c in value)
            {
                if (!char.IsLetterOrDigit(c))
                {
                    valid = false;
                    break;
                }
            }
            if(!valid)
            {
                List<string> errors = new List<string> ();
                errors.Add("The Name can only contain letters and numbers.");
                SetErrors("Name", errors);
            }
            else
            {
                ClearErrors("Name");
            }
            SetProperty(ref name, value);
        }
    }

    private Dictionary<string, List<string>> errors = new Dictionary<string, List<string>>();
    private void SetErrors(string propertyName, List<string> propertyErrors)
    {
        errors.Remove(propertyName);
        errors.Add(propertyName, propertyErrors);
        if (ErrorsChanged != null)
            ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
    }
    private void ClearErrors(string propertyName)
    {
        errors.Remove(propertyName);
        if(ErrorsChanged != null)
            ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
    }

    public bool HasErrors
    {
        get { return errors.Count > 0; }
    }
    public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
    public IEnumerable GetErrors(string? propertyName)
    {
        if(!string.IsNullOrEmpty(propertyName) && errors.ContainsKey(propertyName))
        {
            return errors[propertyName];
        }
        return new List<string>();
    }
}
public class PersonPrice : ViewModelBase, INotifyPropertyChanged
{
    private decimal price = 0;
    public decimal Price
    {
        get => price; 
        set
        {
            SetProperty(ref price, value);
        }
    }
}

public class PriceRule : ValidationRule
{
    private decimal min = 0;
    private decimal max = decimal.MaxValue;
    public decimal Min
    {
        get => min;
        set => min = value;
    }
    public decimal Max
    {
        get => max;
        set => max = value;
    }
    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        decimal price = 0;
        try
        {
            if (((string)value).Length > 0)
            {
                price = Decimal.Parse((string)value, NumberStyles.Any, cultureInfo);
            }
        }
        catch
        {
            return new ValidationResult(false, "Illegal characters.");
        }
        if (price < min || price > max)
        {
            return new ValidationResult(false, "Not in the range " + Min + " to " + Max + ".");
        }
        else
        {
            return new ValidationResult(true, null);
        }
    }
}

public class Person : ViewModelBase
{
    private string name = string.Empty;
    public string Name { get=>name; set=> SetProperty(ref name, value); }
    
    private string id = string.Empty;
    public string ID { get => id; set => SetProperty(ref id, value); }
}
public class PersonRule : ValidationRule
{
    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        BindingGroup bindingGroup = (BindingGroup)value;
        Person? person = bindingGroup.Items[0] as Person;

        string name = (string)bindingGroup.GetValue(person, "Name");
        string id = (string)bindingGroup.GetValue(person, "ID");
        if ((name == "") && (id == ""))
        {
            return new ValidationResult(false, "A Person requires a ID or Name.");
        }
        else
        {
            return new ValidationResult(true, null);
        }
    }
}
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        myGrid.DataContext = this;
    }

    public PersonAge PersonAge { get; set; } = new PersonAge();
    public PersonName PersonName { get; set; } = new PersonName();
    public PersonPrice PersonPrice { get; set; } = new PersonPrice();
    public Person Person { get; set; } = new Person();

    private void Grid_LostFocus(object sender, RoutedEventArgs e)
    {
        personValidation.CommitEdit();
    }
}
相关推荐
界面开发小八哥1 小时前
界面控件DevExpress WPF中文教程:TreeList视图及创建分配视图
.net·wpf·界面控件·devexpress·ui开发
月落.3 小时前
WPF Prism中的区域(Region)管理
wpf·prism
林子漾1 天前
【paper】分布式无人水下航行器围捕智能目标
分布式·wpf
wyh要好好学习1 天前
C# WPF 记录DataGrid的表头顺序,下次打开界面时应用到表格中
开发语言·c#·wpf
lgcgkCQ2 天前
任务调度中心-XXL-JOB使用详解
java·wpf·定时任务·任务调度
Vicky&James2 天前
英雄联盟客户端项目:从跨平台Uno Platform到Win UI3的转换只需要30分钟
github·wpf·跨平台·英雄联盟·winui·unoplatform
就是有点傻2 天前
WPF中如何使用区域导航
wpf
她说彩礼65万2 天前
WPF程序设置单例启动(互斥体)
wpf
就是有点傻2 天前
WPF中Prism框架中 IContainerExtension 和 IRegionManager的作用
wpf
月落.2 天前
WPF中MVVM工具包 CommunityToolkit.Mvvm
wpf·mvvm