1. 程式人生 > 實用技巧 >WPF MVVM 資料驗證詳解

WPF MVVM 資料驗證詳解

目錄

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 設定驗證錯誤。

相關文章:

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; }

如果驗證失敗,它將返回以下錯誤訊息:

所有內建驗證屬性

一個驗證特性的完整列表可以在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. [email protected]")]
    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");
            }
        }
}