[轉][C#][WPF] 數據綁定 Binding 概述 (WPF .NET)

來自:https://learn.microsoft.com/zh-cn/dotnet/desktop/wpf/data/?view=netdesktop-6.0&redirectedfrom=MSDN

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 數據。

數據綁定基本概念

不論要綁定什麼元素,也不論數據源是什麼性質,每個綁定都始終遵循下圖所示的模型。

 

如圖所示,數據綁定實質上是綁定目標與綁定源之間的橋樑。 該圖演示了以下基本的 WPF 數據綁定概念:

  • 通常情況下,每個綁定具有四個組件:

    • 綁定目標對象。
    • 目標屬性。
    • 綁定源。
    • 指向綁定源中要使用的值的路徑。

    例如,如果將 TextBox 的內容綁定到 Employee.Name 屬性,則可以類似如下所示設置綁定:

    設置“值”
    目標 TextBox
    目標屬性 Text
    源對象 Employee
    源對象值路徑 Name
  • 目標屬性必須爲依賴屬性。

    大多數 UIElement 屬性都是依賴屬性,而大多數依賴屬性(只讀屬性除外)默認支持數據綁定。 只有從 DependencyObject 派生的類型才能定義依賴項屬性。 所有 UIElement 類型從 DependencyObject 派生。

  • 綁定源不限於自定義 .NET 對象。

    儘管未在圖中顯示,但請注意,綁定源對象不限於自定義 .NET 對象。 WPF 數據綁定支持 .NET 對象、XML 甚至是 XAML 元素對象形式的數據。 例如,綁定源可以是 UIElement、任何列表對象、ADO.NET 或 Web 服務對象,或包含 XML 數據的 XmlNode。 有關詳細信息,請參閱綁定源概述

請務必記住,在建立綁定時,需要將綁定目標綁定到綁定源。 例如,如果要使用數據綁定在 ListBox 中顯示一些基礎 XML 數據,則需要將 ListBox 綁定到 XML 數據。

若要建立綁定,請使用 Binding 對象。 本文的其餘部分討論了與 Binding 對象相關的許多概念以及該對象的一些屬性和用法。

數據上下文

當在 XAML 元素上聲明數據綁定時,它們會通過查看其直接的 DataContext 屬性來解析數據綁定。 數據上下文通常是綁定源值路徑評估的綁定源對象。 可以在綁定中重寫此行爲,並設置特定的綁定源對象值。 如果未設置承載綁定的對象的 DataContext 屬性,則將檢查父元素的 DataContext 屬性,依此類推,直到 XAML 對象樹的根。 簡而言之,除非在對象上顯式設置,否則用於解析綁定的數據上下文將繼承自父級。

綁定可以配置爲使用特定的對象進行解析,而不是使用數據上下文進行綁定解析。 例如,在將對象的前景色綁定到另一個對象的背景色時,將使用直接指定源對象。 無需數據上下文,因爲綁定在這兩個對象之間解析。 相反,未綁定到特定源對象的綁定使用數據上下文解析。

當 DataContext 屬性發生更改時,重新評估可能會受數據上下文影響的所有綁定。

數據流的方向

正如上圖中的箭頭所示,綁定的數據流可以從綁定目標流向綁定源(例如,當用戶編輯 TextBox 的值時,源值會發生更改)和/或(在綁定源提供正確通知的情況下)從綁定源流向綁定目標(例如,TextBox 內容會隨綁定源中的更改而進行更新)。

你可能希望應用允許用戶更改數據,然後將該數據傳播回源對象。 或者,可能不希望允許用戶更新源數據。 可以通過設置 Binding.Mode 來控制數據流。

此圖演示了不同類型的數據流:

 

  • 通過 OneWay 綁定,對源屬性的更改會自動更新目標屬性,但對目標屬性的更改不會傳播回源屬性。 如果綁定的控件爲隱式只讀,則此類型的綁定適用。 例如,可能會綁定到股票行情自動收錄器這樣的源,也可能目標屬性沒有用於進行更改的控件接口(例如表的數據綁定背景色)。 如果無需監視目標屬性的更改,則使用 OneWay 綁定模式可避免 TwoWay 綁定模式的系統開銷。

  • 通過 TwoWay 綁定,更改源屬性或目標屬性時會自動更新另一方。 此類型的綁定適用於可編輯窗體或其他完全交互式 UI 方案。 大多數屬性默認爲 OneWay 綁定,但某些依賴屬性(通常爲用戶可編輯控件的屬性,例如 TextBox.Text 和 CheckBox.IsChecked)默認爲 TwoWay 綁定。

    用於確定依賴項屬性綁定在默認情況下是單向還是雙向的編程方法是:使用 DependencyProperty.GetMetadata 獲取屬性元數據。 此方法的返回類型爲 PropertyMetadata,它不包含任何有關綁定的元數據。 但是,如果可以將此類型強制轉換爲派生的 FrameworkPropertyMetadata,則可以檢查 FrameworkPropertyMetadata.BindsTwoWayByDefault 屬性的布爾值。 以下代碼示例演示瞭如何獲取 TextBox.Text 屬性的元數據:

    public static void PrintMetadata()
    {
        // Get the metadata for the property
        PropertyMetadata metadata = TextBox.TextProperty.GetMetadata(typeof(TextBox));
    
        // Check if metadata type is FrameworkPropertyMetadata
        if (metadata is FrameworkPropertyMetadata frameworkMetadata)
        {
            System.Diagnostics.Debug.WriteLine($"TextBox.Text property metadata:");
            System.Diagnostics.Debug.WriteLine($"  BindsTwoWayByDefault: {frameworkMetadata.BindsTwoWayByDefault}");
            System.Diagnostics.Debug.WriteLine($"  IsDataBindingAllowed: {frameworkMetadata.IsDataBindingAllowed}");
            System.Diagnostics.Debug.WriteLine($"        AffectsArrange: {frameworkMetadata.AffectsArrange}");
            System.Diagnostics.Debug.WriteLine($"        AffectsMeasure: {frameworkMetadata.AffectsMeasure}");
            System.Diagnostics.Debug.WriteLine($"         AffectsRender: {frameworkMetadata.AffectsRender}");
            System.Diagnostics.Debug.WriteLine($"              Inherits: {frameworkMetadata.Inherits}");
        }
    
        /*  Displays:
         *  
         *  TextBox.Text property metadata:
         *    BindsTwoWayByDefault: True
         *    IsDataBindingAllowed: True
         *          AffectsArrange: False
         *          AffectsMeasure: False
         *           AffectsRender: False
         *                Inherits: False
        */
    }
  • OneWayToSource 綁定與 OneWay 綁定相反;當目標屬性更改時,它會更新源屬性。 一個示例方案是隻需要從 UI 重新計算源值的情況。

  • OneTime 綁定未在圖中顯示,該綁定會使源屬性初始化目標屬性,但不傳播後續更改。 如果數據上下文發生更改,或者數據上下文中的對象發生更改,則更改不會在目標屬性中反映。 如果適合使用當前狀態的快照或數據實際爲靜態數據,則此類型的綁定適合。 如果你想使用源屬性中的某個值來初始化目標屬性,且提前不知道數據上下文,則此類型的綁定也有用。 此模式實質上是 OneWay 綁定的一種簡化形式,它在源值不更改的情況下提供更好的性能。

 若要檢測源更改(適用於 OneWay 和 TwoWay 綁定),則源必須實現合適的屬性更改通知機制,例如 INotifyPropertyChanged。 請參閱如何:實現屬性更改通知 (.NET Framework),獲取 INotifyPropertyChanged 實現的示例。

Binding.Mode 屬性提供有關綁定模式的詳細信息,以及如何指定綁定方向的示例。

觸發源更新的因素

TwoWay 或 OneWayToSource 綁定偵聽目標屬性中的更改,並將更改傳播回源(稱爲更新源)。 例如,可以編輯文本框的文本以更改基礎源值。

但是,在編輯文本時或完成文本編輯後控件失去焦點時,源值是否會更新? Binding.UpdateSourceTrigger 屬性確定觸發源更新的因素。 下圖中右箭頭的點說明了 Binding.UpdateSourceTrigger 屬性的角色。

 如果 UpdateSourceTrigger 值爲 UpdateSourceTrigger.PropertyChanged,則目標屬性更改後,TwoWay 或 OneWayToSource 綁定的右箭頭指向的值會立即更新。 但是,如果 UpdateSourceTrigger 值爲 LostFocus,則僅當目標屬性失去焦點時纔會使用新值更新該值。

與 Mode 屬性類似,不同的依賴屬性具有不同的默認 UpdateSourceTrigger 值。 大多數依賴屬性的默認值爲 PropertyChanged,這將導致源屬性的值在目標屬性值更改時立即更改。 即時更改適用於 CheckBox 和其他簡單控件。 但對於文本字段,每次擊鍵後都進行更新會降低性能,用戶也沒有機會在提交新值之前使用 Backspace 鍵修改鍵入錯誤。 例如,TextBox.Text 屬性默認爲 LostFocus 的 UpdateSourceTrigger 值,這會導致源值僅在控件元素失去焦點時(而不是在 TextBox.Text 屬性更改時)更改。 有關如何查找依賴屬性的默認值的信息,請參閱 UpdateSourceTrigger 屬性頁。

下表以 TextBox 爲例,提供每個 UpdateSourceTrigger 值的示例方案。

UpdateSourceTrigger 值源值更新時間TextBox 的示例方案
LostFocusTextBox.Text 的默認值) TextBox 控件失去焦點時。 與驗證邏輯關聯的 TextBox(請參閱下文的數據驗證)。
PropertyChanged 鍵入 TextBox 時。 聊天室窗口中的 TextBox 控件。
Explicit 應用調用 UpdateSource 時。 可編輯窗體中的 TextBox 控件(僅當用戶按“提交”按鈕時才更新源值)。

