顯示具有 Validation 標籤的文章。 顯示所有文章
顯示具有 Validation 標籤的文章。 顯示所有文章
WPF Data Validation

WPF Data Validation

WPF Data Validation

確認使用者輸入的資料是否合乎規格是一個重大的課題,WPF 提供資料驗證(Data Validation)的辦法 。

WPF 透過驗證規則(Validation Rule)來進行驗證。內建的驗證規則有 ExceptionValidationRule 與 DataErrorValidationRule,程式師可以製造自己的客製化驗證規則(Custom Validation Rule)。

資料驗證在將資料更新到來源端時發生,因此 BindingMode 為 TwoWay或 OneWayToSource 時會使用到資料驗證。

要了解資料驗證如何運作,必須了解資料驗證的過程。

資料驗證過程(Validation Process)

根據 Data Binding Voweview,資料驗證依據下列步驟進行。

  1. The binding engine checks if there are any custom ValidationRule objects defined whose ValidationStep is set to RawProposedValue for that Binding, in which case it calls the Validate method on each ValidationRule until one of them runs into an error or until all of them pass.
  2. The binding engine then calls the converter, if one exists.
  3. If the converter succeeds, the binding engine checks if there are any custom ValidationRule objects defined whose ValidationStep is set to ConvertedProposedValue for that Binding, in which case it calls the Validate method on each ValidationRule that has ValidationStep set to ConvertedProposedValue until one of them runs into an error or until all of them pass.
  4. The binding engine sets the source property.
  5. The binding engine checks if there are any custom ValidationRule objects defined whose ValidationStep is set to UpdatedValue for that Binding, in which case it calls the Validate method on each ValidationRule that has ValidationStep set to UpdatedValue until one of them runs into an error or until all of them pass. If a DataErrorValidationRule is associated with a binding and its ValidationStep is set to the default, UpdatedValue, the DataErrorValidationRule is checked at this point. This is also the point when bindings that have the ValidatesOnDataErrors set to true are checked.
  6. The binding engine checks if there are any custom ValidationRule objects defined whose ValidationStep is set to CommittedValue for that Binding, in which case it calls the Validate method on each ValidationRule that has ValidationStep set to CommittedValue until one of them runs into an error or until all of them pass.

內建驗證規則中,ExceptionValidationRule 在步驟4發生作用,DataErrorValidationRule 在步驟5發生作用。

  • A ExceptionValidationRule checks for exceptions thrown during the update of the binding source property.
  • A DataErrorValidationRule object checks for errors that are raised by objects that implement the IDataErrorInfo interface.

實作

這裡,利用 Prism 6來進行實作。

ViewA 裡面有兩個需要輸入資料的文字方塊,前者的資料繫結來源需要長度介於1到10的字串,後者的資料繫結來源類別為int?而且若有數值則其值必須介於10與50之間。另外又一個Button,該button在前面兩文字方塊的內容都符合規格時才有能(Enable)。

ViewTop 有一個 Button 及一個ViewA。該Button在ViewA裡的兩個文字方塊的內容都符合規格時才有能(Enable)。

ViewModel端

ViewA 的 ViewModel 是 ViewAViewModel,其程式碼如下:

public class ViewAViewModel : BindableBase, IDataErrorInfo
{
    private readonly IDictionary<string, string> errors = new Dictionary<string, string>();
    public event EventHandler ErrorChanged;
    public ViewAViewModel()
    {
        SaveCommand = new DelegateCommand(Save, CanSave);
        Validate();
        PropertyChanged += OnPropertyChanged;
    }
    private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        this.Validate();
        this.SaveCommand.RaiseCanExecuteChanged();
        OnErrorChanged(null);
    }
    private string _Message;
    public string Message
    {
        get { return _Message; }
        set { SetProperty(ref _Message, value); }
    }
    private int? _Price;
    public int? Price
    {
        get { return _Price; }
        set { SetProperty(ref _Price, value); }
    }
    #region IDataErrorInfo Interface
    public string this[string columnName]
    {
        get
        {
            if (this.errors.ContainsKey(columnName))
            {
                return this.errors[columnName];
            }
            return null;
        }
        set
        {
            this.errors[columnName] = value;
        }
    }
    public string Error
    {
        get
        {
            // Not implemented because we are not consuming it in this quick start.
            // Instead, we are displaying error messages at the item level.
            throw new NotImplementedException();
        }
    }
    #endregion
    public DelegateCommand SaveCommand { get; private set; }
    public int GetErrorCount()
    {
        return this.errors.Count;
    }
    private void Save()
    {
    }
    private bool CanSave()
    {
        return this.errors.Count == 0;
    }
    private void Validate()
    {
        if (this.Price != null && (this.Price < 10 || this.Price>50))
        {
            this["Price"] = "Price: null or integer between 10 and 50";
        }
        else
        {
            this.ClearError("Price");
        }
        if (string.IsNullOrEmpty(this.Message) || this.Message.Length>10)
        {
            this["Message"] = "Message: non null string with length between 1 and 10.";
        }
        else
        {
            this.ClearError("Message");
        }
    }
    private void ClearError(string columnName)
    {
        if (this.errors.ContainsKey(columnName))
        {
            this.errors.Remove(columnName);
        }
    }
    protected virtual void OnErrorChanged(EventArgs e)
    {
        ErrorChanged?.Invoke(this, e);
    }
}

