WPF MVVM 数据验证详解

WPF数据验证概述

WPF中,Binding认为从数据源出去的数据都是正确的,所以不进行校验;只有从数据目标回传的数据才有可能是错误的,需要校验。

在WPF应用程序中实现数据验证功能时,最常用的办法是将数据绑定与验证规则关联在一起,对于绑定数据的验证,系统采用如下所示的机制:

WPF MVVM 数据验证详解

使用WPF数据绑定模型可以将ValidationRules与Binding对象相关联。当绑定目标的属性向绑定源属性传递属性时(仅限TwoWay或OneWayToSource模式),执行ValidationRule中的Validate方法,实现对界面输入数据的验证。

WPF提供了两种内置的验证规则和一个自定义验证规则。

  • 内置的ExceptionValidationRule验证规则:用于检查在“绑定源属性”的更新过程中引发的异常,即在更新源时,如果有异常(比如类型不匹配)或不满足条件它会自动将异常添加到错误集合中。此种验证方式若实现自定义的逻辑验证,通常设置数据源的属性的Set访问器,在Set访问器中,根据输入的值结合逻辑,使用throw抛出相应的异常。
  • 内置的DataErrorValidationRule验证规则: 用于检查由源对象的IDataErrorInfo实现所引发的错误,要求数据源对象实现System.ComponentModel命名控件的IDataErrorInfo接口。
  • 自定义验证规则: 除了可直接使用内置的验证规则外,还可以自定义从ValidationRule类派生的类,通过在派生类中实现Validate方法来创建自定义的验证规则。
验证机制 说明
异常 通过在某个 Binding 对象上设置 ValidatesOnExceptions 属性,如果在尝试对源对象属性设置已修改的值的过程中引发异常,则将为该 Binding 设置验证错误。
IDataErrorInfo 通过在绑定数据源对象上实现 IDataErrorInfo 接口并在 Binding 对象上设置 ValidatesOnDataErrors 属性,Binding 将调用从绑定数据源对象公开的 IDataErrorInfo API。如果从这些属性调用返回非 null 或非空字符串,则将为该 Binding 设置验证错误。
ValidationRules Binding 类具有一个用于提供 ValidationRule 派生类实例的集合的属性。这些 ValidationRules 需要覆盖某个 Validate 方法,该方法由 Binding 在每次绑定控件中的数据发生更改时进行调用。如果 Validate 方法返回无效的 ValidationResult 对象,则将为该 Binding 设置验证错误。

相关文章:

MVVM模式下的输入校验(IDataErrorInfo + DataAnnotations)

IDataErrorInfo官方案例

ValidationRules官方案例

数据校验时如何编写View

数据注释

DataAnnotations用于配置模型类,它将突出显示最常用的配置。许多.NET应用程序(例如ASP.NET MVC)也可以理解DataAnnotations,这些应用程序允许这些应用程序利用相同的注释进行客户端验证。DataAnnotation属性将覆盖默认的Code-First约定。

System.ComponentModel.DataAnnotations包含以下影响列的可空性或列大小的属性。

  • Key
  • Timestamp
  • ConcurrencyCheck
  • Required
  • MinLength
  • MaxLength
  • StringLength

System.ComponentModel.DataAnnotations.Schema命名空间包括以下影响数据库架构的属性。

  • Table
  • Column
  • Index
  • ForeignKey
  • NotMapped
  • InverseProperty

总结:给类的属性加上描述性的验证信息,方便数据验证。

相关文章:

System.ComponentModel.DataAnnotations

DataAnnotations

Entity Framework Code First (三)Data Annotations

System.ComponentModel.DataAnnotations 命名空间

ASP.NET动态数据模型概述

适用场景对比与选择

  • 内置的ExceptionValidationRule验证规则:异常实现最为简单,但是会影响性能。不适合组合校验且Model里需要编写过重的校验代码。除了初始测试外,不要使用异常进行验证。
  • 内置的DataErrorValidationRule验证规则:MVVM或等效模式中,对于模型中的错误更优先,是比较普遍且灵活的校验实现方式。验证逻辑保存在视图模型中,易于实施和维护,完全控制ViewModel中的所有字段。
  • ValidationRule自定义验证规则:MVVM或等效模式中,对视图的错误更优先,适合在用户控件或者自定义控件场合使用。适合在单独的类中维护验证规则,提高可重用性,例如:可以实现所需的字段验证类,从而在整个程序中重用它。

总结:按照使用优先级进行排序

  1. IDataErrorInfo+DataAnnotation(后面会讲到) 进行组合验证,最为灵活复用性也高,MVVM推荐优先使用。
  2. IDataErrorInfo验证,涉及较多判断语句,最好配合DataAnnotation 进行验证。
  3. ValidationRule适合在用户控件或者自定义控件场合使用。
  4. 异常验证除了初始测试外,不推荐使用,虽然实现简单但是会影响性能。

