WPF Data Validation
確認使用者輸入的資料是否合乎規格是一個重大的課題,WPF 提供資料驗證(Data Validation)的辦法 。
WPF 透過驗證規則(Validation Rule)來進行驗證。內建的驗證規則有 ExceptionValidationRule 與 DataErrorValidationRule,程式師可以製造自己的客製化驗證規則(Custom Validation Rule)。
資料驗證在將資料更新到來源端時發生,因此 BindingMode 為 TwoWay或 OneWayToSource 時會使用到資料驗證。
要了解資料驗證如何運作,必須了解資料驗證的過程。
資料驗證過程(Validation Process)
根據 Data Binding Voweview,資料驗證依據下列步驟進行。
- 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.
- The binding engine then calls the converter, if one exists.
- 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.
- The binding engine sets the source property.
- 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.
- 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。