有關示例,請參閱如何:控制 TextBox 文本更新源的時間 (.NET Framework)

數據綁定的示例

有關數據綁定的示例,請參閱數據綁定演示(顯示拍賣項的列表)中的以下應用 UI。

 應用演示了數據綁定的以下功能:

  • ListBox 的內容已綁定到 AuctionItem 對象的集合。 AuctionItem 對象具有 Description、StartPrice、StartDate、Category 和 SpecialFeatures 等屬性。

  • ListBox 中顯示的數據(AuctionItem 對象)已進行模板化,以便顯示每個項的說明和當前價格。 通過使用 DataTemplate 來創建模板。 此外,每個項的外觀取決於要顯示的 AuctionItem 的 SpecialFeatures 值。 如果 AuctionItem 的 SpecialFeatures 值爲 Color,則該項具有藍色邊框。 如果值爲 Highlight,則該項具有橙色邊框和一個星號。 數據模板化部分提供了數據模板化的相關信息。

  • 用戶可以使用提供的 CheckBoxes 對數據進行分組、篩選或排序。 在上圖中,選中了“按類別分組”和“按類別和日期排序”CheckBoxes。 你可能已注意到,數據按產品類別分組,類別名稱按字母順序排序。 這些項還按每個類別中的開始日期排序,但難以從圖中注意到這一點。 排序使用集合視圖實現。 綁定到集合部分討論了集合視圖。

  • 當用戶選擇某個項時,ContentControl 顯示所選項的詳細信息。 此體驗稱爲主-從方案。 主-從方案部分提供有關此綁定類型的信息。

  • StartDate 屬性的類型爲 DateTime,該類型返回一個包括精確到毫秒的時間的日期。 在此應用中,使用了一個自定義轉換器,以便顯示較短的日期字符串。 數據轉換部分提供有關轉換器的信息。