IDataErrorInfo-内置的DataErrorValidationRule实现验证

使用DataErrorValidationRule这种验证方式,需要数据源的自定义类继承IDataErrorInfo接口,DataErrorValidationRule的使用方法由两种:

  • 在Binding的ValidationRules的子元素中声明该验证规则,这种方式只能用XAML来描述。
  • 直接在Binding的属性中指定该验证规则,这种方式用法比较简单,而且还可以在C#代码中直接设置此属性。

这两种设置方式完全相同,一般使用第二种方式。

<TextBox Text="{Binding Path=StudentName,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged,ValidatesOnDataErrors=True}"

下面通过例子来说明具体用法。定义一个学生信息表,要求其学生成绩在0~100之间,学生的姓名长度在2-10个字符之间。

public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void bt1_Click(object sender, RoutedEventArgs e)
        {
            //06 添加Click事件获取当前对象的信息
            MyStudentValidation myTemp = this.FindResource("myData") as MyStudentValidation;
            string sTemp = myTemp.StudentName;
            double dTemp = myTemp.Score;
            MessageBox.Show($"学生姓名为{sTemp}学生成绩为{dTemp}");
        }
    }

//01 自定义类MyStudentValidation,使用IDataErrorInfo接口
public class MyStudentValidation : IDataErrorInfo
    {
        public MyStudentValidation()
        {
            StudentName = "张三";
            Score = 90;
        }
        public MyStudentValidation(string studentName, double score)
        {
            StudentName = studentName;
            Score = score;
        }

        public string StudentName { get; set; }
        public double Score { get; set; }

        #region 实现IDataErrorInfo接口的成员
        public string Error => null;

        public string this[string columnName]
        {
            get {
                string result = null;
                switch (columnName)
                {
                    case "StudentName":
                        //设置StudentName属性的验证规则
                        int len = StudentName.Length;
                        if (len < 2 || len > 10)
                        {
                            result = "学生姓名必须2-10个字符";
                        }
                        break;
                    case "Score":
                        //设置Score属性的验证规则
                        if (Score < 0 || Score > 100)
                        {
                            result = "分数必须在0-100之间";
                        }
                        break;
                }
                return result;
            }
        }
        #endregion
    }
<!--03 引入C#自定义的类,存入Winow的资源里-->
<Window.Resources>
        <local:MyStudentValidation x:Key="myData"/>
    </Window.Resources>

<Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
<!--04 绑定数据源到Grid控件的DataContext属性-->
        <Grid.DataContext>
            <Binding Source="{StaticResource ResourceKey=myData}"/>
        </Grid.DataContext>
<!--02 设计外观-->
        <TextBlock Grid.Column="0" HorizontalAlignment="Right" VerticalAlignment="Center" Grid.Row="0" Text="学生姓名:"/>
        <TextBlock Grid.Column="0" HorizontalAlignment="Right" VerticalAlignment="Center" Grid.Row="1" Text="学生分数:"/>
<!--05 定义两个TextBox绑定到StudentName和Score两个属性上,并设置采用DataErrorValidationRule,在Binding中设置验证规则-->
        <TextBox x:Name="txt1" Margin="20" Grid.Column="1" Grid.Row="0"
                 Text="{Binding Path=StudentName,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged,ValidatesOnDataErrors=True}"/>
        <TextBox x:Name="txt2" Margin="20" Grid.Column="1" Grid.Row="1"
                 Text="{Binding Path=Score,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged,ValidatesOnDataErrors=True}"/>
        <Button x:Name="bt1" Grid.Row="2" Grid.ColumnSpan="2" Content="点击" Width="130" Margin="5"
                Click="bt1_Click"/>
</Grid>

从执行的结果来看,当验证出现错误时,系统默认给出一种验证错误的显示方式(控件以红色边框包围),但是需要注意两点:

  • 产生验证错误,验证后的数据仍然会更改数据源的值。
  • 如果系统出现异常,如成绩值输入12x,则系统不会显示错误,空间上的输入值也不会赋值到数据源。这种情况下,需要使用ExceptionValidationRule。

异常-利用内置的ExceptionValidationRule实现验证

当绑定目标的属性值向绑定源的属性值赋值时引发异常所产生的验证。通常设置数据源的属性的Set访问器,在Set访问器中,根据输入的值结合逻辑,使用throw抛出相应的异常。

ExceptionValidationRule也有两种用法:

  • 一种是在Binding的ValidationRules的子元素中声明该验证规则,这种方式只能用XAML来描述。
  • 另一种是直接在Binding属性中指定该验证规则,这种方式简单直观,一般使用这种方式。