說明如下:

  • public string Message;當作第一個文字方塊繫結的路徑(Path),用 private int? _Price; 當作第二個文字方塊繫結的路徑。
  • 利用private readonly IDictionary<string, string> errors 實做介面 IDataErrorInfo 以便提供 DataErrorValidationRule。
  • 利用 public event EventHandler ErrorChanged; 提供外界註冊資料驗證改變事件的處理函數。
  • 每次資料改變時,在私有函數 OnPropertyChanged 中,利用私有的 Vilidate 函數更新資料的驗證結果,並且執行資料驗證改變事件(ErrorChanged)的處理函數。
  • 提供 public int GetErrorCount() 供外界了解資料錯誤的數目。

另一方面,ViewTop 的 ViewModel 是 ViewTopViewModel,其程式碼如下:

public class ViewTopViewModel : BindableBase
{
    private string _Message;
    public string Message
    {
        get { return _Message; }
        set { SetProperty(ref _Message, value); }
    }
    public ViewTopViewModel()
    {
        _ChildVM = new ViewAViewModel();
        _ChildVM.ErrorChanged += _ChildVM_ErrorChenged;
        SaveAllCommand = new DelegateCommand(() => { Message = "Done"; }, () => { return ChildVM.GetErrorCount() == 0; });
    }
    private void _ChildVM_ErrorChenged(object sender, EventArgs e)
    {
        SaveAllCommand.RaiseCanExecuteChanged();
    }
    public DelegateCommand SaveAllCommand { get; private set; }
    private ViewAViewModel _ChildVM;
    public ViewAViewModel ChildVM
    {
        get { return _ChildVM; }
        set { SetProperty(ref _ChildVM, value); }
    }
}

說明如下:

  • _ChileVM 是 ViewAViewModel的一個案例。
  • SaveAllCommand 是一個ICommand的案例。
  • _ChildVM.ErrorChanged += _ChildVM_ErrorChenged;註冊_ChildVM.ErrorChanged事件的處理函數。
  • 在_ChildVM_ErrorChenged函數裡,呼叫 SaveAllCommand.RaiseCanExecuteChanged 讓 SaveAllCommand有能(Enable)或失能(Disable)。

View端

ViewA 的程式碼如下:

<UserControl x:Class="ModuleA.Views.ViewA"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:validate="clr-namespace:ModuleA.Validates"
             xmlns:prism="http://prismlibrary.com/"             
             prism:ViewModelLocator.AutoWireViewModel="True">
    <UserControl.Resources>
        <!-- Reference: http://www.codeproject.com/Tips/858492/WPF-Validation-Using-IDataErrorInfo -->
        <Style x:Key="TextErrorStyle" TargetType="{x:Type TextBox}">
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="True">
                    <!--<Setter Property="Background" Value="Red"/>-->
                    <Setter Property="ToolTip"
        Value="{Binding RelativeSource={x:Static RelativeSource.Self},
        Path=(Validation.Errors)[0].ErrorContent}"></Setter>
                </Trigger>
            </Style.Triggers>
            <Setter Property="Validation.ErrorTemplate">
                <Setter.Value>
                    <ControlTemplate >
                        <DockPanel>
                            <Border BorderBrush="Red" BorderThickness="1" Padding="2" CornerRadius="2">
                                <AdornedElementPlaceholder/>
                            </Border>
                        </DockPanel>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        <validate:StringToNullableNumberConverter x:Key="NullableNumberConverter" />
    </UserControl.Resources>
    <Grid>
        <StackPanel>
            <StackPanel Orientation="Horizontal" Margin="3">
                <TextBlock Text="Message" Width="60"  />
                <TextBox  Width="200" Text="{Binding Message, UpdateSourceTrigger=PropertyChanged,
                    ValidatesOnDataErrors=True,ValidatesOnExceptions=True}"
                    Style="{StaticResource TextErrorStyle}"
                    />
            </StackPanel>
            <StackPanel Orientation="Horizontal"  Margin="3">
                <TextBlock Text="Price"   Width="60" />
                <TextBox Width="200" Text="{Binding Price,  UpdateSourceTrigger=PropertyChanged,
                    ValidatesOnDataErrors=True, ValidatesOnExceptions=True,Converter={StaticResource NullableNumberConverter }}"
                    Style="{StaticResource TextErrorStyle}"
                    />
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <Button Content="Save" Command="{Binding SaveCommand}" />
            </StackPanel>
        </StackPanel>
    </Grid>
