1. Banding基礎
WPF中的數據綁定提供了很強大的功能。與普通的WinForm程序相比,其綁定功能為我們提供了很多便利,例如Binding對象的自動通知/刷新,Converter,Validation Rules,Two Way Binding等功能,省去了很多維護的繁瑣工作。另外對於WPF中提供的數據模板功能,讓我們可以輕松定制可以被復用的控制呈現的模塊—但這是以數據綁定為前提來做到輕松易用的效果的。數據提供者例如XmlDataProvider和ObjectDataProvider更是簡化了將對象以特定方式綁定並呈現的過程。可以說,數據綁定是WPF中讓我們真正能夠開始體現其便利性的特征之一,而對以數據驅動的應用來講,其重要性不言而喻。
數據綁定的關鍵是System.Windows.Data.Binding對象,它會把兩個對象(UI對象與UI對象之間,UI對象與.NET數據對象之間)按照指定的方式粘合在一起,並在他們之間建立一條通信通道,綁定一旦建立,接下來的應用生命周期中它可以自己獨立完成所有的同步工作。根據其應用場合的不同我們將在本文中從以下幾個部分分別討論:
· 對象間的綁定
· 綁定到集合
· 數據模板
· 向綁定添加規則和轉換器
1. UI對象間的綁定
UI對象間的綁定,也是最基本的形式,通常是將源對象Source的某個屬性值綁定 (拷貝) 到目標對象Destination的某個屬性上。源屬性可以是任意類型,但目標屬性必須是依賴屬性(Dependency Property)。通常情況下我們對於UI對象間的綁定源屬性和目標屬性都是依賴屬性 (有些屬性不是) ,因為依賴屬性有垂直的內嵌變更通知機制,WPF可以保持目標屬性和源屬性的同步。
看個簡單的例子是如何在XAML中實現數據綁定的:
<Window x:Class="Allan.WpfBinding.Demo.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Basic Bindings" Height="400" Width="700" Style="{StaticResource windowStyle}"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="40" /> <RowDefinition Height="*" /> <RowDefinition Height="40" /> </Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="5" HorizontalAlignment="Right"> <Button x:Name="btnBasicBinding" Content="Basic" Style="{StaticResource buttonStyle}"></Button> <Button x:Name="btnCollectionBinding" Content="Collection" Style="{StaticResource buttonStyle}"></Button> <Button x:Name="btnDataTemplate" Content="Data Template" Style="{StaticResource buttonStyle}"></Button> <Button x:Name="btnAdvanceBindings" Content="Advance" Style="{StaticResource buttonStyle}"></Button> <Button x:Name="btnExit" Content="Exit" Style="{StaticResource buttonStyle}"></Button> </StackPanel>
<StackPanel Grid.Row="1" HorizontalAlignment="Left"> <TextBox x:Name="txtName" Margin="5" Width="400" BorderThickness="0" Height="50" Text="Source Element"></TextBox> <TextBlock x:Name="tbShowMessage" Margin="5" Width="400" Height="50" Text="{BindingElementName=txtName,Path=Text }" /> </StackPanel> </Grid> </Window> |
· XAML綁定語法:
上邊的代碼我們將名為txtName的對象的Text屬性作為源對象分別綁定給了兩個TextBlock的Text屬性。這里我們用了Binding關鍵字並指定了ElementName和Path,這兩個就是指定源對象(Source)和源屬性(Source Property). 通常我們在設定綁定時都用與StaticResource標記類似的語法{Binding… }並設置ElementName和Path屬性:
Text=”{Binding ElementName=SourceObjectName, Path=SourceProperty}” |
· 用Coding(C#)添加Binding
而對於C#里和綁定相關的代碼,則看起來會羅嗦很多。但它們都同樣的使用了Binding對象,然后指定PropertyPath的一個實例為源屬性,然后可以有兩個方法來加載綁定規則:
1. 調用FrameworkElement 或FrameworkContentElement對象的SetBinding方法
2. 調用BindingOperations.SetBinding靜態方法
以下代碼實現了和上邊XAML文件類似的功能:
Binding binding = new Binding(); //設置源對象 binding.Source = txtName; //設置源屬性 binding.Path = new PropertyPath("Text"); //添加到目標屬性 this.tbShowMessage.SetBinding(TextBlock.TextProperty, binding); //or //BindingOperations.SetBinding(tbShowMessage, TextBlock.TextProperty, binding); |
· 用Coding(C#)移除Binding
當你在應用程序中某個地方添加了綁定,而在某個時候又不想這個綁定在接下來繼續有效時,你可以有兩種方式來斷開這個綁定:
1. 用BindingOperations.ClearBinding靜態方法。
例如BindingOperations.ClearBinding(currentTextBlock, TextBlock.TextProperty); BindingOperations同時還提供了ClearAllBindings方法,只需要傳入要清除綁定的目標對象的名稱,它就會將所有這個對象的綁定移除。
2. 簡單的將目標屬性設置為一個新的值。
這個簡單的方法同樣有效,可以斷開與前邊設置的binding的連接。簡單的設置為任何值即可:如:currentTextBlock.Text = “it’s a new value.”;
· Binding對象的屬性
Property |
Description |
Converter |
轉換器 |
ElementName |
綁定的源對象 |
FallbackValue |
綁定無法返回有效值時的默認顯示。 |
Mode |
綁定方式 |
Path |
屬性 |
RelativeSource |
常用於自身綁定或者數據模板中來指定綁定的源對象。 |
Source |
源對象 |
StringFormat |
格式化表達式 |
UpdateSourceTrigger |
Sets the events on which binding will occur. |
ValidationRules |
驗證規則 |
總結:對於對象間的綁定,綁定源為ElementName,Path為綁定源屬性。ElementName必須為以下可選項之一:
DataContext |
DataContext是WPF最后才試圖查找的源。一旦RelativeSource和Source對象都沒有被設置,則會在邏輯樹種向上搜尋。 |
RelativeSource |
用來標識和當前控件關聯的對象,通常用於自我引用或數據模板。 |
Source |
數據提供者/對象 |
2. 綁定到集合
·利用ItemsSource來綁定數據源
常用標記:{Binding Path =””} ItemSource DisplayMemberPath
通常來說這是我們在做以數據驅動為主的應用時最經常用到的綁定方式。WPF支持任何類型的.NET對象作為數據源綁定到WPF對象。對於所有的ItemsControl對象都有一個ItemsSource依賴屬性,這是專門為數據綁定而准備的。ItemsSource的類型是IEnumerable,所以對於我們幾乎所有的集合類型我們都可以輕易的改變成ItemsSource的源對象。通過以下語句我們可以將一個名為photos的集合賦予ListBox對象,並以顯示Name屬性的值:
<ListBox x:Name=”pictureBox” DisplayMemberPath=”Name” ItemsSource=”(Binding {DynamicResource photos}” |
我們知道,依賴屬性內建的垂直通知功能讓UI對象間的綁定可以自己負責同步處理,但是對於.NET集合/對象來講,它不具備這樣的能力。為了讓目標屬性與源集合的更改保持同步,源集合必須實現一個叫INotifyCollectionChanged的接口,但通常我們只需要將集合類繼承於ObservableCollection類即可。因為ObservableCollection實現了INotifyPropertyChanged和INotifyCollectionChanged接口。示例代碼中我們這么去定義Photos集合類:
public class Photos : ObservableCollection<Photo> |
· 利用DataContext來作為共享數據源
常用標記:{Binding Path=””} DataContext
顧名思義,DataContext就是數據上下文對象,它是為了避免多個對象共享一個數據源時重復的對所有對象顯式地用binding標記每個Source/RelativeSource/ElementName,而把同一個數據源在上下文對象的某個范圍內共享,這樣當一個綁定沒有顯式的源對象時,WPF會便利邏輯數找到一個非空的DataContext為止。
例如我們可以通過以下代碼給ListBox和Title設置綁定:
<StackPanel Orentation=”Vertical” Margin=”5” DataContext=”{DynamicResource photos}”> <Label x:Name=”TitleLabel” Content=”{Binding Path=Count}” DockPanel.Dock=”Bottom” /> <ListBox x:Name=”pictureBox” DisplayMemeberPath=”Name” ItemSource=”{Binding}” /> </StackPanel> |
對於這些簡單的綁定我們可以很靈活的組合他們的應用來達到我們的要求,這也是我們通常使用的方法。例如:
<Window.Resources> <local:Employee x:Key="MyEmployee" EmployeeNumber="123" FirstName="John" LastName="Doe" Department="Product Development" Title="QA Manager" /> </Window.Resources> <Grid DataContext="{StaticResource MyEmployee}"> <TextBox Text="{Binding Path=EmployeeNumber}"></TextBox> <TextBox Text="{Binding Path=FirstName}"></TextBox> <TextBox Text="{Binding Path=LastName}" /> <TextBox Text="{Binding Path=Title}"></TextBox> <TextBox Text="{Binding Path=Department}" /> </Grid> |
總結:對於集合的綁定,通常會需要用到以下幾個標記:
DisplayMemberPath |
指定源對象中被顯示的屬性。ToString()方法會被默認調用。 |
ItemsSource |
指定要顯示的數據源 |
ItemsTemplate |
指定以什么樣的格式來顯示數據(類似於符合控件,可以在數據模板中利用多種控件來控制展現方式) |
Path |
數據源對象中的屬性—控制顯示 |
DataContext |
共享數據源 |
3. 數據模板 – Data Template
當源屬性和目標屬性為兼容的數據類型,且源所顯示的東西正是你需要顯示的東西時,數據綁定確實很簡單,你只需要向Section 1中講的來匹配對象關系即可。而通常情況下我們對數據綁定都要做一些定制,特別對於.NET對象的綁定,你需要將數據源按照不同的方式分割顯示。Data Template就負責來完成這樣的功能:按照預想的數據展現模式將數據源的不同部分顯示,而其作為可以被復用的獨立結構,一旦定義可以被添加到一個對象內部,將會創建一個全新的可視樹。
數據模板通常會被應用到以下幾類控件來填充其類型為DataTemplate的屬性:
· 內容控件(Content Control):ContentTemplate屬性,控制Content的顯示
· 項控件(Items Control) : ItemTemplate屬性,應用於每個顯示的項
· 頭控件(Header Content Control) : HeaderTemplate屬性,控制Header的展現。
每個數據模板的定義都是類似的方式,你可以像設計普通的窗體一樣來設計其展現的方式,而且他們共享數據模板父空間所賦予的綁定源。例如下邊的代碼我們用一個圖片來替代ListBox中的每一項:
<ListBox x:Name="pictureBox" ItemsSource="{Binding}"ScrollViewer.HorizontalScrollBarVisibility="Disabled"> <ListBox.ItemTemplate> <DataTemplate> <Image Source="{Binding Path=FullPath}" Margin="3,8" Height="35"> <Image.LayoutTransform> <StaticResource ResourceKey="st"/> </Image.LayoutTransform> <Image.ToolTip> <StackPanel> <TextBlock Text="{Binding Path=Name}"/> <TextBlock Text="{Binding Path=DateTime}"/> </StackPanel> </Image.ToolTip> </Image> </DataTemplate> </ListBox.ItemTemplate> </ListBox> |
最終的ListBox中每一項的展現將按照我們在數據模板中設定的樣式以圖片來顯示:
通常數據模板是不需要被內聯聲明的,它可以被定義成一個資源存放在Application.Resources這樣的全局資源辭典中,或者單獨的Resource Dictionary中在多個元素間共享。
4. 向綁定添加規則和轉換器
· 使用值轉換器Value Converter
無論你的綁定XAML寫得多么漂亮,所有的綁定值毫無疑問你都可以得到,但是它不總是可以滿足你不經過任何程序變化顯示出來就能滿足要求的。例如對於本文示例代碼的照片總數的顯示,我們還想顯示得更為智能一些:對於一些符合某種要求的數據我們將其背景顯示為黃色,而對於有多於一條記錄時我們顯示15 Items,僅有一條時顯示1 Item。這時Value Converter就派上用場了。
要定義一個Value Converter需要聲明一個類讓其繼承於System.Windows.Data.IValueConverter接口,並實現其中的兩個方法Convert和ConvertBack方法。
public class RawCountToDescriptionConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { // Let Parse throw an exception if the input is bad int num = int.Parse(value.ToString()); return num + (num == 1 ? " item" : " items"); }
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotSupportedException(); } } |
在XAML中聲明資源,然后將其通過靜態資源引用的方式賦予Binding對象的Converter屬性。
<Window.Resources> <local:CountToBackgroundConverter x:Key="myConverter"/> <local:RawCountToDescriptionConverter x:Key="myConverter2"/> </Window.Resources> <TextBlock x:Name="filePath" DockPanel.Dock="Top" Style="{StaticResource titleStyle}" Text="{Binding Count, Converter={StaticResource myConverter2}}"></TextBlock> |
同樣,我們可以對輸入進行轉換。如果數據的輸入是被驗證規則(如果有的話)標記為有效的,那么值轉換器將會被調用,來對輸入進行轉換后反應出來。 (參考附件代碼中的BindingConverter窗體)
· 向綁定添加規則
每個Binding對象都有一個ValidationRules屬性,可以被設置為一個或多個派生自ValidationRule的對象,每個規則都會檢查特定的條件並更具結果來標記數據的有效性。就像我們在ASP.NET中應用RequiredValidator, CustomValidator一樣,你只需要定義自己的規則,WPF會在每次調用數據時(通常是TextBox等輸入控件失去焦點)會調用驗證檢查。這些是在值轉換器之前發生的,如果數據無效,它會標記此次更新無效,並將數據標記為無效—這是通過設置目標元素的Validation.HasError屬性為true並觸發Validation.Error事件(ValidationResult會被返回,並且其IsValid屬性為false)。我們可以通過一個觸發器來設定當數據無效時對用戶的提示。例如下邊的代碼我們就通過定義一個JpgValidationRule,當數據無效時通過tooltip來提示用戶輸入無效。
public class JpgValidationRule : ValidationRule { public override ValidationResult Validate(object value, CultureInfo cultureInfo) { string filename = value.ToString();
// Reject nonexistent files: if (!File.Exists(filename)) { return new ValidationResult(false, "Value is not a valid file."); }
// Reject files that don’t end in .jpg: if (!filename.EndsWith(".jpg", StringComparison.InvariantCultureIgnoreCase)) { return new ValidationResult(false, "Value is not a .jpg file."); } else { return new ValidationResult(true, null); } } } |
上邊的代碼定義了我們驗證的規則。接下來在XAML中來應用這個規則。我們將這個規則用來檢測輸入框中的數據是否合法:
<TextBox Style="{StaticResource validateTextBoxStyle}"> <TextBox.Text> <Binding UpdateSourceTrigger="PropertyChanged" Path="Department"> <Binding.ValidationRules> <local:JpgValidationRule/> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox> |
當數據不合法時我們以什么樣的方式來告訴用戶呢?這里有兩個方法可以做,一個是定義你自己的ErrorTemplate,另外一個是根據Trigger來設置一些可見信息。通常我們都可以來自己定義一些Error Provider和可以復用的ErrorTemplate,這個話題我們會在下一篇文章中講。這里我們只讓背景做改變並用tooltip來提示用戶—顯示的是ValidationRule返回的出錯信息。因為都是控制顯示的,所以定義成共用的Style:
<Style x:Key="validateTextBoxStyle" TargetType="{x:Type TextBox}"> <Setter Property="Width" Value="300" /> <Style.Triggers> <Trigger Property="Validation.HasError" Value="True"> <Setter Property="Background" Value="Red"/> <Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"/> </Trigger> </Style.Triggers> </Style> |
總的來說,對於驗證,我們常用一下幾個屬性來定義錯誤驗證規則和錯誤展現方式:
· Errors – 錯誤信息集合
· HasError – 是否有錯誤出現.
· ErrorTemplate – 錯誤提示的展現方式.
· Binding.ValidationRules 綁定驗證規則
Coming Next:
本文我們了解了有關Binding以及和綁定有關的附加驗證規則,轉換器等。附加驗證規則我們將在下一篇中了解更多自定義Error Provider,Error Template等。附加的Demo里提供了所有本文中的實例。在下一篇中我們會了解以下幾個問題:
· Validation Rules
· Triggers
2. 驗證
在上一篇文章中我們討論了有關WPF綁定的知識點,現在我們可以很容易的將業務數據作為源綁定到WPF控件並可以通過創建不同的Data Template后來以特定的樣式展現。而作為這個最常用的功能我們可以通過Two Way的綁定模式與界面交互,而在這個時候我們就需要類似於ASP.NET中Validator一樣的東西來限制或者說校驗數據有效性。ValidationRule就是為這樣的應用而生的。本文將詳細討論如何應用驗證規則和自定義錯誤模板。
我們首先來看看WPF中有關數據驗證規則相關的幾個常用類:
· ValidationRule : 所有自定義驗證規則的基類。提供了讓用戶定義驗證規則的入口。
· ExceptionValidation :表示一個規則,該規則檢查在綁定源屬性更新過程中引發的異常。它是一個內置的規則,它檢查在綁定源屬性更新過程中引發的異常。
· ValidationResult : 數據驗證結果的表現方式。ValidationRule對象的Validate方法執行完畢后通過ValidationResult來表示驗證的結果。這里包含了錯誤信息—ErrorContent,數據是否有效—IsValid。ValidResult 為 ValidationResult 的有效實例。
· ValidationError :表示一個驗證錯誤,該錯誤在 ValidationRule 報告驗證錯誤時由綁定引擎創建。
對於WPF中綁定的驗證和轉換值我們需要注意:
1. 在將值從目標屬性傳輸到源屬性時,數據綁定引擎首先移除可能已添加到所綁定元素的 Validation.Errors 附加屬性的任何 ValidationError。然后,數據綁定引擎檢查是否為該 Binding 定義了自定義驗證規則;如果定義了自定義驗證規則,那么它將調用每個 ValidationRule 上的 Validate方法,直到其中一個規則失敗或者全部規則都通過為止。如果某個自定義規則未通過,則綁定引擎會創建一個 ValidationError 對象,並將該對象添加到綁定元素的 Validation.Errors 集合。如果 Validation.Errors 不為空,則元素的 Validation.HasError 附加屬性被設置為 true。此外,如果Binding 的 NotifyOnValidationError 屬性設置為 true,則綁定引擎將引發該元素上的 Validation.Error 附加事件。
2. 如果所有規則都通過,則綁定引擎會調用轉換器(如果存在的話)。
3. 如果轉換器通過,則綁定引擎會調用源屬性的 setter。
4. 如果綁定具有與其關聯的 ExceptionValidationRule,並且在步驟 3 或 4 中引發異常,則綁定引擎將檢查是否存在 UpdateSourceExceptionFilter。使用UpdateSourceExceptionFilter 回調可以提供用於處理異常的自定義處理程序。如果未對 Binding 指定 UpdateSourceExceptionFilter,則綁定引擎將對異常創建ValidationError 並將其添加到綁定元素的 Validation.Errors 集合中。
任何方向(目標到源或源到目標)的有效值傳輸操作都將清除 Validation.Errors 附加屬性。
· 簡單驗證:使用ExceptionValidationRule
對於大多數驗證來說我們都是在驗證用戶輸入。ExceptionValidateRule作為WPF內置的簡單驗證器可以捕捉在綁定上發生的任何異常。我們可以用ExceptionValidateRule來作為一個籠統的錯誤收集器,來暴露出內部數據驗證規則的異常信息。
模擬一個Employee信息更改的窗體。假設我們定義了一個Employee實體類,在實體類中我們對數據的有效性都做了簡單的驗證。
public string Title { get { return strTitle; } set { strTitle = value; if (String.IsNullOrEmpty(strTitle)) { throw new ApplicationException("Please input Title."); } } }
public DateTime Birthday { get { return objBirthday; } set { objBirthday =value; if (objBirthday.Year >= DateTime.Now.Year) { throw new ApplicationException("Please enter a valid date."); } } } |
在XAML中添加對字段的綁定,並對各個單獨的控件的Text屬性設置Bindg.ValidationRules. 這樣當這個綁定的實體對象對應的屬性出現驗證錯誤時,錯誤信息會被ExceptionValidationRule拋出來。其默認行為是在當前控件LostFocus時觸發驗證,並將出錯源控件加亮。
<TextBox Grid.Column="1" Grid.Row="2" Width="200" HorizontalAlignment="Left"> <TextBox.Text> <Binding Path="Title"> <Binding.ValidationRules> <ExceptionValidationRule /> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox> |
上邊的驗證是正確了,但是我們並不知道具體的錯誤信息。這對於一個String類型的字段來說可能是可以理解的,我可以試出來。但對於特殊的字段來說,我們根本無法得知到底發生了什么,我怎么填寫數據才能正確。那么如何去控制顯示呢?
· 自定義錯誤驗證規則和定制顯示
我們在上一篇里邊提到過,所有的驗證其實是有一個System.Windows.Controls.Validation類來完成的,對於這個類我們可以看到它有幾個附加屬性和一個附加事件Error.所謂附加事件/屬性就是說可以被加載到另外的控件上來觸發某個對象的事件或操作其屬性,類似於我們有一個接口,通過這個接口你可以操作別的對象。在這里我們着重需要注意下邊幾個屬性:
Gets the collection of all active ValidationError objects on the bound element. |
|
Gets a value that indicates whether any binding on the binding target element has a ValidationError. |
|
Gets or sets the ControlTemplate used to generate validation error feedback on the adorner layer. |
回想一下上一篇的最后部分,我們用到了觸發器,用觸發器來判斷HasError屬性,然后設置其錯誤模板。很容易我們可以做到這一點,這也是我們經常用到的辦法。為了其復用性,我們可以將它定義為一個公用的Style。
<Style TargetType="{x:Type TextBox}">
<Style.Triggers> <Trigger Property="Validation.HasError" Value="true"> <Setter Property="Validation.ErrorTemplate"> <Setter.Value> <ControlTemplate> <DockPanel LastChildFill="True"> <TextBlock DockPanel.Dock="Right" Foreground="Red" FontSize="12pt" Text="{Binding ElementName=MyAdorner, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}"> </TextBlock> <Border BorderBrush="Red" BorderThickness="1"> <AdornedElementPlaceholder Name="MyAdorner" /> </Border> </DockPanel> </ControlTemplate> </Setter.Value> </Setter> </Trigger> </Style.Triggers> </Style> |
樣式屬於一個implicit key的樣式,表明它針對的是所有的TargetType所指明的類型,這里是TextBox. 有個問題是,我們想在這里邊的值改變時就需要觸發驗證而不是LostFocus,怎么改變呢?Binding對象有一個屬性叫UpdateSourceTrigger,這是一個枚舉值來指定什么時候觸發數據源更新,更新的時候才會調用驗證。
Members |
Description |
Default |
The default UpdateSourceTrigger value of the binding target property. The default value for most dependency properties is PropertyChanged, while the Text property has a default value of LostFocus. |
PropertyChanged |
Updates the binding source immediately whenever the binding target property changes. |
LostFocus |
Updates the binding source whenever the binding target element loses focus. |
Explicit |
Updates the binding source only when you call the UpdateSource method. |
<Binding Path="Title" UpdateSourceTrigger="PropertyChanged">就可以幫我們完成任務。
接下來我們可以定義一個對象化一些的錯誤處理類了。定義自己的規則來驗證數據是ValidationRule所處理的主要任務,擴展這些規則就要使我們的類繼承於這個父類:
public class EnumValidationRule : ValidationRule { private string _enumClass; private string _errorMessage;
public string EnumClass { get { return "Allan.WPFBinding.ValidationDemo." + _enumClass; } set { _enumClass = value; } }
public string ErrorMessage { get { return _errorMessage; } set { _errorMessage = value; } }
public override ValidationResult Validate(object value, CultureInfo cultureInfo) { ValidationResult result = new ValidationResult(true, null); string inputString = (value ?? string.Empty).ToString();
Type type = Type.GetType(EnumClass);
if (string.IsNullOrEmpty(inputString) || !ValidateInput(type,inputString)) { result = new ValidationResult(false, this.ErrorMessage); } return result; } } |
更改綁定參數設置:將驗證規則換成EnumValidationRule並設置相關參數。
<Binding Path="Title" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:EnumValidationRule EnumClass="TitleEnum" ErrorMessage="輸入值不存在。請重新輸入。" />
</Binding.ValidationRules>
</Binding>
· 用BindingGroups實現對List的驗證
到目前為止我們所有看到的都是針對每個單個的控件輸入的驗證,那么有沒有辦法對比如某個ListView的每個item進行綁定驗證呢?用上邊的方法是不可以的,否則我們也沒必要針對每個空間都去那么繁瑣的設置各種信息啊。隨同.NET Framework SP1發布了新的功能點,那就是BindingGroups。BindingGroups是對list列表中的綁定之間的關系進行了封裝,使得你在獲得一個上下文對象時可以和BIdingGroups關聯來得到與當前上下文對象相關聯的所有綁定。而這個關系是你需要顯式去設定的—通過BindingGroupName。
FrameworkElement和FrameworkContentElement有一個叫做BindingGroup的依賴屬性,而ItemsControl繼承於FrameworkElement,所以其具有BindingGroup屬性。而對於BindingBase對象添加了BindingGroupName屬性。
public class BindingBase { public string BindingGroupName { get; set; } } |
我們剛才講過,BindingGroup是需要顯式的去聲明,並且對於一個BindingGroup來說,拿到它就相當於拿到了當前對象相關上下文的所有與其有關系的綁定。而它依然本質上又回到了基本的ValidationRule。
在上邊的類圖中我們可以看到幾個很有意思的東西:
· BindingExpressions: 保存了與當前上下文有關的綁定對象。我們給DateContext中綁定了四個字段的值(對應四個控件),BindingExpressions就保存了所有歸屬到當前BindingGroup的所有Binding對象。例如你有四個字段,那就有四個BidingExpression,有5個字段綁定,你這里就有5個BindingExpression,即便他們很可能都是一樣的。
· Items :這個屬性保存了與當前上下文對象有關的綁定。例如,我們在示例程序中只綁定了一個單獨的Employee對象,那么BindingExpressions里就保留了這個上下文對象。換句話講,當我們按照對象的方式來多個驗證的時候(因為Item在ListView, DataGrid等控件里就代表一個單個對象),我們所操作的對象傳遞到驗證規則時是以BindingExpression的target來表示的。這時,你得到了行的源,就可以做你想做的事情了。
· ValidationRules : 關聯到我們給bindingGroup加載的驗證規則。
· BeginEdit, CancelEdit, CommitEdit : 實際上這些是和IEditableCollectionView關聯的一些事件,表明對數據視圖的操作。這里我們可以簡單認為對數據進行更新和提交。
· UpdateSources : 這是一個方法,表明我們要調用ValidationRule來驗證數據。
· ValidateWithoutUpdate : 方法:驗證數據而不更新數據。
現在我們來應用BindingGroup給基於一個Form的綁定來做驗證吧。首先是定義BindingGroup.
<Grid.BindingGroup> <BindingGroup> <BindingGroup.ValidationRules> <local:EmployeeValidationRule ValidationStep="ConvertedProposedValue" /> </BindingGroup.ValidationRules> </BindingGroup> </Grid.BindingGroup> |
注意這里應用了ValidationStep,和前邊的UpdateSourceTrigger很相似,它是用來表示在什么時候來應用驗證規則的。是個枚舉值:
· RawProposedValue = 0, 在沒有變化的原數據上驗證
· ConvertedProposedValue = 1,用戶更改過數據后驗證 default
· UpdatedValue = 2, 在將數據提交給Source時驗證
· CommittedValue = 3 在將數據提交到數據庫后驗證
接下來定義我們的規則。現在的規則是針對每個對象而不是單個屬性的,所以與前邊的有些許不同,但很容易,因為你可以拿到綁定到當前項的對象。
public override ValidationResult Validate(object value, CultureInfo cultureInfo) { BindingGroup bindingGroup = (BindingGroup)value; Employee emp = (Employee)bindingGroup.Items[0];
object startDateObj = bindingGroup.GetValue(emp, "StartDate"); DateTime? startDate = (DateTime)startDateObj;
object endDateObj = bindingGroup.GetValue(emp, "EndDate"); DateTime? endDate = (DateTime)endDateObj;
// check start and end date together if (startDate.Value > endDate.Value) { return new ValidationResult(false, string.Format("StartDate: {0}, cannot be greater than EndDate: {1}",startDate,endDate)); } else { return new ValidationResult(true, null); } } |
Ok了,你可以再去定義一下出錯時的ErrorTempalte,然后去享受這樣的成果了:)
3. 使用觸發器
WPF提供了很重要的一個東西就是綁定Binding, 它幫助我們做了很多事情,這個我們在WPF學習之綁定這篇里邊有講過。對於Binding我們可以設置其綁定對象,關系,並通過某種規則去驗證輸入,或者轉換值等等,這一切的背后是省去了很多我們需要自己去處理的代碼。而對於WPF最主要表現的東西—渲染UI,當然是我們必須去了解和把握的了。美工設計了很多效果,並把其設計成樣式展現(很大程度上我們應該認為Style也是一種資源),而作為程序員的我們不應該只是簡單的拿來這些拼湊的效果,根據程序的邏輯和用戶的操作來動態的展現效果才是我們能發揮它對界面渲染的更好途徑。Trigger就給我們提供了很好的途徑去結合這些元素。
觸發器,從某種意義上來說它也是一種Style,因為它包含有一個Setter集合,並根據一個或多個條件執行Setter中的屬性改變。因為復用的緣故,Styles是放置觸發器的最好位置。但對於每個FrameworkElement來說都有Triggers集合,你也可以放在Triggers集合里。觸發器有三種類型:
· 屬性觸發器Property Trigger:當Dependency Property的值發生改變時觸發。
· 數據觸發器Data Trigger: 當普通.NET屬性的值發生改變時觸發。
· 事件觸發器Event Trigger: 當路由時間被觸發時調用。
1. 屬性觸發器(Property Trigger)
屬性觸發器是WPF中最常用的觸發器類型,因為我們前邊說過依賴屬性具有垂直變更通知的功能,所以在使用屬性觸發器時會很方便,而且因為WPF中每個控件超過2/3的屬性都是依賴屬性,所以它用到的場合更多。屬性觸發器是在當某個依賴屬性的值發生變化時觸發執行一個Setter的集合,當屬性失去這個值時,這些被處罰執行的Setter集合會自動被撤銷。
例如,下邊的例子設置了當鼠標放置於按鈕之上懸停時,按鈕的外表會發生變化。注意,屬性觸發器是用Trigger標識的。
<Style x:Key="buttonMouseOver" TargetType="{x:Type Button}"> <Style.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter Property="RenderTransform"> <Setter.Value> <RotateTransform Angle="10"></RotateTransform> </Setter.Value> </Setter> <Setter Property="RenderTransformOrigin" Value="0.5,0.5"></Setter> <Setter Property="Background" Value="#FF0CC030" /> </Trigger> </Style.Triggers> </Style> |
屬性觸發器還經常被用在做數據驗證時用來顯示驗證錯誤信息。在WPF學習之綁定里的Validation部分我們附有用屬性觸發器來判斷是否有驗證錯誤並顯示相應驗證錯誤信息的示例。
<TextBox Style="{StaticResource validateTextBoxStyle}"> <TextBox.Text> <Binding UpdateSourceTrigger="PropertyChanged" Path="Department"> <Binding.ValidationRules> <local:JpgValidationRule/> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox> …..
<Style x:Key="validateTextBoxStyle" TargetType="{x:Type TextBox}"> <Setter Property="Width" Value="300" /> <Style.Triggers> <Trigger Property="Validation.HasError" Value="True"> <Setter Property="Background" Value="Red"/> <Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"/> </Trigger> </Style.Triggers> </Style> |
2. 數據觸發器Data Trigger
數據觸發器和屬性觸發器除了面對的對象類型不一樣外完全相同。數據觸發器是來檢測非依賴屬性------也就是用戶自定義的.NET屬性-----的值發生變化時來觸發並調用符合條件的一系列Setter集合。
下邊的示例演示了在綁定的ListBox里如果某個User對象符合某種特點(Role=Admin),則以突出方式顯示這個對象。這里就用了DataTrigger,因為我們需要檢測的是User對象的屬性Role,這個對象是自定義的非可視化對象並且其屬性為普通.NET屬性。
<Page.Resources> <clr:Users x:Key="myUsers" /> <DataTemplate DataType="{x:Type clr:User}"> <TextBlock Text="{Binding Path=Name}"/> </DataTemplate> ... </Page.Resources> <StackPanel> <ListBox Width="200" ItemsSource="{Binding Source={StaticResource myUsers}}" /> </StackPanel> |
主要的部分定義在了Style中,其針對的是每個ListBox的項,當其被綁定的數據的屬性Role為Admin時,突出顯示:
<Style TargetType="{x:Type ListBoxItem}"> <Style.Triggers> <DataTrigger Binding="{Binding Path=Role}" Value="Admin"> <Setter Property="Foreground" Value="Red" /> </DataTrigger> </Style.Triggers> </Style> |
3. 事件觸發器Event Trigger
事件觸發器,顧名思義是在某個事件被觸發時來調用這個觸發器的相關操作。因為WPF提供了用XAML來標記對象,事件等,所以其提供了一些在普通.NET開發中看似沒用的屬性例如IsMouseOver, IsPressed等,這是為了XAML來用的,使其可以很方便的通過某個屬性來判斷狀態,也方便了Property Trigger的應用。而作為事件觸發器來說,它所做的事情和Property Trigger類似,不過是它的內部不能是簡單的Setter集合,而必須是TriggerAction的實例。
以下示例演示了如何應用Event Trigger當鼠標點擊按鈕時,讓按鈕的陰影效果發生變化。
<Button Margin="15" Width="200" Name="myButton"> Click Me to Animate Drop Shadow! <Button.BitmapEffect>
<!-- This BitmapEffect is targeted by the animation. --> <DropShadowBitmapEffect x:Name="myDropShadowBitmapEffect" Color="Black" ShadowDepth="0" /> </Button.BitmapEffect> <Button.Triggers> <EventTrigger RoutedEvent="Button.Click"> <BeginStoryboard> <Storyboard>
<!-- Animate the movement of the button. --> <ThicknessAnimation Storyboard.TargetProperty="Margin" Duration="0:0:0.5" From="50,50,50,50" To="0,0,50,50" AutoReverse="True" />
<!-- Animate shadow depth of the effect. --> <DoubleAnimation Storyboard.TargetName="myDropShadowBitmapEffect" Storyboard.TargetProperty="ShadowDepth" From="0" To="30" Duration="0:0:0.5" AutoReverse="True" />
<!-- Animate shadow softness of the effect. As the Button appears to get farther from the shadow, the shadow gets softer. --> <DoubleAnimation Storyboard.TargetName="myDropShadowBitmapEffect" Storyboard.TargetProperty="Softness" From="0" To="1" Duration="0:0:0.5" AutoReverse="True" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Button.Triggers> </Button> |
4. MultiDataTrigger & MultiTrigger
截至目前我們討論的都是針對單個條件的觸發器,也就是說當某一個條件滿足時就會觸發。而現實中我們可能需要滿足很多個條件時才觸發一系列操作,這個時候就需要用到MultiDataTrigger或MultiTrigger。MutliDataTrigger和MultiTrigger都具有一個Conditions集合用來存放一些觸發條件,這里的Condition之間是and的關系,當所有條件都滿足時,Setter集合才會被調用。根據名字就可以看清楚:MultiDataTrigger用來實現多個數據觸發器(只用於普通.NET屬性)滿足條件時調用;MultiTrigger用來實現多個屬性觸發器(用於依賴屬性)滿足條件時調用。
以下示例僅當按鈕的IsEenabled屬性為true,並且可見時(Visibility=Visible)會以醒目的方式顯示,否則當IsEnabled屬性為false時將以灰色顯示。
<Style TargetType="{x:Type Button}" x:Key="highlightStyle"> <Style.Triggers> <Trigger Property="IsEnabled" Value="false"> <Setter Property="Background" Value="#EEEEEE" /> </Trigger>
<MultiTrigger> <MultiTrigger.Conditions> <Condition Property="Visibility " Value="Visible" /> <Condition Property="IsEnabled" Value="true" /> </MultiTrigger.Conditions> <Setter Property="BorderBrush" Value="Red"/> <Setter Property="FontSize" Value="14" /> <Setter Property="FontWeight" Value="Bold" /> <Setter Property="Foreground" Value="Red" /> </MultiTrigger> </Style.Triggers> </Style> … <Button Style="{StaticResource highlightStyle}" Content="Hight Value" x:Name="btnVisible" Click="Button_Click" /> |
給按鈕添加單擊事件用來改變IsEnabled屬性:
private void Button_Click(object sender, RoutedEventArgs e) { this.btnVisible.IsEnabled = !this.btnVisible.IsEnabled; } |
看看效果(左邊為不單擊后不滿足條件時的樣式):
同樣的,你也可以用MultiDataTrigger來對自定義的屬性進行多條件的與關系操作。
5. 在觸發器中執行用戶代碼
DependencyProperty.RegisterAttached方法允許用戶給控件/窗體等定義自己的依賴屬性,其包含的CallBack參數可以允許執行某個特定方法。這允許我們在Trigger中去調用特定的事件處理。其實嚴格的說這和Trigger不太有關系,因為這相當於我們給某個對象添加了自定義屬性並執行某些事件。但trigger可以恰恰利用這個好處來簡介的執行業務邏輯:
public static readonly DependencyProperty SomethingHappenedProperty = DependencyProperty.RegisterAttached("SomethingHappened", typeof(bool), typeof(Window1), new PropertyMetadata(false, new PropertyChangedCallback(SomethingHappened))); public bool GetSomethingHappened(DependencyObject d) { return (bool)d.GetValue(SomethingHappenedProperty); } public void SetSomethingHappened(DependencyObject d, bool value) { d.SetValue(SomethingHappenedProperty, value); } public static void SomethingHappened(DependencyObject d, DependencyPropertyChangedEventArgs e) { //do something here } |