例如上例中,对于Score对应的TextBox,再加入ExceptionValidationRule验证规则:

<TextBox x:Name="txt2" Margin="20" Grid.Column="1" Grid.Row="1"
Text="{Binding Path=Score,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged,ValidatesOnDataErrors=True,ValidatesOnExceptions=True}"/>

相关文章:

ExceptionValidationRule验证规则

ValidationRule-自定义规则实现验证

若要创建一个自定义的校验条件,需要声明一个类,并让这个类派生自ValidationRule类。ValidationRule只有一个名为Validate的方法需要我们实现,这个方法的返回值是一个ValidationResult类型的实例。这个实例携带者两个信息:

  • bool类型的IsValid属性告诉Binding回传的数据是否合法。
  • object类型(一般是存储一个string)的ErrorContent属性告诉Binding一些信息,比如当前是进行什么操作而出现的校验错误等。

示例:以一个Slider为数据源,它的滑块可以从Value=0滑到Value=100;同时,以一个TextBox为数据目标,并通过Validation限制它只能将20-50之间的数据传回数据源。

<!--01 设置外观-->
<StackPanel>
    <TextBox x:Name="textBox1" Margin="5"/>
    <Slider x:Name="slider1" Minimum="0" Maximum="100" Margin="5" Value="30"/>
</StackPanel>
public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
//03 运用自定义规则验证数据
            Binding binding = new Binding("Value");
            binding.Source = slider1;
            binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
            //加载校验条件
            binding.ValidationRules.Add(new MyValidationRule());
            textBox1.SetBinding(TextBox.TextProperty,binding);
        }
    }

//02 自定义规则类
public class MyValidationRule : ValidationRule
    {
        public override ValidationResult Validate(object value, CultureInfo cultureInfo)
        {
            double d = 0;
            if (double.TryParse((string)value,out d)&&(d>=20 &&d<=50))
            {
                return new ValidationResult(true, "OK");
            }
            else
            {
                return new ValidationResult(false, "Error");
            }
        }
    }

DataAnnotations-数据注释实现验证

Data Annotations是在Asp.Net中用于表单验证的,它通过Attribute直接标记字段的有效性,简单且直观。在非Asp.Net程序中(如控制台程序),我们也可以使用Data Annotations进行手动数据验证的。

使用System.ComponentModel.DataAnnotations验证字段数据正确性

使用Data Annotations进行手动数据验证

概述

.NET Framework为我们提供了一组可用于验证对象的属性。通过使用命名空间,System.ComponentModel.DataAnnotations我们可以使用验证属性来注释模型的属性。

有一些属性可以根据需要标记属性,设置最大长度等等。例如:

public class Game
{
    [Required]
    [StringLength(20)]
    public string Name { get; set; }
 
    [Range(0, 100)]
    public decimal Price { get; set; }
}

要检查实例是否有效,我们可以使用以下代码:

Validator.TryValidateObject(obj, new ValidationContext(obj), results, true);

 // 摘要:
        //通过使用验证上下文、验证结果集合和用于指定是否验证所有属性的值,确定指定的对象是否有效。
        //
        // 参数:
        //   instance:
        //     要验证的对象。
        //
        //   validationContext:
        //     用于描述要验证的对象的上下文。
        //
        //   validationResults:
        //     用于包含每个失败的验证的集合。
        //
        //   validateAllProperties:
        //     若为 true,则验证所有属性。若为 false,则只需要验证所需的特性。
        //
        // 返回结果:
        //     如果对象有效,则为 true;否则为 false。
        //
        // 异常:
        //   T:System.ArgumentNullException:
        //     instance 为 null。
        //
        //   T:System.ArgumentException:
        //     instance 与 validationContext 上的 System.ComponentModel.DataAnnotations.ValidationContext.ObjectInstance
        //     不匹配。
        public static bool TryValidateObject(object instance, ValidationContext validationContext, ICollection<ValidationResult> validationResults, bool validateAllProperties);

true如果对象没有任何错误或对象确实有错误,false则返回。并且该参数results填充有错误(如果有)。可以在MSDN文档中找到此方法的完整定义

也可以创建自己的属性。您要做的就是从继承ValidationAttribute。在下面的示例中,该属性将检查该值是否可被7整除。否则,它将返回错误消息。

public class DivisibleBy7Attribute : ValidationAttribute
{
    public DivisibleBy7Attribute()
        : base("{0} value is not divisible by 7")
    {
    }
 
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        decimal val = (decimal)value;
 
        bool valid = val % 7 == 0;
 
        if (valid)
            return null;
 