</UserControl>

說明如下:

  • <Style x:Key="TextErrorStyle" ...</style> 定義了資料驗證失敗時的樣式,取名TextErrorStyle。這個樣式在資料驗證失敗時,將控制項用紅框框起來,並且用Tooltip提供資料驗證失敗的原因。
  • <validate:StringToNullableNumberConverter x:Key="NullableNumberConverter" />提供資料轉換(Converter)StringToNullableNumberConverter的案例,取名NullableNumberConverter供繫結時的資料轉換使用(資料驗證過程的第 2 步)
  • <TextBox Width="200" Text="{Binding Message, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True,ValidatesOnExceptions=True}" Style="{StaticResource TextErrorStyle}" /> 將第一個文字方塊的內容繫結到 ViewAViewModel 的 Message 屬性,樣式採用 TextErrorStyle 。
  • <TextBox Width="200" Text="{Binding Price, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True, ValidatesOnExceptions=True,Converter={StaticResource NullableNumberConverter }}" Style="{StaticResource TextErrorStyle}" /> 將第二個文字方塊的內容繫結到 ViewAViewModel 的 Price 屬性,樣式採用TextErrorStyle。另外還使用NullableNumberConverter進行資料轉換。

ViewTop 的程式碼如下:

<UserControl x:Class="ModuleA.Views.ViewTop"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:views="clr-namespace:ModuleA.Views"
             xmlns:prism="http://prismlibrary.com/"             
             prism:ViewModelLocator.AutoWireViewModel="True">
    <DockPanel>
        <StackPanel Orientation="Horizontal" DockPanel.Dock="Top">
            <Button Command="{Binding SaveAllCommand}" Content="SaveAll" />
            <TextBox Text="{Binding Message}" Width="100"/>
        </StackPanel>
        <views:ViewA DataContext="{Binding ChildVM}" />
    </DockPanel>
</UserControl>

說明如下:

  • <Button Command="{Binding SaveAllCommand}" Content="SaveAll" /> 將Buttom的Command繫結到ViewTopViewModel的SaveCommand。
DataErrorValidationWithNegativeInteger

DataErrorValidationWithNegativeInteger

IDataErrorInfo

WPF的資料繫結功能強大,可是預設的繫結處理方式不見得符合我們的需要,必須加以客製化。

參考Data Binding Overview的內容,尤其是Data Validation裡的Validation Process小節,對資料繫結的運作有較深刻的了解,幫助我們客製化所需的繫結。

下面,我們考慮這樣的繫結。繫結目標為文字方塊的Text屬性,繫結來源的屬性Number之類別為int?,而且我們希望Number的合格數值為null或者介於-3與20間的整數。

我們從標準繫結開始,根據缺失逐步改良繫結:

  • 用標準繫結(下面的Step 1 至 Step 5),資料錯誤時,完全沒有提示。
  • 改良前述繫結(下面的Step 6),讓繫結目標的文字方塊採用客製化的樣式來提供錯誤提示,了解錯誤提示尚待改進的地方。
  • 改良前述繫結(下面的Step 7),讓繫結的資料轉換器採用客製化的轉換器,了解錯誤提示尚待改進的地方。
  • 改良前述繫結(下面的Step 8),將繫結的驗證規則利用繫結來源物件的IDataErrorInfo介面,提供合乎理想的錯誤提示,看看最後的結果。

Step 1

Visual Studio > New Project > WPF Application > 取名 WpfDataValidation > OK