當用戶選擇“添加產品”按鈕時,會出現以下窗體。

 用戶可以編輯窗體中的字段,使用簡略或詳細預覽窗格預覽產品清單,然後選擇 Submit 以添加新的產品清單。 任何現有的分組、篩選和排序設置都將應用於新條目。 在這種特殊情況下,上圖中輸入的項會作爲 Computer 類別中的第二項顯示。

“開始日期”TextBox 中提供的驗證邏輯未在此圖中顯示。 如果用戶輸入一個無效日期(格式無效或日期已過),則會通過 ToolTip 和 TextBox 旁邊顯示的紅色感嘆號來通知用戶。 數據驗證一節討論瞭如何創建驗證邏輯。

在詳細介紹數據綁定的上述不同功能之前,我們會先討論對理解 WPF 數據綁定非常重要的基本概念。

創建綁定

前面部分中討論的一些概念可以重申爲:使用 Binding 對象建立綁定,且每個綁定通常具有四個組件:綁定目標、目標屬性、綁定源以及指向要使用的源值的路徑。 本節討論如何設置綁定。

綁定源綁定到元素的活動 DataContext。 如果元素沒有顯式定義 DataContext,則會自動繼承。

請考慮以下示例,其中的綁定源對象是一個名爲 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 屬性指定值。 在某些情況下,使用 Path 屬性(即使數據爲 XML)可能更爲合適。 例如,如果要訪問返回的 XmlNode(作爲 XPath 查詢的結果)的 Name 屬性,則除 XPath 屬性外,還應使用 Path 屬性。