        return new ValidationResult(base.FormatErrorMessage(validationContext.MemberName)
            , new string[] { validationContext.MemberName });
    }
}

并在要验证的对象中:

[DivisibleBy7]
public decimal Price { get; set; }

如果验证失败,它将返回以下错误消息:
WPF MVVM 数据验证详解

所有内置验证属性

一个验证特性的完整列表可以在MSDN文档中找到

控制台案例

class Program
    {
        static void Main(string[] args)
        {
           Person person = new Person()
            {
                Name = "",
                Email = "aaaa",
                Age = 222,
                Phone = "1111",
                Salary = 200
            };
            var result = ValidatetionHelper.IsValid(person);
            if (!result.IsVaild)
            {
                foreach (ErrorMember errormember in result.ErrorMembers)
                {
                    Console.WriteLine($"{errormember.ErrorMemberName}:{errormember.ErrorMessage}");
                }
            }
            Console.ReadLine();
        }
    }

#region 实现一个Person类,里面包含几个简单的属性,然后指定几个Attribute
    //实现一个Person类,里面包含几个简单的属性,然后指定几个Attribute
    [AddINotifyPropertyChangedInterface]
    public class Person
    {
        [Required(ErrorMessage = "{0}必须填写,不能为空")]
        [DisplayName("姓名")]
        public string Name { get; set; }

        [Required(ErrorMessage = "{0}必须填写,不能为空")]
        [RegularExpression(@"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}", ErrorMessage = "{0}邮件格式不正确")]
        public string Email { get; set; }

        [Required(ErrorMessage = "{0}必须填写,不能为空")]
        [Range(18, 100, ErrorMessage = "{0}年满18岁小于100岁方可申请")]
        public int Age { get; set; }

        [Required(ErrorMessage = "{0}手机号不能为空")]
        [StringLength(11, MinimumLength = 11, ErrorMessage = "{0}请输入正确的手机号")]
        public string Phone { get; set; }

        [Required(ErrorMessage = "{0}薪资不能低于本省最低工资2000")]
        [Range(typeof(decimal), "2000.00", "9999999.00", ErrorMessage = "{0}请填写正确信息")]
        public decimal Salary { get; set; }

    }
    #endregion

#region 实现ValidatetionHelper静态类,这里主要用到的是Validator.TryValidateObject方法,
    //实现ValidatetionHelper静态类,这里主要用到的是Validator.TryValidateObject方法
    public class ValidatetionHelper
    {
        public static ValidResult IsValid(object value)
        {
            ValidResult result = new ValidResult();
            try
            {
                var validationContext = new ValidationContext(value);
                var results = new List<ValidationResult>();
                var isValid = Validator.TryValidateObject(value, validationContext, results,true);

                if (!isValid)
                {
                    result.IsVaild = false;
                    result.ErrorMembers = new List<ErrorMember>();
                    foreach (var item in results)
                    {
                        result.ErrorMembers.Add(new ErrorMember()
                        {
                            ErrorMessage = item.ErrorMessage,
                            ErrorMemberName = item.MemberNames.FirstOrDefault()
                        });
                    }
                }
                else
                {
                    result.IsVaild = true;
                }
            }
            catch (Exception ex)
            {
                result.IsVaild = false;
                result.ErrorMembers = new List<ErrorMember>();
                result.ErrorMembers.Add(new ErrorMember()
                {
                    ErrorMessage = ex.Message,
                    ErrorMemberName = "Internal error"
                });

            }
            return result;
        }
    }

    //返回的错误集合类
    public class ValidResult
    {
        public List<ErrorMember> ErrorMembers { get; set; }
        public bool IsVaild { get; set; }
    }

    //返回的错误信息成员
    public class ErrorMember
    {
        public string ErrorMessage { get; set; }
        public string ErrorMemberName { get; set; }
    }
    #endregion

WPF MVVM案例

数据属性的通知功能,通过NuGet引用PropertyChanged.Fody实现。

View

<Window.Resources>
        <Style TargetType="TextBox">
            <Setter Property="Width" Value="150"/>
        </Style>
    </Window.Resources>
<Window.DataContext>
        <local:MainWindowViewModel/>
    </Window.DataContext>
