WPF 中的資料繫結概觀
Windows Presentation Foundation (WPF) 中的資料繫結提供簡單且一致的方式,讓應用程式能夠呈現資料並與其互動。 元素可以和各種資料來源的資料繫結,資料的型式可以是 .NET 物件和 XML。 任何 ContentControl (例如 Button) 以及任何 ItemsControl (例如 ListBox 和 ListView) 都有內建功能,可彈性設定單一資料項目或資料項目集合的樣式。 您可以在資料上方產生排序、篩選和群組檢視。
相較於傳統模型,WPF 中的資料繫結功能具有數個優點,包括本身就支援資料繫結的相當多屬性、資料的彈性 UI 表示,以及 UI 個別的清楚商務邏輯。
本文會先討論 WPF 資料繫結的基本概念,再說明 Binding 類別的使用方式和資料繫結的其他功能。
資料繫結是什麼?
資料繫結是指在應用程式 UI 與其所顯示資料之間建立連接的程序。 如果繫結具有正確的設定而且資料提供了適當的通知,當資料變更其值時,繫結至資料的項目就會自動反映變更。 資料繫結也代表在元素資料的外部表示變更時,基礎資料也會自動更新以反映變更。 例如,如果使用者編輯 TextBox
元素中的值,基礎資料值會自動更新以反映該變更。
一個資料繫結的常見用法是將伺服器或本機組態資料,放入表單或其他 UI 控制項中。 在 WPF 中會擴充這個概念,以包含相當多屬性對各種資料來源的繫結。 在 WPF 中,元素的相依性屬性可以繫結到 .NET 物件 (包括 ADO.NET 物件或與 Web 服務和 Web 屬性關聯的物件) 和 XML 資料。
如需資料繫結的範例,請參閱資料繫結示範中的下列應用程式使用者介面,其中會顯示拍賣項目清單。
應用程式會示範下列資料繫結功能:
ListBox 的內容會繫結到 AuctionItem 物件的集合。 AuctionItem 物件具有 Description、StartPrice、StartDate、Category 和 SpecialFeatures 等屬性。
ListBox
中顯示的資料 (AuctionItem 物件) 已套用範本,所以會針對每個項目顯示描述和目前價格。 範本是使用 DataTemplate 建立的。 除此之外,每個項目的外觀取決於所顯示 AuctionItem 的 SpecialFeatures 值。 如果 AuctionItem 的 SpecialFeatures 值是 Color,項目就具有藍色框線。 如果值是 Highlight,項目就具有橘色框線和星號。 資料範本化一節會提供資料範本化的相關資訊。使用者可以使用提供的
CheckBoxes
來分組、篩選或排序資料。 在上圖中,已選取 [依類別分組] 和 [依類別和日期排序]CheckBoxes
。 您可能已經注意到,資料的群組化是依據產品的分類,且分類名稱是以字母順序排列。 雖然在圖中很難辨識,但項目在每個分類內也有以開始日期排序。 排序是使用「集合檢視」來完成。 繫結至集合一節會討論集合檢視。當使用者選取項目時,ContentControl 會顯示所選項目的詳細資料。 此體驗稱為「主從式案例」。 主從式案例一節會提供這類型繫結的相關資訊。
StartDate 屬性的類型是 DateTime,這會傳回包含時間 (到毫秒) 的日期。 在此應用程式中,已使用自訂轉換器,因此顯示較短的日期字串。 資料轉換一節會提供轉換器的相關資訊。
當使用者選取 [Add Product] \(新增產品\) 按鈕時,會出現下列表單。
使用者可以編輯表單中的欄位、使用簡短或詳細預覽窗格來預覽產品清單,然後選取 Submit
以新增產品清單。 任何現有的群組、篩選和排序設定都會套用至新項目。 在這種特定情形下,輸入上圖的項目會在 [Computer (電腦)] 分類內顯示為第二個項目。
此圖中未顯示的是 [開始日期]TextBox 中提供的驗證邏輯。 如果使用者輸入無效的日期 (無效的格式或過去的日期),就會以 ToolTip 通知使用者,且 TextBox 旁邊會有紅色驚嘆號。 資料驗證一節會討論如何建立驗證邏輯。
在進入上述資料繫結的各種不同功能之前,我們會先討論對於了解 WPF 資料繫結至關重要的基本概念。
基本資料繫結概念
不管您的繫結元素是什麼,也不論資料來源的本質,每個繫結一定會遵循下圖所說明的模型。
如上圖所示,資料繫結基本上是繫結目標和繫結來源間的橋樑。 該圖示範下列基本 WPF 資料繫結概念:
一般而言,每個繫結都有四個元件:
- 繫結目標物件。
- 目標屬性。
- 繫結來源:
- 要使用之繫結來源中的值路徑。
例如,如果您想要將
TextBox
的內容繫結至Employee.Name
屬性,您的目標物件是TextBox
,目標屬性是 Text 屬性,要使用的值為 Name,而來源物件是 Employee 物件。目標屬性必須是相依性屬性。 大部分 UIElement 屬性是相依性屬性,而大部分的相依性屬性預設都支援資料繫結,除了唯讀屬性之外。 (只有衍生自 DependencyObject 的類型可以定義相依性屬性;而且所有 UIElement 類型都衍生自
DependencyObject
。)雖然圖中未顯示,但應該注意的是,繫結來源物件不限於自訂 .NET 物件。 WPF 資料繫結支援 .NET 物件和 XML 格式的資料。 舉幾個例子,您的繫結來源可能是 UIElement、任何清單物件、ADO.NET 或 Web 服務物件,或是包含 XML 資料的 XmlNode。 如需詳細資訊,請參閱繫結來源概觀。
請務必記住,當您建立繫結時,會將繫結目標「繫結到」繫結來源。 例如,如果您要使用資料繫結在 ListBox 中顯示一些基礎 XML 資料,則會將 ListBox
繫結到 XML 資料。
若要建立繫結,請使用 Binding 物件。 本文的其餘部分將討論與 Binding
物件相關聯的許多概念,以及該物件的一些屬性和使用方式。
資料流程的方向
如上圖中箭頭所指出,繫結的資料流程是從繫結目標走向繫結來源 (例如在使用者編輯 TextBox
的值時來源值會變更),以及/或者在繫結來源有提供適當通知時,是從繫結來源走向繫結目標 (例如 TextBox
內容會隨著繫結來源變更而更新)。
您可能會想讓使用者透過應用程式變更資料,並將變更散佈回來源物件。 或者您可能不希望使用者更新來源資料。 您可以藉由設定 Binding.Mode 來控制資料流程。
下圖說明不同類型的資料流程:
OneWay 繫結會讓來源屬性的變更自動更新到目標屬性,但目標屬性的變更不會散佈回來源屬性。 如果要繫結的控制項是隱含唯讀的,這種類型的繫結很適當。 例如,您可以繫結到股票行情即時看板這類的來源,或者目標屬性沒有可供進行變更的控制項介面,例如資料表的資料繫結背景色彩。 如果不需要監視目標屬性的變更,使用 OneWay 繫結模式可以避免 TwoWay 繫結模式的額外負荷。
TwoWay 繫結會讓來源屬性或目標屬性的變更,自動更新另外一個。 這種類型的繫結適合可編輯表單或其他完全互動式的 UI 案例。 大部分屬性預設為 OneWay 繫結,但某些相依性屬性 (通常是使用者可編輯控制項的屬性,例如 TextBox.Text 和 CheckBox.IsChecked) 預設為 TwoWay 繫結。 判斷相依性屬性預設會單向或雙向繫結的程式設計方式是,使用 DependencyProperty.GetMetadata 取得屬性中繼資料,然後檢查 FrameworkPropertyMetadata.BindsTwoWayByDefault 屬性的布林值。
OneWayToSource 是 OneWay 繫結的反向,會在目標屬性變更時更新來源屬性。 範例案例之一是當您只需要從 UI 重新評估來源值時。
圖中沒有說明的 OneTime 繫結,會讓來源屬性初始化目標屬性,但不會散佈後續的變更。 如果資料內容或資料內容中的物件有所變更,該變更「不會」反映在目標屬性中。 如果適合使用目前狀態的快照集或資料是真正的靜態,則此類型的繫結很適當。 如果您想要以來源屬性的某些值初始化目標屬性,但無法預先得知資料內容,則此類型的繫結也很有用。 此模式基本上是 OneWay 繫結的簡易形式,萬一來源值不變更,可提供較佳的效能。
若要偵測來源變更 (適用於 OneWay 和 TwoWay 繫結),來源必須實作適當的屬性變更通知機制,例如 INotifyPropertyChanged。 如需 INotifyPropertyChanged 實作的範例,請參閱操作說明:實作屬性變更通知。
Binding.Mode 屬性提供繫結模式的詳細資訊,以及如何指定繫結方向的範例。
觸發來源更新的機制
TwoWay 或 OneWayToSource 繫結會接聽目標屬性中的變更,並將其散佈回來源 (稱為更新來源)。 舉例來說,您可以編輯 TextBox 的文字以變更基礎來源值。
不過,當您編輯文字時,或者是在完成文字編輯且控制項遺失焦點之後,來源值是否已更新? Binding.UpdateSourceTrigger 屬性會決定觸發來源更新的機制。 下圖中向右箭頭的點說明 Binding.UpdateSourceTrigger 屬性的角色。
如果 UpdateSourceTrigger
值是 UpdateSourceTrigger.PropertyChanged,則只要目標屬性變更,TwoWay 或 OneWayToSource 繫結向右箭頭所指向的值就會更新。 不過,如果 UpdateSourceTrigger
值是 LostFocus,則只會在目標屬性失去焦點時,才會以新的值更新該值。
類似於 Mode 屬性,不同的相依性屬性有不同的預設 UpdateSourceTrigger 值。 大多數相依性屬性的預設值為 PropertyChanged,而 TextBox.Text
屬性具有 LostFocus 的預設值。 PropertyChanged
表示每當目標屬性變更時,通常會發生來源更新。 立即變更適用於 CheckBox 和其他簡單的控制項。 然而,對於文字欄位而言,在每個按鍵輸入後更新會降低效能,並剝奪使用者在送交新值前按退格鍵和修正輸入錯誤的一般機會。
如需如何尋找相依性屬性預設值的資訊,請參閱 UpdateSourceTrigger 屬性頁面。
下表使用 TextBox 作為範例,提供每個 UpdateSourceTrigger 值的範例案例。
UpdateSourceTrigger 值 | 來源值更新時機 | TextBox 的範例案例 |
---|---|---|
LostFocus (TextBox.Text 的預設值) |
當 TextBox 控制項失去焦點時。 | 與驗證邏輯相關聯的 TextBox (請參閱下面的資料驗證)。 |
PropertyChanged |
當您在 TextBox 中輸入時。 | 聊天室視窗中的 TextBox 控制項。 |
Explicit |
當應用程式呼叫 UpdateSource 時。 | 可編輯表單中的 TextBox 控制項 (只有在使用者按一下提交按鈕時才會更新來源值)。 |
如需範例,請參閱如何:控制 TextBox 文字更新來源的時機。
建立繫結
再重複聲明一次前幾節所討論的一些概念,您會使用 Binding 物件建立繫結,而且每個繫結通常有四個元件:繫結目標、目標屬性、繫結來源,以及要使用的來源值路徑。 本節討論如何設定繫結。
請考慮下列範例,其中的繫結來源物件是名為 MyData 的類別,定義於 SDKSample 命名空間中。 為了便於示範,MyData 的字串屬性名為 ColorName,其值設定為 "Red"。 因此,本範例會產生具有紅色背景的按鈕。
<DockPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:SDKSample">
<DockPanel.Resources>
<c:MyData x:Key="myDataSource"/>
</DockPanel.Resources>
<DockPanel.DataContext>
<Binding Source="{StaticResource myDataSource}"/>
</DockPanel.DataContext>
<Button Background="{Binding Path=ColorName}"
Width="150" Height="30">
I am bound to be RED!
</Button>
</DockPanel>
如需繫結宣告語法的詳細資訊,以及如何在程式碼中設定繫結的範例,請參閱繫結宣告概觀。
如果將這個範例套用到我們的基本圖表,結果會類似下圖。 此圖描述 OneWay 繫結,因為 Background 屬性預設支援 OneWay 繫結。
您可能想知道,為何即使 ColorName 屬性的類型為字串,而 Background 屬性的類型為 Brush,此繫結還是可以運作。 此繫結使用了資料轉換一節中所討論的預設類型轉換。
指定繫結來源
請注意,在上一個範例中,繫結來源是藉由設定 DockPanel.DataContext 屬性來指定。 Button 接著會從 DockPanel (也就是其父元素) 繼承 DataContext 值。 再重複聲明一次,繫結來源物件是繫結的四個必要元件之一。 因此,沒有指定繫結來源物件,就無法進行繫結。
有數種方式可以指定繫結來源物件。 當您要將多個屬性繫結到相同來源時,在父元素上使用 DataContext 屬性就很有用。 然而,有時候在個別的繫結宣告上指定繫結來源可能比較恰當。 針對前述範例,您可以不要使用 DataContext 屬性,而改為在按鈕的繫結宣告上直接設定 Binding.Source 屬性來指定繫結來源,如下列範例所示。
<DockPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:SDKSample">
<DockPanel.Resources>
<c:MyData x:Key="myDataSource"/>
</DockPanel.Resources>
<Button Background="{Binding Source={StaticResource myDataSource}, Path=ColorName}"
Width="150" Height="30">
I am bound to be RED!
</Button>
</DockPanel>
除了直接在元素上設定 DataContext 屬性、從上階繼承 DataContext 值 (例如第一個範例中的按鈕),以及在繫結上設定 Binding.Source 屬性來明確指定繫結來源 (例如最後一個範例中的按鈕),您也可以使用 Binding.ElementName 屬性或 Binding.RelativeSource 屬性來指定繫結來源。 當您要繫結到應用程式中的其他元素時 (例如使用滑桿調整按鈕寬度時),ElementName 屬性會很有用。 在 ControlTemplate 或 Style 中指定繫結時,RelativeSource 屬性會很有用。 如需詳細資訊,請參閱如何:指定繫結來源。
指定值的路徑
如果繫結來源是物件,請使用 Binding.Path 屬性來指定要用於繫結的值。 如果您要繫結到 XML 資料,請使用 Binding.XPath 屬性來指定值。 在某些情況下,即使資料是 XML,可能也適合使用 Path 屬性。 例如,如果您想要存取所傳回 XmlNode (XPath 查詢結果) 的 Name 屬性,除了 XPath 屬性,還應該使用 Path 屬性。
雖然我們強調要使用值的 Path 是繫結的四個必要元件之一,但在要繫結到整個物件的案例中,要使用的值會與繫結來源物件相同。 在此情況下,不適合指定 Path。 請思考一下下列範例。
<ListBox ItemsSource="{Binding}"
IsSynchronizedWithCurrentItem="true"/>
上述範例使用空白繫結語法:{Binding}。 在此情況下,ListBox 會從父 DockPanel 元素繼承 DataContext (未在此範例中顯示)。 沒有指定路徑時,預設會繫結到整個物件。 換句話說,本範例中的路徑保留空白,是因為要將 ItemsSource 屬性繫結到整個物件。 (如需深入討論,請參閱繫結至集合一節)。
除了繫結到集合之外,當您要繫結到整個物件而非只是物件的單一屬性時,這個案例也很好用。 例如,如果來源物件的類型是 String,您可能只想要繫結到字串本身。 另一個常用案例是當您要將元素繫結到具有許多屬性的物件時。
您可能必須套用自訂邏輯,如此資料對於繫結的目標屬性來說才有意義。 如果沒有預設型別轉換,自訂邏輯的形式可以是自訂轉換器。 如需轉換器的資訊,請參閱資料轉換。
繫結和 BindingExpression
在進入資料繫結的其他功能和使用方式之前,介紹 BindingExpression 類別會很有用。 如您在前幾節所見,Binding 類別是繫結宣告的高階類別,提供許多屬性,可讓您指定繫結的特性。 而相關類別 BindingExpression,則是維持來源和目標間連接的基礎物件。 繫結包含可以跨數種繫結運算式共用的所有資訊。 BindingExpression 是無法共用的執行個體運算式,並包含 Binding 的所有執行個體資訊。
請考慮下列範例,其中 myDataObject
是 MyData
類別的執行個體,myBinding
是來源 Binding 物件,而 MyData
是包含名為 ColorName
之字串屬性的定義類別。 下列範例會將 TextBlock 執行個體 myText
的文字內容繫結到 ColorName
。
// Make a new source
var myDataObject = new MyData();
var myBinding = new Binding("ColorName")
{
Source = myDataObject
};
// Bind the data source to the TextBox control's Text dependency property
myText.SetBinding(TextBlock.TextProperty, myBinding);
' Make a New source
Dim myDataObject As New MyData
Dim myBinding As New Binding("ColorName")
myBinding.Source = myDataObject
' Bind the data source to the TextBox control's Text dependency property
myText.SetBinding(TextBlock.TextProperty, myBinding)
您可以使用相同的 myBinding 物件建立其他繫結。 例如,您可以使用 myBinding 物件,將核取方塊的文字內容繫結到 ColorName。 在該案例中,會有兩個 BindingExpression 執行個體共用 myBinding 物件。
BindingExpression 物件是透過在資料繫結物件上呼叫 GetBindingExpression 來傳回。 下列文章示範 BindingExpression 類別的一些使用方式:
資料轉換
在建立繫結一節中,按鈕是紅色的,因為其 Background 屬性繫結到值為 "Red" 的字串屬性。 此字串值運作正常,因為 Brush 類型有類型轉換器,可將字串值轉換成 Brush。
將這項資訊新增至建立繫結一節中的圖看起來會像這樣。
不過,如果您的繫結來源物件具有 Color 類型的 Color 屬性,而不是字串類型的屬性,該怎麼辦? 在該情況下,為了讓繫結運作正常,您需要先將 Color 屬性值改為 Background 屬性能夠接受的某個值。 您會需要藉由實作 IValueConverter 介面來建立自訂轉換器,如下列範例所示。
[ValueConversion(typeof(Color), typeof(SolidColorBrush))]
public class ColorBrushConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
Color color = (Color)value;
return new SolidColorBrush(color);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return null;
}
}
<ValueConversion(GetType(Color), GetType(SolidColorBrush))>
Public Class ColorBrushConverter
Implements IValueConverter
Public Function Convert(ByVal value As Object, ByVal targetType As Type, ByVal parameter As Object, ByVal culture As System.Globalization.CultureInfo) As Object Implements IValueConverter.Convert
Dim color As Color = CType(value, Color)
Return New SolidColorBrush(color)
End Function
Public Function ConvertBack(ByVal value As Object, ByVal targetType As Type, ByVal parameter As Object, ByVal culture As System.Globalization.CultureInfo) As Object Implements IValueConverter.ConvertBack
Return Nothing
End Function
End Class
如需相關資訊,請參閱 IValueConverter 。
現在使用自訂轉換器來取代預設轉換,我們的圖表看起來會像這樣。
再重複聲明一次,預設轉換也可以使用,因為要繫結的型別中存在型別轉換器。 這個行為會取決於目標中提供的型別轉換器。 如果有疑問,請建立自己的轉換器。
以下是適合實作資料轉換器的一些常見案例:
依據文化特性的不同,您的資料顯示方式會有所不同。 例如,依據特定文化特性所使用的轉換,您可能會想要實作貨幣轉換器或日曆日期/時間轉換器。
要使用的資料不見得是要變更屬性的文字值,但有可能是改為變更某些其他值,例如影像的來源,或者是顯示文字的色彩或樣式。 在這個情況下可能的轉換器使用方式,是藉由將可能不適合的屬性繫結轉換,例如將文字欄位繫結到表格儲存格的 Background 屬性。
一個以上的控制項或者是控制項的多個屬性,會繫結到相同資料。 在這個情況下,主要繫結可能只是顯示文字,而其他繫結會處理特定顯示問題,但仍然使用相同的繫結做為來源資訊。
目標屬性具有繫結的集合,稱為 MultiBinding。 針對 MultiBinding,您可以使用自訂 IMultiValueConverter,從繫結值產生最終值。 舉例來說,顏色可以由紅藍綠的值計算而來,而這些值可以來自相同或不同的繫結來源物件。 如需範例和資訊,請參閱 MultiBinding。
繫結至集合
繫結來源物件可以視為其屬性包含資料的單一物件,或是通常會分組在一起之多型物件的資料集合 (例如資料庫的查詢結果)。 到目前為止,我們只討論繫結到單一物件。 不過,繫結到資料集合是常見案例。 例如,一個常見案例是使用 ListBox、ListView 或 TreeView 等 ItemsControl (如資料繫結是什麼一節顯示的應用程式中所示) 來顯示資料集合。
所幸,我們的基本圖表仍然適用。 如果您要將 ItemsControl 繫結到集合,圖表看起來會像這樣。
如圖所示,若要將 ItemsControl 繫結到集合物件,ItemsControl.ItemsSource 屬性是要使用的屬性。 您可以將 ItemsSource
想成是 ItemsControl 的內容。 繫結是 OneWay,因為 ItemsSource
屬性預設支援 OneWay
繫結。
如何實作集合
您可以列舉實作 IEnumerable 介面的任何集合。 不過,若要設定動態繫結,讓集合中的插入或刪除作業自動更新 UI,則集合必須實作 INotifyCollectionChanged 介面。 這個介面會公開每次基礎集合變更時必須引發的事件。
WPF 提供 ObservableCollection<T> 類別,這是公開 INotifyCollectionChanged 介面之資料集合的內建實作。 為充分支援從來源物件傳輸資料值到目標,集合中支援可繫結屬性的每個物件,也都必須實作 INotifyPropertyChanged 介面。 如需詳細資訊,請參閱繫結來源概觀。
實作您自己的集合之前,請考慮使用 ObservableCollection<T> 或其中一個現有的集合類別,例如 List<T>、Collection<T> 和 BindingList<T> 等。 如果您擁有進階案例,而想實作自己的集合,請考慮使用提供非泛型物件集合的 IList,這些物件可依索引個別存取,因此提供最佳效能。
清單和格線
將 ItemsControl 繫結到資料集合之後,您可能會想要排序、篩選或分組資料。 若要這樣做,請使用集合檢視,這是實作 ICollectionView 介面的類別。
集合檢視是什麼?
集合檢視是以繫結來源集合為基礎的一層,可以讓您依據排序、篩選和群組查詢來巡覽和顯示來源集合,而不需要變更基礎來源集合本身。 集合檢視也會保留集合中目前項目的指標。 如果來源集合實作 INotifyCollectionChanged 介面,則 CollectionChanged 事件所引發的變更會散佈到檢視。
因為檢視不會變更基礎來源集合,每個來源集合可以有多個相關聯的檢視。 舉例來說,您可以有一個 Task 物件集合。 使用檢視時,您可以不同方式顯示相同資料。 舉例來說,在頁面左方您可以顯示依優先順序排序的工作,右方顯示依區域分組的工作。
如何建立檢視
建立和使用檢視的方法之一,是直接具現化檢視物件,然後將它做為繫結來源使用。 例如,以資料繫結是什麼一節中顯示的資料繫結示範應用程式為例。 該應用程式的實作讓 ListBox 繫結到資料集合的檢視,而非直接繫結到資料集合。 下列範例擷取自資料繫結示範應用程式。 CollectionViewSource 類別是繼承自 CollectionView 之類別的 XAML Proxy。 在此特定範例中,檢視的 Source 會繫結到目前應用程式物件的 AuctionItems 集合 (類型為 ObservableCollection<T>)。
<Window.Resources>
<CollectionViewSource
Source="{Binding Source={x:Static Application.Current}, Path=AuctionItems}"
x:Key="listingDataView" />
</Window.Resources>
資源 listingDataView 接著會作為應用程式中元素的繫結來源,例如 ListBox。
<ListBox Name="Master" Grid.Row="2" Grid.ColumnSpan="3" Margin="8"
ItemsSource="{Binding Source={StaticResource listingDataView}}" />
若要建立相同集合的另一個檢視,您可以建立另一個 CollectionViewSource 執行個體,並為其指定不同的 x:Key
名稱。
下表顯示哪些檢視資料類型會建立為預設集合檢視,或是由 CollectionViewSource 根據來源集合類型建立。
來源集合型別 | 集合檢視型別 | 備註 |
---|---|---|
IEnumerable | 以 CollectionView 為基礎的內部類型 | 無法群組項目。 |
IList | ListCollectionView | 最快。 |
IBindingList | BindingListCollectionView |
使用預設檢視
指定集合檢視做為繫結來源是建立和使用集合檢視的方式之一。 WPF 也會為做為繫結來源使用的每個集合建立預設集合檢視。 如果您直接繫結至集合,WPF 會繫結至它的預設檢視。 此預設檢視是由相同集合的所有繫結共用,因此一個繫結控制項或程式碼對預設檢視所做的變更 (例如排序或目前項目指標變更,這會在稍後討論),會反映在相同集合的所有其他繫結中。
若要取得預設檢視,您可以使用 GetDefaultView 方法。 如需範例,請參閱取得資料集合的預設檢視。
包含 ADO.NET DataTables 的集合檢視
為了改善效能,ADO.NET DataTable 或 DataView 物件的集合檢視會將排序和篩選委派給 DataView,這會使得資料來源的所有集合檢視共用排序和篩選。 若要讓每個集合檢視能夠獨立排序和篩選,請使用其本身的 DataView 物件初始化每個集合檢視。
排序
如先前所述,檢視可以將排序順序套用到集合上。 當資料存在於基礎集合中時,資料本身可能有也可能沒有相關的順序。 對集合的檢視可以讓您依據所提供的比較準則,安排順序或變更預設順序。 因為是資料的用戶端檢視,常見的案例是使用者會想要針對資料行對應的值,而排序表格式資料的資料行。 藉由使用檢視,就可以套用這個使用者驅動的排序,同樣不需要對基礎集合進行任何變更,或甚至不需要重新查詢集合內容。 如需範例,請參閱在按一下標頭時排序 GridView 資料行。
下列範例顯示資料繫結是什麼一節中應用程式 UI 之 [Sort by category and date] \(依類別和日期排序\) CheckBox 的排序邏輯。
private void AddSortCheckBox_Checked(object sender, RoutedEventArgs e)
{
// Sort the items first by Category and then by StartDate
listingDataView.SortDescriptions.Add(new SortDescription("Category", ListSortDirection.Ascending));
listingDataView.SortDescriptions.Add(new SortDescription("StartDate", ListSortDirection.Ascending));
}
Private Sub AddSortCheckBox_Checked(sender As Object, e As RoutedEventArgs)
' Sort the items first by Category And then by StartDate
listingDataView.SortDescriptions.Add(New SortDescription("Category", ListSortDirection.Ascending))
listingDataView.SortDescriptions.Add(New SortDescription("StartDate", ListSortDirection.Ascending))
End Sub
篩選
檢視也可以將篩選套用至集合,讓檢視只顯示完整集合的特定子集。 您可以對資料篩選條件。 例如,就像資料繫結是什麼一節中的應用程式做法,[Show only bargains] \(僅顯示廉價品\) CheckBox 包含的邏輯會篩選出價格為美金 25 元 (含) 以上的商品。 執行下列程式碼,以在選取 CheckBox 時,將 ShowOnlyBargainsFilter 設定為 Filter 事件處理常式。
private void AddFilteringCheckBox_Checked(object sender, RoutedEventArgs e)
{
if (((CheckBox)sender).IsChecked == true)
listingDataView.Filter += ListingDataView_Filter;
else
listingDataView.Filter -= ListingDataView_Filter;
}
Private Sub AddFilteringCheckBox_Checked(sender As Object, e As RoutedEventArgs)
Dim checkBox = DirectCast(sender, CheckBox)
If checkBox.IsChecked = True Then
AddHandler listingDataView.Filter, AddressOf ListingDataView_Filter
Else
RemoveHandler listingDataView.Filter, AddressOf ListingDataView_Filter
End If
End Sub
ShowOnlyBargainsFilter 事件處理常式實作如下。
private void ListingDataView_Filter(object sender, FilterEventArgs e)
{
// Start with everything excluded
e.Accepted = false;
// Only inlcude items with a price less than 25
if (e.Item is AuctionItem product && product.CurrentPrice < 25)
e.Accepted = true;
}
Private Sub ListingDataView_Filter(sender As Object, e As FilterEventArgs)
' Start with everything excluded
e.Accepted = False
Dim product As AuctionItem = TryCast(e.Item, AuctionItem)
If product IsNot Nothing Then
' Only include products with prices lower than 25
If product.CurrentPrice < 25 Then e.Accepted = True
End If
End Sub
如果是直接使用其中一個 CollectionView 類別,而非 CollectionViewSource,則應該使用 Filter 屬性指定回呼。 如需範例,請參閱篩選檢視中的資料。
分組
除了檢視 IEnumerable 集合的內部類別之外,所有集合檢視都支援「群組」,其可讓使用者將集合檢視中的集合分割成數個邏輯群組。 群組可以是明確的,由使用者提供群組清單,或者是隱含的,讓群組依據資料動態產生。
下列範例顯示 [Group by category] \(依類別分組\) CheckBox 的邏輯。
// This groups the items in the view by the property "Category"
var groupDescription = new PropertyGroupDescription();
groupDescription.PropertyName = "Category";
listingDataView.GroupDescriptions.Add(groupDescription);
' This groups the items in the view by the property "Category"
Dim groupDescription = New PropertyGroupDescription()
groupDescription.PropertyName = "Category"
listingDataView.GroupDescriptions.Add(groupDescription)
如需另一個群組範例,請參閱實作 GridView 的 ListView 中的群組項目。
目前項目指標
檢視也支援目前項目的概念。 您可以在集合檢視中逐一巡覽物件。 當您巡覽時,移動項目指標可以讓您擷取集合中存在該特定位置的物件。 如需範例,請參閱透過資料 CollectionView 中的物件巡覽。
由於 WPF 只使用檢視 (可能是您指定的檢視或集合的預設檢視) 繫結至集合,因此集合的所有繫結都有目前項目指標。 當繫結至檢視時,Path
值中的斜線 ("/") 字元會指定檢視的目前項目。 在下列範例中,資料內容是集合檢視。 第一行繫結至集合。 第二行繫結至集合中的目前項目。 第三行繫結至集合中目前項目的 Description
屬性。
<Button Content="{Binding }" />
<Button Content="{Binding Path=/}" />
<Button Content="{Binding Path=/Description}" />
斜線和屬性語法也可以堆疊來周遊集合的階層架構。 下列範例繫結至名為 Offices
之集合的目前項目,此集合是來源集合目前項目的屬性。
<Button Content="{Binding /Offices/}" />
目前項目指標可能會受套用至集合的任何排序或篩選所影響。 排序會將目前項目指標保留在最後選取的項目上,但現在集合檢視已在其周圍重組結構。 (或許選取的項目先前是在清單的開頭,但現在選取的項目可能是在中間的某處)。如果選取的項目在篩選之後仍然在檢視範圍內,篩選就會保留該選取項目。 否則,目前項目指標會設定在篩選集合檢視的第一個項目。
主從式繫結案例
目前項目的概念不但對巡覽集合項目很有用,也能應用在主從式繫結案例。 請再回想資料繫結是什麼一節中的應用程式 UI。 在該應用程式中,ListBox 中的選取項目會決定 ContentControl 中顯示的內容。 換句話說,當選取 ListBox 項目時,ContentControl 會顯示所選項目的詳細資料。
您可以實作主從式案例,只要藉由讓兩或多個控制項繫結到相同檢視即可。 下列範例來自資料繫結示範,顯示您在資料繫結是什麼一節中於應用程式使用者介面上所看到的 ListBox 和 ContentControl 標記。
<ListBox Name="Master" Grid.Row="2" Grid.ColumnSpan="3" Margin="8"
ItemsSource="{Binding Source={StaticResource listingDataView}}" />
<ContentControl Name="Detail" Grid.Row="3" Grid.ColumnSpan="3"
Content="{Binding Source={StaticResource listingDataView}}"
ContentTemplate="{StaticResource detailsProductListingTemplate}"
Margin="9,0,0,0"/>
請注意,這兩個控制項都會繫結到相同來源:listingDataView 靜態資源 (請參閱如何建立檢視一節中這個資源的定義)。 此繫結運作正常,因為當一個物件 (在此案例中為 ContentControl) 繫結到集合檢視時,會自動繫結到該檢視的 CurrentItem。 CollectionViewSource 物件會自動同步貨幣和選取項目。 如果您的清單控制項未繫結到此範例中所示的 CollectionViewSource 物件,則必須將其 IsSynchronizedWithCurrentItem 屬性設定為 true
,此控制項才能運作。
如需其他範例,請參閱繫結至集合並根據選取項目顯示資訊和使用含階層式資料的主從式模式。
您可能已經注意到上述範例有使用範本。 事實上,如果沒有使用範本 (ContentControl 所明確使用的範本,以及 ListBox 所隱含使用的範本),資料的顯示不會如我們所預期。 現在,下節中要說明資料範本化。
資料範本化
沒有使用資料範本的話,資料繫結是什麼一節中的應用程式 UI 如下所示。
如上一節中的範例所示,ListBox 控制項和 ContentControl 都會繫結到 AuctionItem 的整個集合物件 (或更具體來說,集合物件的檢視)。 如果沒有如何顯示資料集合的特定指示,ListBox 會顯示基礎集合中每一個物件的字串表示,而 ContentControl 會顯示所繫結之物件的字串表示。
為了解決該問題,應用程式會定義 DataTemplate。 如上一節中的範例所示,ContentControl 會明確使用 detailsProductListingTemplate 資料範本。 在顯示集合中 AuctionItem 物件時,ListBox 控制項會隱含使用下列資料範本。
<DataTemplate DataType="{x:Type src:AuctionItem}">
<Border BorderThickness="1" BorderBrush="Gray"
Padding="7" Name="border" Margin="3" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20"/>
<ColumnDefinition Width="86"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Polygon Grid.Row="0" Grid.Column="0" Grid.RowSpan="4"
Fill="Yellow" Stroke="Black" StrokeThickness="1"
StrokeLineJoin="Round" Width="20" Height="20"
Stretch="Fill"
Points="9,2 11,7 17,7 12,10 14,15 9,12 4,15 6,10 1,7 7,7"
Visibility="Hidden" Name="star"/>
<TextBlock Grid.Row="0" Grid.Column="1" Margin="0,0,8,0"
Name="descriptionTitle"
Style="{StaticResource smallTitleStyle}">Description:</TextBlock>
<TextBlock Name="DescriptionDTDataType" Grid.Row="0" Grid.Column="2"
Text="{Binding Path=Description}"
Style="{StaticResource textStyleTextBlock}"/>
<TextBlock Grid.Row="1" Grid.Column="1" Margin="0,0,8,0"
Name="currentPriceTitle"
Style="{StaticResource smallTitleStyle}">Current Price:</TextBlock>
<StackPanel Grid.Row="1" Grid.Column="2" Orientation="Horizontal">
<TextBlock Text="$" Style="{StaticResource textStyleTextBlock}"/>
<TextBlock Name="CurrentPriceDTDataType"
Text="{Binding Path=CurrentPrice}"
Style="{StaticResource textStyleTextBlock}"/>
</StackPanel>
</Grid>
</Border>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=SpecialFeatures}">
<DataTrigger.Value>
<src:SpecialFeatures>Color</src:SpecialFeatures>
</DataTrigger.Value>
<DataTrigger.Setters>
<Setter Property="BorderBrush" Value="DodgerBlue" TargetName="border" />
<Setter Property="Foreground" Value="Navy" TargetName="descriptionTitle" />
<Setter Property="Foreground" Value="Navy" TargetName="currentPriceTitle" />
<Setter Property="BorderThickness" Value="3" TargetName="border" />
<Setter Property="Padding" Value="5" TargetName="border" />
</DataTrigger.Setters>
</DataTrigger>
<DataTrigger Binding="{Binding Path=SpecialFeatures}">
<DataTrigger.Value>
<src:SpecialFeatures>Highlight</src:SpecialFeatures>
</DataTrigger.Value>
<Setter Property="BorderBrush" Value="Orange" TargetName="border" />
<Setter Property="Foreground" Value="Navy" TargetName="descriptionTitle" />
<Setter Property="Foreground" Value="Navy" TargetName="currentPriceTitle" />
<Setter Property="Visibility" Value="Visible" TargetName="star" />
<Setter Property="BorderThickness" Value="3" TargetName="border" />
<Setter Property="Padding" Value="5" TargetName="border" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
在使用這兩個 DataTemplate 的情況下,結果 UI 就是資料繫結是什麼一節中所顯示的 UI。 如在螢幕擷取畫面中所見,除了讓您將資料放置在控制項中,DataTemplate 也可讓您為資料定義引人注目的視覺效果。 例如,上述的 DataTemplate 使用 DataTrigger,讓 SpecialFeatures 值為 HighLight 的 AuctionItem 會使用橘色框線和星號來顯示。
如需資料範本的詳細資訊,請參閱資料範本化概觀。
資料驗證
接受使用者輸入的大部分應用程式都需要驗證邏輯,以確保使用者輸入的是預期的資訊。 驗證會依據類型、範圍、格式或其他應用程式特定的需求而進行檢查。 本節討論 WPF 中的資料驗證運作方式。
建立驗證規則與繫結的關聯
WPF 資料繫結模型可讓您建立 ValidationRules 與 Binding 物件的關聯。 例如,下列範例會將 TextBox 繫結到名為 StartPrice
的屬性,並將 ExceptionValidationRule 物件新增至 Binding.ValidationRules 屬性。
<TextBox Name="StartPriceEntryForm" Grid.Row="2"
Style="{StaticResource textStyleTextBox}" Margin="8,5,0,5" Grid.ColumnSpan="2">
<TextBox.Text>
<Binding Path="StartPrice" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<ExceptionValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
ValidationRule 物件會檢查屬性的值是否有效。 WPF 有兩種類型的內建 ValidationRule 物件:
ExceptionValidationRule 會檢查繫結來源屬性更新期間所擲回的例外狀況。 在上面的範例中,
StartPrice
的型別為整數。 當使用者輸入的值無法轉換為整數時,就會擲回例外狀況,造成繫結標記為無效。 明確設定 ExceptionValidationRule 的替代語法是在您的 Binding 或 MultiBinding 物件上將 ValidatesOnExceptions 屬性設定為true
。DataErrorValidationRule 物件會檢查實作 IDataErrorInfo 介面之物件所引發的錯誤。 如需使用此驗證規則的範例,請參閱 DataErrorValidationRule。 明確設定 DataErrorValidationRule 的替代語法是在您的 Binding 或 MultiBinding 物件上將 ValidatesOnDataErrors 屬性設定為
true
。
您也可以從 ValidationRule 類別衍生並實作 Validate 方法,來建立自己的驗證規則。 下列範例顯示資料繫結是什麼一節中 [Add Product Listing] \(新增產品清單\) 之 [Start Date] \(開始日期\) TextBox 所使用的規則。
public class FutureDateRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
// Test if date is valid
if (DateTime.TryParse(value.ToString(), out DateTime date))
{
// Date is not in the future, fail
if (DateTime.Now > date)
return new ValidationResult(false, "Please enter a date in the future.");
}
else
{
// Date is not a valid date, fail
return new ValidationResult(false, "Value is not a valid date.");
}
// Date is valid and in the future, pass
return ValidationResult.ValidResult;
}
}
Public Class FutureDateRule
Inherits ValidationRule
Public Overrides Function Validate(value As Object, cultureInfo As CultureInfo) As ValidationResult
Dim inputDate As Date
' Test if date is valid
If Date.TryParse(value.ToString, inputDate) Then
' Date is not in the future, fail
If Date.Now > inputDate Then
Return New ValidationResult(False, "Please enter a date in the future.")
End If
Else
' // Date Is Not a valid date, fail
Return New ValidationResult(False, "Value is not a valid date.")
End If
' Date is valid and in the future, pass
Return ValidationResult.ValidResult
End Function
End Class
StartDateEntryForm TextBox 會使用這個 FutureDateRule,如下列範例所示。
<TextBox Name="StartDateEntryForm" Grid.Row="3"
Validation.ErrorTemplate="{StaticResource validationTemplate}"
Style="{StaticResource textStyleTextBox}" Margin="8,5,0,5" Grid.ColumnSpan="2">
<TextBox.Text>
<Binding Path="StartDate" UpdateSourceTrigger="PropertyChanged"
Converter="{StaticResource dateConverter}" >
<Binding.ValidationRules>
<src:FutureDateRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
因為 UpdateSourceTrigger 值是 PropertyChanged,所以繫結引擎會依每個按鍵輸入來更新來源值,這代表也會依每個按鍵輸入來檢查 ValidationRules 集合中的每個規則。 我們將在<驗證程序>一節進一步討論這個部分。
提供視覺化回饋
如果使用者輸入無效的值,您可能會想要提供一些有關應用程式 UI 錯誤的回饋。 提供這類回饋的一個方式是將 Validation.ErrorTemplate 附加屬性設定為自訂 ControlTemplate。 如上一小節所示,StartDateEntryForm TextBox 使用名為 validationTemplate 的 ErrorTemplate。 下列範例顯示 validationTemplate 的定義。
<ControlTemplate x:Key="validationTemplate">
<DockPanel>
<TextBlock Foreground="Red" FontSize="20">!</TextBlock>
<AdornedElementPlaceholder/>
</DockPanel>
</ControlTemplate>
AdornedElementPlaceholder 元素會指定要裝飾的控制項應該放置的位置。
此外,您也可以使用 ToolTip 來顯示錯誤訊息。 StartDateEntryForm 和 StartPriceEntryFormTextBox 都會使用樣式 textStyleTextBox,這會建立顯示錯誤訊息的 ToolTip。 下列範例顯示 textStyleTextBox 的定義。 當繫結元素的屬性有一或多個繫結發生錯誤時,附加屬性 Validation.HasError 為 true
。
<Style x:Key="textStyleTextBox" TargetType="TextBox">
<Setter Property="Foreground" Value="#333333" />
<Setter Property="MaxLength" Value="40" />
<Setter Property="Width" Value="392" />
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
使用自訂 ErrorTemplate 和 ToolTip,當發生驗證錯誤時,StartDateEntryFormTextBox 會如下所示。
如果您的 Binding 有相關聯的驗證規則,但您未在繫結控制項上指定 ErrorTemplate,則會使用預設 ErrorTemplate 在發生驗證錯誤時通知使用者。 預設 ErrorTemplate 是控制項範本,會定義裝飾項層的紅色框線。 使用預設 ErrorTemplate 和 ToolTip,當發生驗證錯誤時,StartPriceEntryFormTextBox 的 UI 會如下所示。
如需如何提供邏輯以驗證對話方塊中所有控制項的範例,請參閱對話方塊概觀中的<自訂對話方塊>一節。
驗證程序
通常驗證發生的時機,是將目標的值傳輸到繫結來源屬性的時候。 此傳輸發生在 TwoWay 和 OneWayToSource 繫結上。 再重複聲明一次,造成來源更新的原因取決於 UpdateSourceTrigger 屬性的值,如觸發來源更新的機制一節中所述。
下列項目描述「驗證」程序。 如果在此程序期間的任何時間發生驗證錯誤或其他類型的錯誤,程序就會中止:
繫結引擎會檢查是否有為該 Binding 定義了 ValidationStep 設定為 RawProposedValue 的任何自訂 ValidationRule 物件,如果有的話,就針對每個 ValidationRule 呼叫 Validate 方法,直到其中一個規則發生錯誤或是所有規則都通過為止。
接著,繫結引擎就會在有轉換器的情況下呼叫轉換器。
如果轉換器成功,繫結引擎會檢查是否有為該 Binding 定義了 ValidationStep 設定為 ConvertedProposedValue 的任何自訂 ValidationRule 物件,如果有的話,就針對已將 ValidationStep 設定為 ConvertedProposedValue 的每個 ValidationRule 呼叫 Validate 方法,直到其中一個規則發生錯誤或是所有規則都通過為止。
繫結引擎會設定來源屬性。
繫結引擎會檢查是否有為該 Binding 定義了 ValidationStep 設定為 UpdatedValue 的任何自訂 ValidationRule 物件,如果有的話,就針對已將 ValidationStep 設定為 UpdatedValue 的每個 ValidationRule 呼叫 Validate 方法,直到其中一個規則發生錯誤或是所有規則都通過為止。 如果 DataErrorValidationRule 和繫結相關聯,且已將其 ValidationStep 設定為預設值 UpdatedValue,則會在此時檢查 DataErrorValidationRule。 此時會檢查已將 ValidatesOnDataErrors 設定為
true
的任何繫結。繫結引擎會檢查是否有為該 Binding 定義了 ValidationStep 設定為 CommittedValue 的任何自訂 ValidationRule 物件,如果有的話,就針對已將 ValidationStep 設定為 CommittedValue 的每個 ValidationRule 呼叫 Validate 方法,直到其中一個規則發生錯誤或是所有規則都通過為止。
在整個程序期間,如果有一個 ValidationRule 沒有通過,繫結引擎就會建立 ValidationError 物件,並將其新增至繫結元素的 Validation.Errors 集合。 在繫結引擎於任何指定步驟中執行 ValidationRule 物件之前,它會移除已在該步驟期間新增至繫結元素之 Validation.Errors 附加屬性的任何 ValidationError。 例如,如果 ValidationStep 設定為 UpdatedValue 的 ValidationRule 失敗,則下次驗證程序進行時,繫結引擎會在呼叫已將 ValidationStep 設定為 UpdatedValue 的任何 ValidationRule 之前,立即移除該 ValidationError。
當 Validation.Errors 不是空白時,該元素的 Validation.HasError 附加屬性會設定為 true
。 此外,如果 Binding 的 NotifyOnValidationError 屬性設定為 true
,則繫結引擎會針對元素引發 Validation.Error 附加事件。
另請注意,以任一方向 (目標至來源或來源至目標) 傳輸有效值時,都會清除 Validation.Errors 附加屬性。
如果繫結具有相關聯的 ExceptionValidationRule 或已將 ValidatesOnExceptions 屬性設定為 true
,而且在繫結引擎設定來源時擲回例外狀況,則繫結引擎會檢查 UpdateSourceExceptionFilter 是否存在。 您可選擇使用 UpdateSourceExceptionFilter 回呼來提供處理例外狀況的自訂處理常式。 如果未在 Binding 上指定 UpdateSourceExceptionFilter,則繫結引擎會建立具有例外狀況的 ValidationError,並將其新增至繫結元素的 Validation.Errors 集合。
偵錯機制
您可以在繫結相關物件上設定附加屬性 PresentationTraceSources.TraceLevel,來接收特定繫結的狀態資訊。