有關詳細信息,請參閱 Path 和 XPath 屬性。

雖然我們已強調要使用的值的 Path 是綁定的四個必需組件之一,但在要綁定到整個對象的方案中,要使用的值會與綁定源對象相同。 在這些情況下,可以不指定 Path。 請看下面的示例。

<ListBox ItemsSource="{Binding}"
         IsSynchronizedWithCurrentItem="true"/>

以上示例使用空綁定語法:{Binding}。 在此示例中,ListBox 從父 DockPanel 元素繼承 DataContext(此示例中未顯示)。 未指定路徑時,默認爲綁定到整個對象。 換句話說,此示例中的路徑已省略,因爲要將 ItemsSource 屬性綁定到整個對象。 (有關深入討論,請參閱綁定到集合部分。)

除了綁定到集合以外,在希望綁定到整個對象,而不是僅綁定到對象的單個屬性時,也可以使用此方案。 例如,如果源對象的類型爲 String,則可能僅希望綁定到字符串本身。 另一種常見情況是希望將一個元素綁定到一個具有多個屬性的對象。

你可能需要應用自定義邏輯,以便數據對於綁定的目標屬性有意義。 如果不存在默認類型轉換,則自定義邏輯可能採用自定義轉換器的形式。 有關轉換器的信息,請參閱數據轉換

Binding 和 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);

可以使用同一 myBinding 對象來創建其他綁定。 例如,可使用 myBinding 對象將複選框的文本內容綁定到 ColorName。 在該方案中,將有兩個 BindingExpression 實例共享 myBinding 對象。

通過對數據綁定對象調用 GetBindingExpression 來返回 BindingExpression 對象。 以下文章演示了 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;
    }
}

有關詳細信息,請參閱IValueConverter

現在,使用的是自定義轉換器而不是默認轉換,關係圖如下所示。

 

重申一下,由於要綁定到的類型中提供了類型轉換器,因此可以使用默認轉換。 此行爲取決於目標中可用的類型轉換器。 如果無法確定,請創建自己的轉換器。

下面提供了一些典型方案,在這些方案中,實現數據轉換器非常有意義:

  • 數據應根據區域性以不同方式顯示。 例如,可能需要根據在特定區域性中使用的約定,實現貨幣轉換器或日曆日期/時間轉換器。

  • 使用的數據不一定會更改屬性的文本值,但會更改其他某個值(如圖像的源,或顯示文本的顏色或樣式)。 在這種情況下,可以通過轉換可能不合適的屬性綁定(如將文本字段綁定到表單元格的 Background 屬性)來使用轉換器。

  • 多個控件或控件的多個屬性會綁定到相同數據。 在這種情況下,主綁定可能僅顯示文本,而其他綁定則處理特定的顯示問題,但仍使用同一綁定作爲源信息。

  • 目標屬性具有綁定集合,稱爲 MultiBinding。 對於 MultiBinding,使用自定義 IMultiValueConverter 從綁定的值中生成最終值。 例如,可以從紅色、藍色和綠色的值來計算顏色,這些值可能來自相同綁定源對象,也可能來自不同綁定源對象。 有關示例和信息,請參閱 MultiBinding