<DockPanel>
        <StackPanel Width="250" Height="250" Background="DodgerBlue" DockPanel.Dock="Left" VerticalAlignment="Top" Margin="5">
            <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,10,0,0">
                <TextBlock Text="姓名:" VerticalAlignment="Center"/>
                <TextBox Margin="10" Text="{Binding Person.Name,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged,ValidatesOnDataErrors=True}" />
            </StackPanel>
            <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
                <TextBlock Text="邮箱:" VerticalAlignment="Center"/>
                <TextBox Margin="10" Text="{Binding Person.Email}"/>
            </StackPanel>
            <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
                <TextBlock Text="年龄:" VerticalAlignment="Center"/>
                <TextBox Margin="10" Text="{Binding Person.Age}"/>
            </StackPanel>
            <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
                <TextBlock Text="手机:" VerticalAlignment="Center"/>
                <TextBox Margin="10" Text="{Binding Person.Phone}"/>
            </StackPanel>
            <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
                <TextBlock Text="工资:" VerticalAlignment="Center"/>
                <TextBox Margin="10" Text="{Binding Person.Salary}"/>
            </StackPanel>
            <Button Margin="10" Content="{Binding Bt1}" Command="{Binding BtCommand}"/>

        </StackPanel>
        <GroupBox Header="错误信息" DockPanel.Dock="Right" VerticalAlignment="Top">
            <ListView ItemsSource="{Binding ErrorMembers}" >
                <ListView.View>
                    <GridView>
                        <GridViewColumn DisplayMemberBinding="{Binding ErrorMemberName}"/>
                        <GridViewColumn DisplayMemberBinding="{Binding ErrorMessage}"/>
                    </GridView>
                </ListView.View>
            </ListView>
        </GroupBox>
    </DockPanel>

ViewModel

[AddINotifyPropertyChangedInterface]
public class MainWindowViewModel
    {
        public string Bt1 { get; set; } = "注册";
        public MainWindowModel Person { get; set; }
        public List<ErrorMember> ErrorMembers { get; set; } 

        public DelegateCommand BtCommand => new DelegateCommand(obj =>
        {
            Person = new MainWindowModel()
            {
                Name = "",
                Email = "",
                Age = 11,
                Phone = "123455111111",
                Salary = 2001
            };


            var result = ValidatetionHelper.IsValid(Person);
            if (!result.IsVaild)
            {
                ErrorMembers = result.ErrorMembers;
            }
        });
    }

Model

[AddINotifyPropertyChangedInterface]
public class MainWindowModel
{
    [Required(ErrorMessage ="{0}必须填写,不能为空")]
    [DisplayName("姓名")]
    public string Name { get; set; }

    [Required(ErrorMessage = "{0}必须填写,不能为空")]
    [RegularExpression(@"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}", ErrorMessage = "邮件格式不正确")]
    public string Email { get; set; }

    [Required(ErrorMessage ="{0}必须填写,不能为空")]
    [Range(18,100,ErrorMessage ="年满18岁小于100岁方可申请")]
    public int Age { get; set; }

    [Required(ErrorMessage ="{0}手机号不能为空")]
    [StringLength(11,MinimumLength =11,ErrorMessage ="{0}请输入正确的手机号")]
    public string Phone { get; set; }

    [Required(ErrorMessage ="{0}薪资不能低于本省最低工资2000")]
    [Range(typeof(decimal),"20000.00","9999999.00",ErrorMessage ="请填写正确信息")]
    public decimal Salary { get; set; }
}

DelegateCommand

public class DelegateCommand : ICommand
{
    private readonly Action<object> _executeAction;
    private readonly Func<object, bool> _canExecuteAction;

    public event EventHandler CanExecuteChanged;

    public DelegateCommand(Action<object> executeAction, Func<object, bool> canExecuteAction = null)
    {
        _executeAction = executeAction;
        _canExecuteAction = canExecuteAction;
    }

    public void Execute(object parameter) => _executeAction(parameter);

    public bool CanExecute(object parameter) => _canExecuteAction?.Invoke(parameter) ?? true;

    public void InvokeCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}

ValidatetionHelper

public class ValidatetionHelper
    {
        public static ValidResult IsValid(object value)
        {
            ValidResult result = new ValidResult();
            try
            {
                ValidationContext validationContext = new ValidationContext(value);
                var results = new List<ValidationResult>();
                var isValid = Validator.TryValidateObject(value,validationContext,results,true);

                if (!isValid)
                {
                    result.IsVaild = false;
                    result.ErrorMembers = new List<ErrorMember>();
                    foreach (var item in results)
                    {
                        result.ErrorMembers.Add(new ErrorMember() { 
                        ErrorMessage = item.ErrorMessage,
                        ErrorMemberName = item.MemberNames.FirstOrDefault()
                        });
                    }
                }
                else
                {
                    result.IsVaild = true;
                }
            }
            catch (Exception ex)
            {
                result.IsVaild = false;
                result.ErrorMembers = new List<ErrorMember>();
                result.ErrorMembers.Add(new ErrorMember()
                {
                    ErrorMessage = ex.Message,
                    ErrorMemberName = "Internal error"
                }); 
                
            }
            return result;
        }
    }