建立名稱為WpfDataValidation的WPF Application專案

Step 2

Solution Explorer > 專案 WpfDataValidation 右鍵 > Add >
Class > 取名 MainWindowViewModel.cs

在專案中加入MVVM模式所需要的ViewModel,類別為MainWindowViewModel。

Step 3

為了讓 MainWindowViewModel 可以當作繫結的來源物件,在MainWindowViewModel.cs中的MainWindowViewModel類別實作INotifyPropertyChanged介面:

檔案的最前面加上

using System.ComponentModel;

MainWindowViewModel類別的第一行由

class MainWindowViewModel

改成

class MainWindowViewModel :  INotifyPropertyChanged

加入程式碼:

public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

Step 4

需要在MainWindowViewModel類別裡實作屬性Number,類別為int?。當Number發生改變時,觸發 PropertyChanged 事件。實作如下:

private int? _Number;
public int? Number
{
    get
    {
        return _Number;
    }
    set
    {
        if (value !=_Number)
        {
            _Number = value;
            OnPropertyChanged("Number");
        }
    }
}

Step 5

必須為繫結的目標端做些安排:

為了在MainWindow知道它的繫結來源物件之所在,在MainWindow.xaml.cs裡,

InitializeComponent();

的後面增加

DataContext = new MainWindowViewModel();

變成

InitializeComponent();
DataContext = new MainWindowViewModel();

為了設定繫結目標的物件與屬性,在MainWindow.xaml裡,將

<Grid>
</Gid>

改成

<StackPanel>
    <StackPanel Orientation="Horizontal"  Margin="5">
        <TextBlock Width="60" Text="Number:" />
       <TextBox Width="100"
       Text="{Binding Number,
           UpdateSourceTrigger=PropertyChanged,ValidatesOnExceptions=True}" />
    </StackPanel>
</StackPanel>

此時,執行程式。在文字方塊中輸入整數(例如,12)的時候表現正常,一旦輸入的字串無法轉變成整數(例如,12a)的時候,系統會將文字方塊加上紅色的外框,代表資料有誤。這樣,雖然告訴使用者資料錯誤,但沒告知發生了甚麼錯誤,必須加以改良。

Step 6

本步驟將改良文字方塊顯示錯誤的樣式,並提供錯誤訊息。

在MainWindow.xaml裡, <Window>的裡面,增加<Window.Resources>

<Window.Resources>
    <Style x:Key="TextErrorStyle" TargetType="{x:Type TextBox}">
        <Style.Triggers>
            <Trigger Property="Validation.HasError" Value="True">
                <!--<Setter Property="Background" Value="Red"/>-->
                <Setter Property="ToolTip"
    Value="{Binding RelativeSource={x:Static RelativeSource.Self},
    Path=(Validation.Errors)[0].ErrorContent}"></Setter>
            </Trigger>
        </Style.Triggers>
        <Setter Property="Validation.ErrorTemplate">
            <Setter.Value>
                <ControlTemplate >
                    <DockPanel>
                        <Border BorderBrush="Red" BorderThickness="1"
                         Padding="2" CornerRadius="2">
                            <AdornedElementPlaceholder/>
                        </Border>
                    </DockPanel>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</Window.Resources>

<TextBox Width="100"
Text="{Binding Number,
    UpdateSourceTrigger=PropertyChanged,ValidatesOnExceptions=True}" />

改成

<TextBox Width="100" Text="{Binding Number,
   UpdateSourceTrigger=PropertyChanged,ValidatesOnExceptions=True}"
   Style="{StaticResource TextErrorStyle}"  />

此時,執行程式。

在文字方塊中輸入整數(例如,12)的時候表現正常,一旦輸入的字串無法轉變成整數(例如,12a)的時候,文字方塊加上紅色的外框,紅色外框與方塊本身有少許的距離,代表資料有誤。此時,若將滑鼠hover在文字方塊上的時候,出現"input string was not in correct fromat"的提示。這樣,不只告訴使用者資料錯誤,而且告知發生了甚麼錯誤。

  • 可是,將文字方塊的字串改成空白的時候,提示的錯誤訊息為"input string was not in correct fromat"。這個不理想,因為我們希望空字串轉換成null。
  • 而且我們希望Number屬性的可能值為null或介於-3與20間的整數,但是輸入123的時候不會顯示錯誤。