綁定到集合

綁定源對象可以被視爲其屬性包含數據的單個對象,也可以被視爲通常組合在一起的多態對象的數據集合(例如數據庫查詢的結果)。 目前爲止,我們僅討論了綁定到單個對象, 但綁定到數據集合也是常見方案。 例如,一種常見方案是使用 ItemsControl(例如 ListBoxListView 或 TreeView)來顯示數據集合,如在什麼是數據綁定部分所示的應用中。

幸運的是,基本關係圖仍然適用。 如果將 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 代理。 在此特定示例中,視圖的 Source 綁定到當前應用對象的 AuctionItem 集合(類型爲 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 方法。 有關示例,請參閱獲取數據集合的默認視圖 (.NET Framework)

包含 ADO.NET DataTables 的集合視圖

爲了提高性能,ADO.NET DataTable 或 DataView 對象的集合視圖將排序和篩選委託給 DataView,這導致排序和篩選在數據源的所有集合視圖之間共享。 若要使每個集合視圖都能獨立進行排序和篩選,請使用每個集合視圖自己的 DataView 對象進行初始化。

排序

如前所述,視圖可以將排序順序應用於集合。 如同在基礎集合中一樣,數據可能具有或不具有相關的固有順序。 藉助集合視圖,可以根據自己提供的比較條件來強制確定順序,或更改默認順序。 由於這是基於客戶端的數據視圖,因此一種常見情況是用戶可能希望根據列對應的值,對多列表格數據進行排序。 通過使用視圖,可以應用這種用戶實施的排序,而無需對基礎集合進行任何更改,甚至不必再次查詢集合內容。 有關示例,請參閱在單擊標題時對 GridView 列進行排序 (.NET Framework)

以下示例演示了什麼是數據綁定部分中的應用 UI 的“按類別和日期排序”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));
}

篩選

視圖還可以將篩選器應用於集合,以便視圖僅顯示完整集合的特定子集。 可以根據條件在數據中進行篩選。 例如,正如什麼是數據綁定部分中的應用所做的那樣,“僅顯示成交商品”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;
}

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;
}

如果直接使用其中一個 CollectionView 類而不是 CollectionViewSource,則可以使用 Filter 屬性指定回叫。 有關示例,請參閱篩選視圖中的數據 (.NET Framework)

分組

除了用來查看 IEnumerable 集合的內部類之外,所有集合視圖都支持分組功能,用戶可以利用此功能將集合視圖中的集合劃分成邏輯組。 這些組可以是顯式的,由用戶提供組列表;也可以是隱式的,這些組依據數據動態生成。

以下示例演示了“按類別分組”CheckBox 的邏輯。

// This groups the items in the view by the property "Category"
var groupDescription = new PropertyGroupDescription();
groupDescription.PropertyName = "Category";
listingDataView.GroupDescriptions.Add(groupDescription);

有關其他分組示例,請參閱對實現 GridView 的 ListView 中的項進行分組 (.NET Framework)

當前項指針

視圖還支持當前項的概念。 可以在集合視圖中的對象之間導航。 在導航時,你是在移動項指針,該指針可用於檢索存在於集合中特定位置的對象。 有關示例,請參閱在數據 CollectionView 中的對象之間導航 (.NET Framework)

由於 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 顯示所選項的詳細信息。

只需將兩個或更多控件綁定到同一視圖即可實現主-從方案。 數據綁定演示中的以下示例演示了在什麼是數據綁定部分中的應用 UI 上看到的 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 才能起作用。

有關其他示例,請參閱綁定到集合並基於選擇顯示信息 (.NET Framework)對層次結構數據使用主-從模式 (.NET Framework)

你可能已經注意到上述示例使用了一個模板。 實際上,如果不使用模板(ContentControl 顯式使用的模板以及 ListBox 隱式使用的模板),數據不會按照我們希望的方式顯示。 現在,我們開始介紹下一節中的數據模板化。

數據模板化

如果不使用數據模板,數據綁定示例部分中的應用 UI 將如下所示:

 