//返回的错误集合类
public class ValidResult
{
    public List<ErrorMember> ErrorMembers { get; set; }
    public bool IsVaild { get; set; }
}

//返回的错误信息成员
public class ErrorMember
    {
        public string ErrorMessage { get; set; }
        public string ErrorMemberName { get; set; }
    }

IDataErrorInfo + DataAnnotations实现验证

方案一

在实际开发中,我们还经常使用 EF 等 ORM 来做数据访问层,Model 通常会由这个中间件自动生成(利用T4等代码生成工具)。而他们通常是 POCO 数据类型,这时候如何能把属性的校验特性加入其中呢。这时候, TypeDescriptor.AddProviderTransparent + AssociatedMetadataTypeTypeDescriptionProvider 可以派上用场,它可以实现在另一个类中增加对应校验特性来增强对原类型的元数据描述。按照这种思路,将上面的 Person 类分离成两个文件:第一个分离类,可以想象是中间件自动生成的 Model 类。第二个分离类中实现 IDataErrorInfo,并定义一个Metadata 类来增加校验特性。(EF CodeFirst 也可以使用这一思路)

这部分推荐阅读原文,原文出处:MVVM模式下的输入校验IDataErrorInfo + DataAnnotations

方案二

实现一个继承IDataErrorInfo接口的抽象类PropertyValidateModel,以此实现IDataErrorInfo验证数据模型

第一步:为了告诉屏幕它应该验证某个属性,我们需要实现IDataErrorInfo接口。例如:

[AddINotifyPropertyChangedInterface]
public abstract class PropertyValidateModel : IDataErrorInfo
{
    //检查对象错误 
    public string Error { get { return null; } }

    //检查属性错误  
    public string this[string columnName]
    {
        get {
            var validationResults = new List<ValidationResult>();

            if (Validator.TryValidateProperty(
                    GetType().GetProperty(columnName).GetValue(this)
                    , new ValidationContext(this)
                    {
                        MemberName = columnName
                    }
                    , validationResults))
                return null;

            return validationResults.First().ErrorMessage;
        }
    }
}

第二步:数据注释的模型继承抽象类PropertyValidateModel,这样就可以模型将自动实现IDataErrorInfo

[AddINotifyPropertyChangedInterface]
public class Game:PropertyValidateModel
{
    [Required(ErrorMessage = "必填项")]
    [StringLength(6,ErrorMessage = "请输入正确的姓名")]
    public string Name { get; set; }

    [Required(ErrorMessage = "必填项")]
    [StringLength(5,ErrorMessage = "请输入正确的性别")]
    public string Genre { get; set; }

    [Required(ErrorMessage ="必填项")]
    [Range(18, 100,ErrorMessage = "年龄应在18-100岁之间")]
    public int MinAge { get; set; }
}

第三步:设置对应的Binding

<TextBox Text="{Binding Name,Mode=TwoWay,ValidatesOnDataErrors=True,UpdateSourceTrigger=PropertyChanged,NotifyOnValidationError=True}" Margin="10"/>

第四步:实现控件的错误信息提示

<Window.Resources>
        <Style TargetType="TextBox">
            <Setter Property="Validation.ErrorTemplate">
                <Setter.Value>
                    <ControlTemplate>
                        <StackPanel>
                            <Border BorderThickness="2" BorderBrush="DarkRed">
                                <StackPanel>
                                    <AdornedElementPlaceholder    
                                x:Name="errorControl" />
                                </StackPanel>
                            </Border>
                            <TextBlock Text="{Binding AdornedElement.ToolTip    
                        , ElementName=errorControl}" Foreground="Red" />
                        </StackPanel>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="true">
                    <Setter Property="BorderBrush" Value="Red" />
                    <Setter Property="BorderThickness" Value="1" />
                    <Setter Property="ToolTip"    
                Value="{Binding RelativeSource={RelativeSource Self}    
                    , Path=(Validation.Errors)[0].ErrorContent}" />
                </Trigger>
            </Style.Triggers>
        </Style>
</Window.Resources>

其他实现思路

主窗体