所以,還需要進一步的改良。顯然,WPF如何將字串轉換成int?的預設轉換器(Converter)不能符合我們的需要,必須客製化自己的轉換器。

Step 7

本節,我們製作想要的轉換器,並且加以使用。

Solution Explorer > 專案 WpfDataValidation 右鍵 > Add > Class >
取名 StringToNullableNumber.cs。

在專案中加入檔案StringToNullableNumber.cs。

在 StringToNullableNumber.cs裡,檔案的前面加上兩行

using System.Globalization;
using System.Windows.Data;

將StringToNullableNumber.cs裡的StringToNullableNumber類別實作成:

public class StringToNullableNumber : IValueConverter
{
    public object Convert(object value, Type targetType,
       object parameter, CultureInfo culture)
    {
        return value;
    }
    public object ConvertBack(object value, Type targetType,
      object parameter, CultureInfo culture)
    {
        string stringValue = value as string;
        if (stringValue != null)
        {
            if (targetType == typeof(int?))
            {
                int result;
                if (int.TryParse(stringValue, out result))
                    return result;
                //return null;
                if (stringValue == "")
                    return null;
                else
                    return value;
            }
            if (targetType == typeof(decimal?))
            {
                decimal result;
                if (decimal.TryParse(stringValue, out result))
                    return result;
                //return null;
                if (stringValue == "")
                    return null;
                else
                    return value;
            }
        }
        return value;
    }
}

在MainWindow.xaml裡,</Window.Resource>的前面加上

<local:StringToNullableNumber x:Key="stringToNullableNumber" />

在MainWindow.xaml裡,將

<TextBox Width="100" Text="{Binding Number,
   UpdateSourceTrigger=PropertyChanged,ValidatesOnExceptions=True}"
   Style="{StaticResource TextErrorStyle}"  />

改成

<TextBox Width="100"
  Text="{Binding Number,
  UpdateSourceTrigger=PropertyChanged,ValidatesOnExceptions=True,
  Converter={StaticResource stringToNullableNumber}}"
  Style="{StaticResource TextErrorStyle}"  />

此時,執行程式。

  • 將文字方塊的字串改成空白的時候,不會有錯誤訊息,這是我們想要的。
  • 將文字方塊的字串改成"-"的時候有錯誤提示"input string was not in correct fromat",這是正常的,因為"-"無法轉成整數。
  • 將文字方塊的字串改成"-8"或"23"的時候,不會有錯誤提示,這不符合我們的需要,因為-8<-3以及23>20。因此,仍有改進的空間。

下面我們在繫結來源物件實作IDataErrorInfo介面,透過 IDataErrorInfo 提供更好的錯誤提示,完成我們的目標。

Step 8

在MainWindowViewModel類別裡實作IDataErrorInfo:

MainWindowViewModel類別的第一行由

class MainWindowViewModel :  INotifyPropertyChanged

改成

class MainWindowViewModel : IDataErrorInfo, INotifyPropertyChanged

加入程式碼:

public string this[string columnName]
{
    get
    {
        if (columnName == "Number")
        {
            if (_Number < -3 || _Number > 20)
                return string.Format("Number is {0}, but should be empty or integer between -3 and 20.",_Number);
            else
                return null;
        }
        return null;
    }
}
public string Error
{
    get
    {
        throw new NotImplementedException();
    }
}

在MainWindow.xaml裡,將

<TextBox Width="100"
  Text="{Binding Number,
  UpdateSourceTrigger=PropertyChanged,ValidatesOnExceptions=True,
  Converter={StaticResource stringToNullableNumber}}"
  Style="{StaticResource TextErrorStyle}"  />

改成

<TextBox Width="100" Text="{Binding Number,
    UpdateSourceTrigger=PropertyChanged,ValidatesOnExceptions=True,
    Converter={StaticResource stringToNullableNumber},
    ValidatesOnDataErrors=True}"
     Style="{StaticResource TextErrorStyle}"  />

此時,執行程式。

  • 將文字方塊的字串改成空白的時候,不會有錯誤訊息,這是我們想要的。
  • 將文字方塊的字串改成"-"的時候有錯誤提示"input string was not in correct fromat",這是正常的,因為"-"無法轉成整數。
  • 將文字方塊的字串改成"-8"的時候,會有錯誤提示"Number is -8, but should be empty or integer between -3 and 20.",符合我們的需要。
  • 事實上,只有輸入的字串為空白或者介於-3與20間的整數的時候才不會有錯誤提示。
技術提供:Blogger.