如前面部分中的示例所示,ListBox 控件和 ContentControl 都綁定到 AuctionItem 的整個集合對象(更具體地說,是綁定到集合對象視圖)。 如果未提供如何顯示數據集合的特定說明,則 ListBox 會以字符串形式顯示基礎集合中的每個對象,ContentControl 會以字符串形式顯示綁定到的對象。

爲了解決該問題,應用定義了 DataTemplates。 如前面部分中的示例所示,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 會顯示爲帶有橙色邊框和一個星號。

有關數據模板的詳細信息,請參閱數據模板化概述 (.NET Framework)

數據驗證

接受用戶輸入的大多數應用都需要具有驗證邏輯,以確保用戶輸入了預期信息。 可基於類型、範圍、格式或特定於應用的其他要求執行驗證檢查。 本部分討論數據驗證在 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 對象:

還可以通過從 ValidationRule 類派生並實現 Validate 方法來創建自己的驗證規則。 以下示例演示了什麼是數據綁定部分中添加產品清單“起始日期”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;
    }
}

StartDateEntryFormTextBox 使用此 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。 如前面部分所示,StartDateEntryFormTextBox 使用名爲 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 (Validation.Errors).CurrentItem.ErrorContent, RelativeSource={RelativeSource Self}}" />
        </Trigger>
    </Style.Triggers>
</Style>

使用自定義 ErrorTemplate 和 ToolTip 時,StartDateEntryFormTextBox 在發生驗證錯誤時如下所示。

 如果 Binding 具有關聯的驗證規則,但未在綁定控件上指定 ErrorTemplate,則發生驗證錯誤時,將使用默認的 ErrorTemplate 通知用戶。 默認的 ErrorTemplate 是一個控件模板,它在裝飾層中定義紅色邊框。 使用默認的 ErrorTemplate 和 ToolTip 時,StartPriceEntryFormTextBox 的 UI 在發生驗證錯誤時如下所示。

 

有關如何提供邏輯以驗證對話框中所有控件的示例,請參閱對話框概述中的“自定義對話框”部分。

驗證過程

通常,在目標的值傳輸到綁定源屬性時會進行驗證。 此傳輸在 TwoWay 和 OneWayToSource 綁定上發生。 重申一下,導致源更新的因素取決於 UpdateSourceTrigger 屬性的值,如觸發源更新的因素部分所述。

以下各項描述了驗證過程。 只要驗證過程中發生驗證錯誤或其他類型的錯誤,該過程就會中斷:

  1. 綁定引擎檢查是否爲該 Binding 定義了任何將 ValidationStep 設置爲 RawProposedValue 的自定義 ValidationRule 對象,在這種情況下,綁定引擎將對每個 ValidationRule 調用 Validate 方法,直到其中一個出錯或直到全部通過。

  2. 綁定引擎隨後會調用轉換器(如果存在)。

  3. 如果轉換器成功,則綁定引擎會檢查是否爲該 Binding 定義了任何將 ValidationStep 設置爲 ConvertedProposedValue 的自定義 ValidationRule 對象,在這種情況下,綁定引擎將對每個 ValidationRule(將 ValidationStep 設置爲 ConvertedProposedValue)調用 Validate 方法,直到其中一個出錯或直到全部通過。

  4. 綁定引擎設置源屬性。

  5. 綁定引擎檢查是否爲該 Binding 定義了任何將 ValidationStep 設置爲 UpdatedValue 的自定義 ValidationRule 對象,在這種情況下,綁定引擎將對每個 ValidationRule(將 ValidationStep 設置爲 UpdatedValue)調用 Validate 方法,直到其中一個出錯或直到全部通過。 如果 DataErrorValidationRule 與綁定關聯並且其 ValidationStep 設置爲默認的 UpdatedValue,則此時將檢查 DataErrorValidationRule。 此時檢查將 ValidatesOnDataErrors 設置爲 true 的所有綁定。

  6. 綁定引擎檢查是否爲該 Binding 定義了任何將 ValidationStep 設置爲 CommittedValue 的自定義 ValidationRule 對象,在這種情況下,綁定引擎將對每個 ValidationRule(將 ValidationStep 設置爲 CommittedValue)調用 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,以接收有關特定綁定狀態的信息。

另請參閱

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章