<Window x:Class="AttributeValidation.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:AttributeValidation"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <Style TargetType="{x:Type TextBox}">
            <Setter Property="ToolTip">
                <Setter.Value>
                    <Binding RelativeSource="{RelativeSource Self}" Path="(Validation.Errors)[0].ErrorContent" />
                </Setter.Value>
            </Setter>
            <Setter Property="Margin" Value="4,4" />
        </Style>
    </Window.Resources>
    <Grid Margin="0,0,151,0">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="120"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Label Content="_Name :" Margin="3,2" />
        <TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" Grid.Column="1" Margin="4,4" Grid.Row="0" />

        <Label Content="_Mobile :" Margin="3,2" Grid.Row="1" />
        <TextBox Text="{Binding Mobile, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" Grid.Column="1" Grid.Row="1" />

        <Label Content="_Phone number :" Margin="3,2" Grid.Row="2" />
        <TextBox Text="{Binding PhoneNumber,UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" Grid.Column="1" Grid.Row="2" />
        
        <Label Content="_Email :" Margin="3,2" Grid.Row="3" />
        <TextBox Text="{Binding Email, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" Grid.Column="1" Grid.Row="3" />

        <Label Content="_Address :" Margin="3,2" Grid.Row="4" />
        <TextBox Text="{Binding Address, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" Grid.Column="1" Grid.Row="4" />
    </Grid>
</Window>

Model类

public class Contact : ValidatorBase
{
    [Required(ErrorMessage = " Name is required.")]
    [StringLength(50, ErrorMessage = "No more than 50 characters")]
    [Display(Name = "Name")]
    public string Name { get; set; }

    [Required(ErrorMessage = "Email is required.")]
    [StringLength(50, ErrorMessage = "No more than 50 characters")]
    [RegularExpression(".+\\@.+\\..+", ErrorMessage = "Valid email required e.g. abc@xyz.com")]
    public string Email { get; set; }

    [Display(Name = "Phone Number")]
    [Required]
    [RegularExpression(@"^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$",
          ErrorMessage = "Entered phone format is not valid.")]
    public string PhoneNumber { get; set; }

    public string Address { get; set; }
    [RegularExpression(@"^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$",
                        ErrorMessage = "Entered phone format is not valid.")]
    public string Mobile { get; set; }
}

ValidatorBase类

public abstract class ValidatorBase : IDataErrorInfo
    {
        string IDataErrorInfo.Error
        {
            get {
                throw new NotSupportedException("IDataErrorInfo.Error is not supported, use IDataErrorInfo.this[propertyName] instead.");
            }
        }
        string IDataErrorInfo.this[string propertyName]
        {
            get {
                if (string.IsNullOrEmpty(propertyName))
                {
                    throw new ArgumentException("Invalid property name", propertyName);
                }
                string error = string.Empty;
                var value = GetValue(propertyName);
                var results = new List<System.ComponentModel.DataAnnotations.ValidationResult>(1);
                var result = Validator.TryValidateProperty(
                    value,
                    new ValidationContext(this, null, null)
                    {
                        MemberName = propertyName
                    },
                    results);
                if (!result)
                {
                    var validationResult = results.First();
                    error = validationResult.ErrorMessage;
                }
                return error;
            }
        }
        private object GetValue(string propertyName)
        {
            PropertyInfo propInfo = GetType().GetProperty(propertyName);
            return propInfo.GetValue(this);
        }
    }

常用类封装

XAML模板

<!--使用触发器将TextBox的ToolTip绑定到控件中遇到的第一个错误。通过设置TextBox的错误模板,我们可以通过访问AdornedElement并抓住包含错误消息的ToolTip来显示错误消息-->
<Window.Resources>
        <Style TargetType="TextBox">
            <Setter Property="Validation.ErrorTemplate">
                <Setter.Value>
                    <ControlTemplate>
                        <StackPanel>
                            <Border BorderThickness="2" BorderBrush="DarkRed">
                                <StackPanel>
                                    <AdornedElementPlaceholder    
                                x:Name="errorControl" />
                                </StackPanel>
                            </Border>
                            <TextBlock Text="{Binding AdornedElement.ToolTip    
                        , ElementName=errorControl}" Foreground="Red" />
                        </StackPanel>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="true">
                    <Setter Property="BorderBrush" Value="Red" />
                    <Setter Property="BorderThickness" Value="1" />
                    <Setter Property="ToolTip"    
                Value="{Binding RelativeSource={RelativeSource Self}    
                    , Path=(Validation.Errors)[0].ErrorContent}" />
                </Trigger>
            </Style.Triggers>
        </Style>
</Window.Resources>
<!--控件的Binding方式-->
<StackPanel>
        <TextBlock Text="姓名:" Margin="10"/>
        <TextBox Text="{Binding Name,Mode=TwoWay,ValidatesOnDataErrors=True,UpdateSourceTrigger=PropertyChanged,NotifyOnValidationError=True}" Margin="10"/>
        <TextBlock Text="性别:" Margin="10"/>
        <TextBox Text="{Binding Genre,Mode=TwoWay,ValidatesOnDataErrors=True,UpdateSourceTrigger=PropertyChanged,NotifyOnValidationError=True}" Margin="10"/>
        <TextBlock Text="年龄:" Margin="10"/>
        <TextBox Text="{Binding MinAge,Mode=TwoWay,ValidatesOnDataErrors=True,UpdateSourceTrigger=PropertyChanged,NotifyOnValidationError=True}" Margin="10"/>
