WPF數據驗證概述
WPF中,Binding認為從數據源出去的數據都是正確的,所以不進行校驗;只有從數據目標回傳的數據才有可能是錯誤的,需要校驗。
在WPF應用程序中實現數據驗證功能時,最常用的辦法是將數據綁定與驗證規則關聯在一起,對於綁定數據的驗證,系統采用如下所示的機制:

使用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 設置驗證錯誤。 | 
相關文章:
數據注釋
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
Entity Framework Code First (三)Data Annotations
適用場景對比與選擇
- 內置的ExceptionValidationRule驗證規則:異常實現最為簡單,但是會影響性能。不適合組合校驗且Model里需要編寫過重的校驗代碼。除了初始測試外,不要使用異常進行驗證。
 - 內置的DataErrorValidationRule驗證規則:MVVM或等效模式中,對於模型中的錯誤更優先,是比較普遍且靈活的校驗實現方式。驗證邏輯保存在視圖模型中,易於實施和維護,完全控制ViewModel中的所有字段。
 - ValidationRule自定義驗證規則:MVVM或等效模式中,對視圖的錯誤更優先,適合在用戶控件或者自定義控件場合使用。適合在單獨的類中維護驗證規則,提高可重用性,例如:可以實現所需的字段驗證類,從而在整個程序中重用它。
 
總結:按照使用優先級進行排序
- IDataErrorInfo+DataAnnotation(后面會講到) 進行組合驗證,最為靈活復用性也高,MVVM推薦優先使用。
 - IDataErrorInfo驗證,涉及較多判斷語句,最好配合DataAnnotation 進行驗證。
 - ValidationRule適合在用戶控件或者自定義控件場合使用。
 - 異常驗證除了初始測試外,不推薦使用,雖然實現簡單但是會影響性能。
 
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}"/>
 
        相關文章:
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進行手動數據驗證的。
概述
.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; }
 
         
        所有內置驗證屬性
控制台案例
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");
            }
        }
}
 
       
