理解和使用WPF 驗證機制
首先建立一個demo用以學習和實驗WPF Data Validation機制。建立一個數據實體類:
public class Employee
{
public string Name { get; set; }
public int? Age { get; set; }
}
建立一個使用者控制元件或者視窗,用以輸入Name和Age,如下:
<Grid Width="400" Height="200">
<
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition/>
</
<TextBlock Text="User Name:"VerticalAlignment="Center"/>
<TextBox Grid.Column="1" x:Name="tb" Height="30">
<TextBox.Text>
<Binding Path="Name">
<Binding.ValidationRules>
<local:NotNullValidationRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<TextBlock Text="Age" Grid.Row="1"VerticalAlignment="Center"/>
<TextBox Grid.Row="1"Grid.Column="1" Height="30">
<TextBox.Text>
<Binding Path="Age">
<Binding.ValidationRules>
<local:NotNullValidationRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Button Grid.Row="2"Grid.Column="1" Content="Save" Width="60" Height="23"/>
</Grid>
在後置程式碼中連線資料上下文,如下:
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
Employee p = new Employee();
DataContext = p;
}
要執行此demo還需要建立一NotNullValidationRule 類,資料驗證的工作正是在此類中完成,此類的程式碼如下:
public class NotNullValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
if (value == null || string.IsNullOrWhiteSpace(value as string))
{
return new ValidationResult(false, "value cannot benull");
}
return ValidationResult.ValidResult;
}
}
可以看到,此類必須從ValidationRule派生,然後重寫Validate方法,引數value就是設定資料繫結的控制元件屬性所表示的值。在我們的示例中,value就是TextBox.Text的值,也就是使用者輸入的文字。驗證邏輯非常簡單,不再贅述。驗證完成後需要返回一個ValidationResult 物件表示驗證結果,如果驗證的資料無效,就需要為驗證結果指定一個字串作為錯誤資訊反饋給使用者。
好了,現在demo可以運行了,在表示Name的文字框中輸入一些字元,然後刪除所有剛才輸入的字元,最後按下tab鍵讓焦點離開改文字框。可以看見文字框出現了一個紅色邊框。顯然,紅色邊框不是很美觀,而且驗證錯誤資訊也沒有通過Tooltip的方式呈現出來,記得以前是可以的,現在用的是.NetFramework 4.5,是沒有Tooltip提示的。下面我們就自定義一下驗證出錯時的UI顯示。
驗證錯誤的顯示樣式是由Validation.ErrorTemplate來控制的,這是一個關聯屬性,型別是ControlTemplate,下面是一個驗證錯誤控制元件模板的示例:
<ControlTemplate x:Key="ErrorTempalte">
<StackPanel Orientation="Horizontal">
<StackPanel.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded" SourceName="bd">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="bd" Storyboard.TargetProperty="RenderTransform.ScaleX" From="0" To="1" Duration="0:0:0.2"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</StackPanel.Triggers>
<AdornedElementPlaceholder/>
<Border CornerRadius="3"BorderBrush="DarkMagenta" Background="#AAFF0000" BorderThickness="1" Padding="5 2" x:Name="bd">
<Border.RenderTransform>
<ScaleTransform/>
</Border.RenderTransform>
<TextBlock Foreground="White" VerticalAlignment="Center" Text="{Binding Path=/ErrorContent}"/>
</Border>
</StackPanel>
</ControlTemplate>
把以上模板應用到TextBox,
<TextBox Validation.ErrorTemplate="{StaticResource ErrorTempalte}">…
再次執行demo,當焦點離開文字框後,會有一個紅色的錯誤提示顯示在文字框的右邊,而且還有一個X放心的放大動畫,使介面變得有一些動感了。對於這個ControlTemplate,需要記住的是,模板根元素的DataContext是一個ValidationError物件的列表,而在上面的模板中,我們將列表中第一個物件的ErrorContent顯示了出來(實際上,當一個繫結有多個ValidationRule的時候,WPF繫結引擎一旦發現有一個ValidationRule驗證失敗,那麼後續的ValidationRule將不會被執行),ErrorContent屬性就是我們在建構函式中指定的錯誤資訊。另外一個需要注意的是,這個模板中的介面元素是顯示在AdornerLayer中的;要將顯示錯誤的介面元素顯示在被驗證控制元件的周圍,需要AdornedElementPlaceholder元素的支援,該元素是一個佔位符,跟被驗證控制元件有著同樣的位置和尺寸;而且還可以通過此類的AdornedElement屬性來訪問被驗證控制元件。
解決了錯誤資訊顯示的問題,還有下一個問題等著我們解決;假設有一個新增使用者的介面,而且所有欄位都是必須的,使用者在填寫了部分資訊後,點選了儲存按鈕;如果我們直接儲存,那麼大部分情況下會出錯,因為還存在無效值。但是你可能會想,我們已經為每個繫結設定了ValidationRule,為什麼這些ValidationRule沒有起作用呢?這是因為對於Binding來說,如果Target值沒有變化,那麼是不會引發驗證的;而且如果設定了繫結的UpdateSourceTrigger="LostFocus" 即使文字框的值變了,但是在文字框焦點離開之前,也是不會引發驗證。更糟糕的是,使用者根本就沒有對文字框做任何輸入,所以也就談不上焦點離開。所以這就要求我們在使用者點選儲存按鈕的時候,手動引發所有驗證操作,如果存在任何嚴重錯誤,那麼驗證錯誤就會像之前那樣滑動出來。所謂手動引發,指的是我們自己寫程式碼去引發。可以想象,這不是一個輕鬆的工作,首先需要獲取所有的BindingExpressionBase物件,然後對每個繫結物件呼叫:
BindingExpressionBase exp = tb.GetBindingExpression(TextBox.TextProperty);
exp.ValidateWithoutUpdate();
顯然,這種方案是讓人無法忍受的。VaildationRule.ValidateOnTargetUpdated屬性或許會給我們帶來解決問題的曙光。修改NotNullValidationRule類如下:
public NotNullValidationRule()
{
this.ValidatesOnTargetUpdated = true;
}
我們給NotNullValidationRule 添加了一個建構函式,其中把ValidatesOnTargetUpdated設定為true;意思是,當Target被更新的時候執行驗證邏輯。當我們設定DataContext的時候,Target將會被更新。所以當我們做了這個修改後,重新執行demo,你會發現,視窗一出現,所有的驗證錯誤就被立刻顯示了出來。這顯然不是我們想要的,我們希望的邏輯是,當用戶第一次進入編輯介面,即使資料物件是無效的,也不要顯示驗證錯誤,只有當用戶點選了儲存按鈕或者焦點離開了某個綁定了資料物件的控制元件的時候才顯示驗證錯誤。由此我們得到的結論是:這個屬性也許在某個時候會有用處,但是現在對我們來說卻是無用的。
另一個解決此問題的方案是使用BindingGroup類,改型別是在.NetFramework 3.5 SP1引入的。BindingGroup能夠將一組繫結集合起來,整體更新。如下所示:
<Grid Width="400" Height="200">
<Grid.BindingGroup>
<BindingGroup x:Name="bg"/>
</Grid.BindingGroup>…
如果設定了Grid的BindingGroup屬性,那麼Grid裡面控制元件的所有繫結都會屬於同一個BindingGroup。實際上屬於不屬於同一個BindingGroup,主要是看Binding物件的BindingGroupName屬性是否和BindingGroup的Name相同,如果都沒有設定,自然就是相同的了。
添加了BindingGroup之後,我們需要在儲存按鈕的事件處理方法中,驗證所有屬於BindingGroup的繫結,如下:
private void Button_Click(object sender, RoutedEventArgs e)
{
bool isValid = bg.ValidateWithoutUpdate();
if (isValid)
{
//Saveyour employee
}
}
試著執行demo,你會發現,點選了儲存按鈕後,介面沒有任何響應,而且檢視isValid的值,居然為true。但是,假如嘗試著在Name和Age的文字框裡鍵入一些文字,再刪除這些鍵入的文字,然後點選儲存按鈕,驗證就會起作用。根本的原因是:BindingGroup總是認為初始值是有效的值,不需要再驗證。這在編輯一個使用者的時候是有用的,但是因為我們是在新增一個物件,我們的初始值都是無效的,所以BindingGroup這一特性使得我們無法完成驗證任務。BindingGroup還有一個UpdateSources方法,可以將所有屬於BindingGroup的BindingExpressionBase執行UpdateSource方法;但是這個方法有著同樣的缺陷,只對那些改變過的屬性進行更新。
而且BindingGroup還有另外一個特性也非常令人生厭,當你應用了BindingGroup後,它會改變屬於BindingGroup的Binding的UpdateSourceTrigger屬性的預設值為Explicit,這就意味著,當焦點離開的時候,驗證將不會觸發;除非顯式呼叫每個BindingExpressionBase的UpdateSource方法,而這也是我們要使用的方法。但是顯示指定每個Binding的UpdateSourceTrigger會覆蓋這個行為,BindingGroup會尊重你的設定。所以這基本上完成了我們的任務,改動的程式碼如下:
<Grid Width="400" Height="200" x:Name="grid">
<Grid.BindingGroup>
<BindingGroup x:Name="bg"/>…
<Binding Path="Name"UpdateSourceTrigger="LostFocus">…
<Binding Path="Age"UpdateSourceTrigger="LostFocus">…
public MainWindow()
{
InitializeComponent();
Employee p = new Employee();
DataContext = p;
}
儲存按鈕事件處理方法:
private void Button_Click(object sender, RoutedEventArgs e)
{
foreach (var item in bg.BindingExpressions)
{
item.UpdateSource();
}
}
可以看到,問題的解決方案還是很簡單的。問題基本上解決了,但是對於仔細檢視demo就會發現,當介面顯示以後,將焦點轉移到Name文字框上,然後再離開,這時候沒有發生驗證。這是可以理解的,因為文字框裡的資料沒有發生變化,既然之前沒有顯示驗證錯誤,那麼現在也不應該顯示。如果一定要實現這個需求,就應該做出如下變化:
public MainWindow()
{
InitializeComponent();
Employee p = new Employee();
DataContext = p;
Loaded += MainWindow_Loaded;
LostFocus += MainWindow_LostFocus;
}
void MainWindow_LostFocus(object sender, RoutedEventArgs e)
{
foreach (var item in bg.BindingExpressions)
{
if (item.Target == e.Source)
{
item.UpdateSource();
break;
}
}
}
因為LostFocus是一個路由事件,所以在主視窗中的LostFocus事件處理方法能夠處理所有包含控制元件的LostFocus事件。這方法裡面,我們獲取了當前失去焦點的控制元件的繫結物件,對其進行手動更新。注意到,我們顯式呼叫的UpdateSource,所以在Xaml中就不需要再設定UpdateSourceTrigger="LostFocus"了。
如果把上面的程式碼封裝到一個關聯屬性裡,用起來會更方便,如下:
public static class FEExtension
{
public static bool GetValidateOnLostFocus(DependencyObject obj)
{
return (bool)obj.GetValue(ValidateOnLostFocusProperty);
}
public static void SetValidateOnLostFocus(DependencyObject obj, bool value)
{
obj.SetValue(ValidateOnLostFocusProperty, value);
}
public static readonly DependencyProperty ValidateOnLostFocusProperty =
DependencyProperty.RegisterAttached("ValidateOnLostFocus", typeof(bool), typeof(FEExtension),
new FrameworkPropertyMetadata(false, OnValidateOnLostFocusChanged));
private static void OnValidateOnLostFocusChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var fe = sender as FrameworkElement;
if (e.NewValue.Equals(true))
{
fe.LostFocus += fe_LostFocus;
}
else
{
fe.LostFocus -= fe_LostFocus;
}
}
static void fe_LostFocus(object sender, RoutedEventArgs e)
{
var fe = sender as FrameworkElement;
foreach (var item in fe.BindingGroup.BindingExpressions)
{
if (item.Target == e.Source)
{
item.UpdateSource();
break;
}
}
}
}
使用方法如下:
<Grid Width="400" Height="200" x:Name="grid" local:FEExtension.ValidateOnLostFocus="true">
深入理解ValidationRule
假設我們有一個畢業時間屬性,如下:
public class Employee
{
public string Name { get; set; }
public int? Age { get; set; }
public DateTime GraduationDate { get; set; }
}
該屬性不能大於當前時間,所以需要建立一個驗證類如下:
class PassedDateTimeValidationRule : ValidationRule
{
public PassedDateTimeValidationRule()
{
ValidationStep= ValidationStep.ConvertedProposedValue;
}
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
DateTime dt = (DateTime)value;
if (dt < DateTime.Now)
{
return ValidationResult.ValidResult;
}
return new ValidationResult(false, "Cannot select afuture date");
}
}
驗證邏輯非常簡單,需要注意的是建構函式中把ValidationStep設定為ValidationStep.ConvertedProposedValue,這表示我們得到的引數value的值已經被Converter轉換過了,如果沒有Converter,WPF繫結引擎至少會幫你做一個型別轉換。所以在上例中,我們的value引數已經不是字串了,而是一個DateTime物件。
ValidationStep是一個列舉,每個值的解釋如下:
public enum ValidationStep
{
// 使用原始值,對於TextBox來說,就是Text屬性表示的字串
RawProposedValue = 0,
//
// 使用轉換後的值,將原始值經過型別轉換或者經過Binding的Converter轉換過的值,這個值
// 還沒有被更新到我們的資料物件裡面。
ConvertedProposedValue = 1,
//
// 使用更新過的值,也就是說,我們的資料物件的屬性值已經被更新了,
// 然後用這個更新的值再做資料驗證
UpdatedValue = 2,
//
// 使用提交後的值,驗證將會發生在呼叫了BindingGroup.CommitEdit之後。
//大部分情況下我們都不會再資料提交了再做驗證,所以使用該值的情況應該非常少見。
CommittedValue = 3,
}
對於ValidationStep.UpdatedValue,value引數會有所不同,value引數實際上是包含當前ValidationRule物件的BindingExpressionBase物件。說包含有些不恰當,因為是Binding物件包含了ValidationRule。但是BindingExpressionBase和Binding之間是通過public屬性相互引用的。如果當前的ValidationRule是屬於BindingGroup的,那麼value引數就是BindingGroup物件,你可以對其進行轉換,這樣就可以方法BindingGroup公開的任何方法和屬性了。
深入理解BindingGroup
首先來看看BindingGroup提供了那些功能:
public class BindingGroup : DependencyObject
{
//得到所有屬於當前BindingGroup的BindingExpressionBase物件
public Collection<BindingExpressionBase> BindingExpressions { get; }
//獲取當前BindingGroup的所有資料上下文物件,在本文中只有一個,就是Employee物件
public IList Items { get; }
//該集合將在UpdateSources,ValidateWithoutUpdate,CommitEdit被呼叫的時候進行驗證呼叫
public Collection<ValidationRule> ValidationRules { get; }
//下面這三個方法對應IEditableObject的方法,呼叫下面的方法後,如果你的物件實現了IEditableObject介面,那麼你的方法將會被呼叫
public void BeginEdit();
public void CancelEdit();
public bool CommitEdit();
//下面兩個方法前面已經介紹過了
public bool UpdateSources();
public boolValidateWithoutUpdate();
}
BindingGroup提供了很多功能,但是確有一個缺陷。這就是我前面提到的,如果文字框裡的值沒有變化,即使是無效的,它也不會再次驗證,也就是說,他會假定初始資料都是有效的,在新增一個新的實體的時候,就無法完成驗證資料的功能。
前面說到,我們可以單獨呼叫每個binding物件的Update方法,但是,當你這麼做了之後,BindingGroup本身的ValidationRules就得不到執行了。
一個比較Hack的方法是,將每個繫結的NeedsVlidation屬性設定為true,如下:
foreach (var item in bg.BindingExpressions)
{
typeof(BindingExpressionBase)
.GetProperty("NeedsValidation",System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
.SetValue(item, true);
}
bg.ValidateWithoutUpdate();
如此,ValidateWithoutUpdate方法就顯的正常了。因為用反射更改了BindingExpressionBase的內部屬性,所以說這個方法有點Hack。
參考文章:
http://msdn.microsoft.com/en-us/magazine/ff714593.aspx
http://blogs.msdn.com/b/vinsibal/archive/2008/08/22/bindinggroups-and-ieditablecollectionview.aspx