</StackPanel>

PropertyValidateModel抽象类模板

[AddINotifyPropertyChangedInterface]
public abstract class PropertyValidateModel : IDataErrorInfo
{
    //检查对象错误 
    public string Error { get { return null; } }

    //检查属性错误  
    public string this[string columnName]
    {
        get {
            var validationResults = new List<ValidationResult>();

            if (Validator.TryValidateProperty(
                    GetType().GetProperty(columnName).GetValue(this)
                    , new ValidationContext(this)
                    {
                        MemberName = columnName
                    }
                    , validationResults))
                return null;

            return validationResults.First().ErrorMessage;
        }
    }
}  

ValidatetionHelper静态类模板

public class ValidatetionHelper
    {
        public static ValidResult IsValid(object value)
        {
            ValidResult result = new ValidResult();
            try
            {
                var validationContext = new ValidationContext(value);
                var results = new List<ValidationResult>();
                var isValid = Validator.TryValidateObject(value, validationContext, results,true);

                if (!isValid)
                {
                    result.IsVaild = false;
                    result.ErrorMembers = new List<ErrorMember>();
                    foreach (var item in results)
                    {
                        result.ErrorMembers.Add(new ErrorMember()
                        {
                            ErrorMessage = item.ErrorMessage,
                            ErrorMemberName = item.MemberNames.FirstOrDefault()
                        });
                    }
                }
                else
                {
                    result.IsVaild = true;
                }
            }
            catch (Exception ex)
            {
                result.IsVaild = false;
                result.ErrorMembers = new List<ErrorMember>();
                result.ErrorMembers.Add(new ErrorMember()
                {
                    ErrorMessage = ex.Message,
                    ErrorMemberName = "Internal error"
                });

            }
            return result;
        }
    }

模型模板

[AddINotifyPropertyChangedInterface]
public class MainWindowModel
{
    [Required(ErrorMessage ="{0}必须填写,不能为空")]
    [DisplayName("姓名")]
    public string Name { get; set; }

    [Required(ErrorMessage = "{0}必须填写,不能为空")]
    [RegularExpression(@"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}", ErrorMessage = "邮件格式不正确")]
    public string Email { get; set; }

    [Required(ErrorMessage ="{0}必须填写,不能为空")]
    [Range(18,100,ErrorMessage ="年满18岁小于100岁方可申请")]
    public int Age { get; set; }

    [Required(ErrorMessage ="{0}手机号不能为空")]
    [StringLength(11,MinimumLength =11,ErrorMessage ="{0}请输入正确的手机号")]
    public string Phone { get; set; }

    [Required(ErrorMessage ="{0}薪资不能低于本省最低工资2000")]
    [Range(typeof(decimal),"20000.00","9999999.00",ErrorMessage ="请填写正确信息")]
    public decimal Salary { get; set; }
}

IDataErrorInfo模板

//01 自定义类MyStudentValidation,使用IDataErrorInfo接口
public class MyStudentValidation : IDataErrorInfo
    {
        public MyStudentValidation()
        {
            StudentName = "张三";
            Score = 90;
        }
        public MyStudentValidation(string studentName, double score)
        {
            StudentName = studentName;
            Score = score;
        }

        public string StudentName { get; set; }
        public double Score { get; set; }
    
		//IDataErrorInfo模板
        #region 实现IDataErrorInfo接口的成员
        public string Error => null;

        public string this[string columnName]
        {
            get {
                string result = null;
                switch (columnName)
                {
                    case "StudentName":
                        //设置StudentName属性的验证规则
                        int len = StudentName.Length;
                        if (len < 2 || len > 10)
                        {
                            result = "学生姓名必须2-10个字符";
                        }
                        break;
                    case "Score":
                        //设置Score属性的验证规则
                        if (Score < 0 || Score > 100)
                        {
                            result = "分数必须在0-100之间";
                        }
                        break;
                }
                return result;
            }
        }
        #endregion
}

ValidationRule模板

public class MyValidationRule : ValidationRule
    {
        public override ValidationResult Validate(object value, CultureInfo cultureInfo)
        {
            double d = 0;
            if (double.TryParse((string)value,out d)&&(d>=20 &&d<=50))
            {
                return new ValidationResult(true, "OK");
            }
            else
            {
                return new ValidationResult(false, "Error");
            }
        }
}

WPF MVVM 数据验证详解

上一篇:C#发送腾讯企业邮箱


下一篇:SQL开发中容易忽视的一些小地方(二)