共用方式為


Windows 資料繫結深入

本文說明使用 Microsoft.UI.Xaml.Data 命名空間中的 API 的 WinUI 資料系結功能。

備註

本主題詳細說明資料繫結功能。 如需簡短、實用的介紹,請參閱 資料繫結概觀

重要 API

簡介

資料繫結是一種技術,可讓您的應用程式 UI 有效率地顯示和同步處理資料。 透過將資料問題與 UI 問題分開,它簡化了應用程式設計、增強了可讀性並提高了可維護性。

您可以使用資料繫結,在第一次顯示 UI 時只顯示資料來源中的值,但不回應這些值的變更。 這種繫結模式稱為 一次性,而且它適用於在執行階段期間不會變更的值。 或者,您可以選擇「觀察」值,並在值變更時更新 UI。 此模式稱為 單向,適用於唯讀資料。 最後,您可以選擇觀察和更新,讓使用者對 UI 中的值所做的變更會自動推送回資料來源。 這種模式稱為 雙向,它適用於讀寫資料。 以下是一些範例。

  • 您可以使用一次性模式將 影像 繫結至目前使用者的相片。
  • 您可以使用單向模式,將 ListView 繫結至依報紙區段分組的即時新聞文章集合。
  • 您可以使用雙向模式,將 TextBox 繫結至表單中的客戶名稱。

與模式無關,有兩種繫結,而且您通常會在 UI 標記中宣告這兩種繫結。 您可以選擇使用 {x:Bind} 標記延伸模組{Binding} 標記延伸模組。 您甚至可以在同一個應用程式中混合使用兩者,甚至在同一個 UI 元素上。 {x:Bind} 是 Windows 10 的 UWP 中的新功能,它具有更好的性能。 本主題中所述的所有詳細資料都適用於這兩種繫結,除非我們明確說明其他說明。

示範 {x:Bind} 的 UWP 範例應用程式

示範 {Binding} 的 UWP 範例應用程式

每個裝訂都涉及這些部分

  • 具有約束力的來源。 此來源提供繫結的資料。 它可以是任何類別的實例,其成員的值要在 UI 中顯示。
  • 繫結目標。 此目標是您的 UI 中 FrameworkElementDependencyProperty,負責顯示資料。
  • 繫結物件。 此物件會將資料值從來源傳輸回目標,並選擇性地從目標傳輸回來源。 繫結物件是在 XAML 載入時從 {x:Bind}{Binding} 標記延伸模組建立。

在下列各節中,您將仔細查看繫結來源、繫結目標和繫結物件。 這些區段連結在一起,以及將按鈕內容繫結至名為 NextButtonText的字串屬性的範例,該屬性屬於名為 HostViewModel的類別。

繫結來源

以下是可用作繫結來源的類別的基本實作。

public class HostViewModel
{
    public HostViewModel()
    {
        NextButtonText = "Next";
    }

    public string NextButtonText { get; set; }
}

該實作 HostViewModel 及其屬性 NextButtonText ,僅適用於一次性繫結。 但單向和雙向綁定極為常見。 在這些類型的繫結中,UI 會自動更新,以回應繫結來源資料值的變更。 若要讓這些類型的繫結正常運作,您必須讓繫 結來源可供 繫結物件觀察。 因此,在我們的範例中,如果您想要單向或雙向繫結至 NextButtonText 屬性,則在執行階段對該屬性值發生的任何變更都需要對繫結物件進行觀察。

其中一種方式是從 DependencyObject 衍生代表繫結來源的類別,並透過 DependencyProperty* 公開資料值。 這就是 FrameworkElement 變成可觀察的方式。 A FrameworkElement 是開箱即用的良好綁定來源。

使類別可觀察的一種更輕量級的方法(對於已經具有基類的類別來說是必要的方法)是實現 System.ComponentModel.INotifyPropertyChanged。 這種方法涉及實現名為 的單一事件 PropertyChanged。 以下程式碼顯示了使用 using HostViewModel 的範例。

...
using System.ComponentModel;
using System.Runtime.CompilerServices;
...
public class HostViewModel : INotifyPropertyChanged
{
    private string nextButtonText;

    public event PropertyChangedEventHandler PropertyChanged = delegate { };

    public HostViewModel()
    {
        NextButtonText = "Next";
    }

    public string NextButtonText
    {
        get { return nextButtonText; }
        set
        {
            nextButtonText = value;
            OnPropertyChanged();
        }
    }

    public void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        // Raise the PropertyChanged event, passing the name of the property whose value has changed.
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

現在該 NextButtonText 屬性是可觀察的。 當您撰寫該屬性的單向或雙向繫結時 (稍後會示範如何),產生的繫結物件會訂閱事件 PropertyChanged 。 引發該事件時,繫結物件的處理常式會收到引數,其中包含已變更屬性的名稱。 這就是繫結物件知道要再次讀取哪個屬性值的方式。

因此,如果您使用 C# BindableBase ,則不需要多次實作先前顯示的模式,您可以衍生自 QuizGame 範例 (在 “Common” 資料夾中) 中找到的基類。 這是一個看起來如何的範例。

public class HostViewModel : BindableBase
{
    private string nextButtonText;

    public HostViewModel()
    {
        NextButtonText = "Next";
    }

    public string NextButtonText
    {
        get { return nextButtonText; }
        set { SetProperty(ref nextButtonText, value); }
    }
}

引發 PropertyChanged 事件,若引數為 String.Emptynull,則表示應重新讀取物件上的所有非索引子屬性。 您可以針對特定索引子使用「Item[indexer]」引數 (其中 索引子 是索引值) ,或針對所有索引子使用 「Item[]」 值,來引發事件,以指出物件上的索引子屬性已變更。

您可以將繫結來源視為屬性包含資料的單一物件,或視為物件的集合。 在 C# 程式碼中,您可以一次性系結至實作 清單<T> 的物件,以顯示在執行階段不會變更的集合。 對於可觀察的集合 (觀察何時將項目新增至集合和從集合中移除),請改為單向繫結至 ObservableCollection<T> 。 若要系結至您自己的集合類別,請使用下表中的指引。

Scenario C# (CLR) C++/WinRT
繫結至物件。 可以是任何物件。 可以是任何物件。
從繫結物件取得屬性變更通知。 物件必須實作 INotifyPropertyChanged 物件必須實作 INotifyPropertyChanged
繫結至集合。 清單<T> IInspectableIVectorIBindableObservableVector。 請參閱 XAML 專案控制項;繫結至 C++/WinRT 集合使用 C++/WinRT 的集合
從繫結的集合取得集合變更通知。 可觀察集合<T> IObservableVector 的 IInspectable。 例如, winrt::single_threaded_observable_vector<T>
實作支援繫結的集合。 擴充 清單<T> 或實作 IListIList<物件>、 IEnumerableIEnumerable<物件>。 繫結至泛型 IList<T>IEnumerable<T> 不支援。 實作 IInspectableIVector。 請參閱 XAML 專案控制項;繫結至 C++/WinRT 集合使用 C++/WinRT 的集合
實作支援集合變更通知的集合。 擴充 ObservableCollection<T> 或實作 (非泛型) IListINotifyCollectionChanged 實作 IInspectableIObservableVectorIBindableObservableVector
實作支援累加載入的集合。 擴充 ObservableCollection<T> 或實作 (非泛型) IListINotifyCollectionChanged。 此外,實作 ISupportIncrementalLoading 實作 IInspectableIObservableVectorIBindableObservableVector。 此外,實作 ISupportIncrementalLoading

您可以使用累加載入,將清單控制項繫結至任意大型資料來源,但仍能達到高效能。 例如,您可以將清單控制項繫結至 Bing 影像查詢結果,而不需要一次載入所有結果。 相反地,您可以立即只載入部分結果,並視需要載入其他結果。 若要支援累加載入,您必須在支援集合變更通知的資料來源上實作 ISupportIncrementalLoading 。 當資料繫結引擎要求更多資料時,您的資料來源必須提出適當的要求、整合結果,然後傳送適當的通知,才能更新 UI。

綁定目標

在下列兩個範例中, Button.Content 屬性是繫結目標。 其值會設定為宣告繫結物件的標記擴展。 第一個範例顯示 {x:Bind},第二個範例顯示 {Binding}。 在標記中宣告繫結是常見的情況,因為它方便、可讀且易於工具化。 但如果需要,您可以避免使用標記,而是以命令式(編程方式)改為建立 Binding 類別的實例。

<Button Content="{x:Bind ...}" ... />
<Button Content="{Binding ...}" ... />

如果您使用 C++/WinRT,則必須將 BindableAttribute 屬性新增至您想要使用 {Binding} 標記延伸模組的任何執行階段類別。

這很重要

如果您使用 C++/WinRT,則 BindableAttribute 屬性可與 Windows 應用程式 SDK 搭配使用。 如果沒有該屬性,您必須實作 ICustomPropertyProviderICustomProperty 介面,才能使用 {Binding} 標記延伸模組。

使用 {x:Bind} 宣告的繫結物件

撰寫 {x:Bind} 標記之前,您必須從代表標記頁面的類別公開繫結來源類別。 將屬性 (在本例中為類型 HostViewModel ) 新增至視窗 MainWindow 類別。

namespace DataBindingInDepth
{
    public sealed partial class MainWindow : Window
    {
        public MainWindow()
        {
            this.InitializeComponent();
            ViewModel = new HostViewModel();
        }
    
        public HostViewModel ViewModel { get; set; }
    }
}

新增屬性之後,您可以仔細查看宣告繫結物件的標記。 下列範例使用您在稍早的「繫結目標」一節中看到的相同 Button.Content 繫結目標。 它會顯示繫結目標已繫結至 HostViewModel.NextButtonText 屬性。

<!-- MainWindow.xaml -->
<Window x:Class="DataBindingInDepth.MainWindow" ... >
    <Button Content="{x:Bind Path=ViewModel.NextButtonText, Mode=OneWay}" ... />
</Window>

請注意您指定給Path的值。 視窗會在自己的內容中解譯此值。 在此情況下,路徑會從參考您剛新增至MainWindow頁面的ViewModel屬性開始。 該屬性會傳回實 HostViewModel 例,因此您可以 入該物件以存取該 HostViewModel.NextButtonText 屬性。 您可以指定 Mode 置換一次性的 {x:Bind} 預設值。

Path 屬性支援各種語法選項,可系結至巢狀屬性、附加屬性,以及整數和字串索引子。 如需詳細資訊,請參閱 屬性路徑語法。 繫結至字串索引子可讓您取得系結至動態屬性的效果,而不需要實作 ICustomPropertyProvider。 如需其他設定,請參閱 {x:Bind} 標記延伸模組

若要說明屬性是可觀察的,請將 Click 事件處理常式新增至按鈕,並更新 HostViewModel.NextButtonText 的值。 建置、執行,然後按一下按鈕,以查看按鈕更新 Content 的值。

// MainWindow.xaml.cs
private void Button_Click(object sender, RoutedEventArgs e)
{
    ViewModel.NextButtonText = "Updated Next button text";
}

備註

TextBox 失去焦點時,TextBox.Text 的變更會傳送至雙向系結來源,而不是在每次使用者按鍵之後。

DataTemplate 和 x:DataType

DataTemplate 內(無論您是將其用作項目範本、內容範本或標頭範本),Path 的值不會在視窗的內容中被解譯。 相反地,它會在您正在範本化的資料物件的相關環境中運作。 當您在資料範本中使用時 {x:Bind} ,您可以在編譯階段驗證其繫結,並為其產生有效率的程式碼。 為此,需要 DataTemplate 使用 x:DataType宣告其資料物件的類型。 下列範例可用作繫結至SampleDataGroup物件集合的項目控制ItemTemplate

<DataTemplate x:Key="SimpleItemTemplate" x:DataType="data:SampleDataGroup">
    <StackPanel Orientation="Vertical" Height="50">
      <TextBlock Text="{x:Bind Title}"/>
      <TextBlock Text="{x:Bind Description}"/>
    </StackPanel>
  </DataTemplate>

路徑中的弱型別物件

假設您有一個名為 SampleDataGroup 的類型,該類型實作名為 Title的字串屬性。 您還有一個屬性 MainWindow.SampleDataGroupAsObject,其類型為 object,但實際上返回的是 SampleDataGroup 的實例。 繫結<TextBlock Text="{x:Bind SampleDataGroupAsObject.Title}"/>會導致編譯錯誤,因為在類型Title上找不到屬性object。 若要修正此錯誤,請將型別轉換新增至您的 Path 語法,如下所示: <TextBlock Text="{x:Bind ((data:SampleDataGroup)SampleDataGroupAsObject).Title}"/>。 這是另一個範例,其中 Element 宣告為 object 但實際上是 TextBlock<TextBlock Text="{x:Bind Element.Text}"/>。 鑄造可修正此問題: <TextBlock Text="{x:Bind ((TextBlock)Element).Text}"/>

如果您的資料以非同步方式載入

Windows 的部分類別會產生程式碼,以便在編譯階段支援 {x:Bind} 。 您可以在資料夾中找到 obj 這些檔案,名稱類似於 (對於 C#) <view name>.g.cs。 產生的程式碼包含視窗 Loading 事件的處理常式。 該處理常式會在代表視窗繫結的產生類別上呼叫 Initialize 方法。 Initialize 呼叫 Update 開始在繫結來源和目標之間移動資料。 Loading 會在視窗或使用者控制項的第一個量值傳遞之前引發。 如果您的資料以非同步方式載入,則在呼叫時可能尚未準備就緒 Initialize 。 載入資料之後,您可以呼叫 this.Bindings.Update();來強制一次性繫結初始化。 如果您只需要針對非同步載入的資料進行一次性綁定,將它們初始化為這種方式會比使用單向綁定並監聽資料變更便宜得多。 如果您的資料未進行精細變更,而且可能會在特定動作中更新,您可以一次性進行繫結,並隨時呼叫 Update來強制手動更新。

備註

{x:Bind} 不適合延遲繫結案例,例如導覽 JSON 物件的字典結構,也不適合鴨子類型。 「鴨子類型」是一種基於屬性名稱詞彙匹配的弱類型形式(例如,「如果它像鴨子一樣走路、游泳和嘎嘎叫,那麼它就是鴨子」)。 使用鴨子類型時,屬性的 Age 繫結會同樣滿足 a PersonWine 物件 (假設這些類型每個類型都有屬性 Age ) 。 針對這些案例,請使用 {Binding} 標記延伸模組。

使用 {Binding} 宣告的繫結物件

如果您使用 C++/WinRT,請將 BindableAttribute 屬性新增至您在使用 {Binding} 標記延伸模組時想要系結的任何執行階段類別。 若要使用 {x:Bind},您不需要該屬性。

// HostViewModel.idl
// Add this attribute:
[Microsoft.UI.Xaml.Data.Bindable]
runtimeclass HostViewModel : Microsoft.UI.Xaml.Data.INotifyPropertyChanged
{
    HostViewModel();
    String NextButtonText;
}

這很重要

如果您使用 C++/WinRT,則 BindableAttribute 屬性可與 Windows 應用程式 SDK 搭配使用。 如果沒有該屬性,您必須實作 ICustomPropertyProviderICustomProperty 介面,才能使用 {Binding} 標記延伸模組。

根據預設, {Binding} 會假設您要系結至標記視窗的 DataContext 。 因此,將你的視窗的 DataContext 設定為類型為 HostViewModel 的繫結來源類別的實例。 下列範例顯示宣告繫結物件的標記。 它會使用稍早「繫結目標」一節中使用的相同 Button.Content 繫結目標,而且會繫結至 HostViewModel.NextButtonText 屬性。

<Window xmlns:viewmodel="using:DataBindingInDepth" ... >
    <Window.DataContext>
        <viewmodel:HostViewModel x:Name="viewModelInDataContext"/>
    </Window.DataContext>
    ...
    <Button Content="{Binding Path=NextButtonText}" ... />
</Window>
// MainWindow.xaml.cs
private void Button_Click(object sender, RoutedEventArgs e)
{
    viewModelInDataContext.NextButtonText = "Updated Next button text";
}

請注意指定的Path值。 視窗的 DataContext 會解譯此值,在此範例中,該值會設定為 的 HostViewModel實例。 路徑會 HostViewModel.NextButtonText 參考屬性。 您可以省略 Mode,因為單向的 {Binding} 預設值在這裡有效。

UI 元素的 DataContext 預設值是其父元素的繼承值。 您可以透過顯式設定 DataContext 來覆寫該預設值,且此設定預設會由子系繼承。 當您想要有多個使用相同來源的繫結時,明確設定 DataContext 元素會很有用。

繫結物件具有屬性 Source ,預設為宣告繫結之 UI 元素的 DataContext 。 您可以在繫結上明確設定 SourceRelativeSourceElementName 來覆寫此預設值 (如需詳細資料,請參閱 {Binding} )。

DataTemplate 內, DataContext 會自動設定為要範本化的資料物件。 下列範例可用作繫結到任何具有名為 TitleDescription 的字串屬性的類型集合的項目控制項的ItemTemplate

<DataTemplate x:Key="SimpleItemTemplate">
    <StackPanel Orientation="Vertical" Height="50">
      <TextBlock Text="{Binding Title}"/>
      <TextBlock Text="{Binding Description"/>
    </StackPanel>
  </DataTemplate>

備註

根據預設,當 TextBox 失去焦點時,TextBox.Text 的變更會傳送至雙向系結來源。 若要在每次使用者按鍵之後傳送變更,請在標記中的繫結上設定UpdateSourceTrigger為 。PropertyChanged 您也可以透過設定 UpdateSourceTriggerExplicit完全控制何時將變更傳送至來源。 然後,您可以處理文字方塊上的事件 (通常是 TextBox.TextChanged) ,在目標上呼叫 GetBindingExpression 以取得 BindingExpression 物件,最後呼叫 BindingExpression.UpdateSource 以程式設計方式更新資料來源。

Path 屬性支援各種語法選項,可系結至巢狀屬性、附加屬性,以及整數和字串索引子。 如需詳細資訊,請參閱 屬性路徑語法。 繫結至字串索引子可讓您取得系結至動態屬性的效果,而不需要實作 ICustomPropertyProvider ElementName 屬性對於元素對元素繫結很有用。 RelativeSource 屬性有數個用途,其中一個是作為 ControlTemplate 內範本系結的更強大替代方案。 如需其他設定,請參閱 {Binding} 標記延伸模組Binding 類別。

如果來源和目標不是相同的類型怎麼辦?

如果您想根據布林屬性的值控制 UI 元素的可見度,或是根據數值範圍或趨勢,為 UI 元素設置具有函數特徵的顏色,或者您想要在需要字串的 UI 元素屬性中顯示日期和/或時間值,那麼您需要將這些值從一種類型轉換為另一種類型。 在某些情況下,正確的解決方案是從繫結來源類的屬性中公開另一個正確類型的屬性,並將轉換邏輯封裝在那裡,使其可測試。 但是,當您面臨大量的來源和目標屬性,或是這些屬性的複雜組合時,該解決方案並不靈活或具可擴展性。 在這種情況下,您有幾個選擇:

  • 如果使用 {x:Bind} then 您可以直接綁定到函數來進行轉換
  • 或者,您可以指定值轉換器,這是設計用來執行轉換的物件

價值轉換器

這是一個值轉換器,適用於一次性或單向繫結,可將 DateTimestring 轉換為包含月份的值。 類別會實作 IValueConverter

public class DateToStringConverter : IValueConverter
{
    // Define the Convert method to convert a DateTime value to 
    // a month string.
    public object Convert(object value, Type targetType, 
        object parameter, string language)
    {
        // value is the data from the source object.
        DateTime thisDate = (DateTime)value;
        int monthNum = thisDate.Month;
        string month;
        switch (monthNum)
        {
            case 1:
                month = "January";
                break;
            case 2:
                month = "February";
                break;
            default:
                month = "Month not found";
                break;
        }
        // Return the value to pass to the target.
        return month;
    }

    // ConvertBack is not implemented for a OneWay binding.
    public object ConvertBack(object value, Type targetType, 
        object parameter, string language)
    {
        throw new NotImplementedException();
    }
}

以下是您在繫結物件標記中使用該值轉換器的方式。

<UserControl.Resources>
  <local:DateToStringConverter x:Key="Converter1"/>
</UserControl.Resources>
...
<TextBlock Grid.Column="0" 
  Text="{x:Bind ViewModel.Month, Converter={StaticResource Converter1}}"/>
<TextBlock Grid.Column="0" 
  Text="{Binding Month, Converter={StaticResource Converter1}}"/>

如果繫結已定義 Converter 參數,繫結引擎會呼叫 ConvertConvertBack 方法。 從來源傳遞資料時,繫結引擎會呼叫 Convert 傳回的資料,並將其傳遞至目標。 當資料從目標傳遞時 (針對雙向繫結) ,繫結引擎會呼叫 ConvertBack 傳回的資料,並將其傳遞至來源。

轉換器也有可選參數: ConverterLanguage,允許指定轉換中使用的語言,以及 ConverterParameter,允許傳遞轉換邏輯的參數。 如需使用轉換器參數的範例,請參閱 IValueConverter

備註

如果轉換中發生錯誤,請勿擲回例外狀況。 相反地,傳回 DependencyProperty.UnsetValue,這會停止資料傳輸。

若要顯示在無法解析繫結來源時要使用的預設值,請在標記中設定 FallbackValue 繫結物件的屬性。 這對於處理轉換和格式錯誤很有用。 繫結至非異質類型系結集合中所有物件上可能不存在的來源屬性也很有用。

如果您將文字控制項繫結至非字串的值,資料繫結引擎會將值轉換成字串。 如果值是參考類型,資料繫結引擎會呼叫 ICustomPropertyProvider.GetStringRepresentationIStringable.ToString (如果有的話) 來擷取字串值,否則會呼叫 Object.ToString。 不過請注意,繫結引擎會忽略任何 ToString 隱藏基底類別實作的實作。 子類別實作應該改為覆寫基底類別 ToString 方法。 同樣地,在原生語言中,所有 Managed 物件似乎都會實作 ICustomPropertyProviderIStringable。 不過,所有對 和 的GetStringRepresentation呼叫都會路由傳送至IStringable.ToString該方法或覆寫該方法,而且永遠不會路由傳送至Object.ToString隱藏基底類別實作的新實ToString作。

備註

Windows 社群工具組提供 BoolToVisibilityConverter。 轉換器會對應 trueVisible 列舉值 和 falseCollapsed 因此您可以將屬性繫結 Visibility 至布林值,而不需要建立轉換器。 若要使用轉換器,您的專案必須新增 CommunityToolkit.WinUI.Converters NuGet 套件。

{x:Bind} 中的函式繫結

{x:Bind} 讓繫結路徑中的最後一個步驟成為函式。 使用此功能來執行轉換,或建立相依於多個屬性的繫結。 如需詳細資訊,請參閱 x:Bind 中的函式

元素對元素繫結

您可以將一個 XAML 元素的屬性系結至另一個 XAML 元素的屬性。 以下是該繫結在標記中的外觀範例。

<TextBox x:Name="myTextBox" />
<TextBlock Text="{x:Bind myTextBox.Text, Mode=OneWay}" />

具有 {x:Bind} 的資源字典

{x:Bind} 標記延伸模組取決於程式碼產生,因此它需要程式碼後置檔案,其中包含呼叫 InitializeComponent (初始化產生程式碼) 的建構函式。 若要重複使用資源字典,請具現化其類型 (以便 InitializeComponent 呼叫) ,而不是參考其檔案名稱。 以下是如果您有現有資源字典並想要在其中使用的 {x:Bind} 範例。

<!-- TemplatesResourceDictionary.xaml -->
<ResourceDictionary
    x:Class="ExampleNamespace.TemplatesResourceDictionary"
    .....
    xmlns:examplenamespace="using:ExampleNamespace">
    
    <DataTemplate x:Key="EmployeeTemplate" x:DataType="examplenamespace:IEmployee">
        <Grid>
            <TextBlock Text="{x:Bind Name}"/>
        </Grid>
    </DataTemplate>
</ResourceDictionary>
// TemplatesResourceDictionary.xaml.cs
using Microsoft.UI.Xaml.Data;
 
namespace ExampleNamespace
{
    public partial class TemplatesResourceDictionary
    {
        public TemplatesResourceDictionary()
        {
            InitializeComponent();
        }
    }
}
<!-- MainWindow.xaml -->
<Window x:Class="ExampleNamespace.MainWindow"
    ....
    xmlns:examplenamespace="using:ExampleNamespace">

    <Window.Resources>
        <ResourceDictionary>
            .... 
            <ResourceDictionary.MergedDictionaries>
                <examplenamespace:TemplatesResourceDictionary/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Window.Resources>
</Window>

在可重複使用的樣式中混合 {x:Bind} 和 {Binding}

上一個範例示範如何在 {x:Bind} DataTemplates 中使用。 您也可以建立可重複使用的樣式,以結合{x:Bind}{Binding}兩個標記延伸模組。 當您想要使用 將 {x:Bind} 某些屬性繫結至編譯階段已知值,並使用 {Binding}將其他屬性繫結至執行階段 DataContext 值時,此組合很有用。

下列範例示範如何建立使用這兩種繫結方法的可重複使用的 Button 樣式:

TemplatesResourceDictionary.xaml

<!-- TemplatesResourceDictionary.xaml -->
<ResourceDictionary
    x:Class="ExampleNamespace.TemplatesResourceDictionary"
    .....
    xmlns:examplenamespace="using:ExampleNamespace">
    
    <!-- DataTemplate using x:Bind -->
    <DataTemplate x:Key="EmployeeTemplate" x:DataType="examplenamespace:IEmployee">
        <Grid>
            <TextBlock Text="{x:Bind Name}"/>
        </Grid>
    </DataTemplate>
    
    <!-- Style that mixes x:Bind and Binding -->
    <Style x:Key="CustomButtonStyle" TargetType="Button">
        <Setter Property="Background" Value="{Binding ButtonBackgroundBrush}"/>
        <Setter Property="Foreground" Value="{Binding ButtonForegroundBrush}"/>
        <Setter Property="FontSize" Value="16"/>
        <Setter Property="Margin" Value="4"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="Button">
                    <Border x:Name="RootBorder"
                            Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}"
                            CornerRadius="4">
                        <StackPanel Orientation="Horizontal" 
                                    HorizontalAlignment="Center"
                                    VerticalAlignment="Center">
                            <!-- x:Bind to a static property or page-level property -->
                            <Ellipse Width="8" Height="8" 
                                     Fill="{x:Bind DefaultIndicatorBrush}" 
                                     Margin="0,0,8,0"/>
                            <!-- Binding to DataContext -->
                            <ContentPresenter x:Name="ContentPresenter"
                                              Content="{TemplateBinding Content}"
                                              Foreground="{TemplateBinding Foreground}"
                                              FontSize="{TemplateBinding FontSize}"/>
                        </StackPanel>
                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup x:Name="CommonStates">
                                <VisualState x:Name="Normal"/>
                                <VisualState x:Name="PointerOver">
                                    <VisualState.Setters>
                                        <!-- Binding to DataContext for hover color -->
                                        <Setter Target="RootBorder.Background" 
                                                Value="{Binding ButtonHoverBrush}"/>
                                    </VisualState.Setters>
                                </VisualState>
                                <VisualState x:Name="Pressed">
                                    <VisualState.Setters>
                                        <!-- x:Bind to a compile-time known resource -->
                                        <Setter Target="RootBorder.Background" 
                                                Value="{x:Bind DefaultPressedBrush}"/>
                                    </VisualState.Setters>
                                </VisualState>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

TemplatesResourceDictionary.xaml.cs

// TemplatesResourceDictionary.xaml.cs
using Microsoft.UI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Media;
 
namespace ExampleNamespace
{
    public partial class TemplatesResourceDictionary
    {
        public TemplatesResourceDictionary()
        {
            InitializeComponent();
        }
        
        // Properties for x:Bind - these are compile-time bound
        public SolidColorBrush DefaultIndicatorBrush { get; } = 
            new SolidColorBrush(Colors.Green);
            
        public SolidColorBrush DefaultPressedBrush { get; } = 
            new SolidColorBrush(Colors.DarkGray);
    }
}

在 MainWindow.xaml 中搭配提供執行階段值的 ViewModel 使用:

<!-- MainWindow.xaml -->
<Window x:Class="ExampleNamespace.MainWindow"
    ....
    xmlns:examplenamespace="using:ExampleNamespace">

    <Window.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <examplenamespace:TemplatesResourceDictionary/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Window.Resources>

    <Grid>
        <Grid.DataContext>
            <examplenamespace:ButtonThemeViewModel/>
        </Grid.DataContext>
        
        <StackPanel Margin="20">
            <!-- These buttons use the mixed binding style -->
            <Button Content="Save" Style="{StaticResource CustomButtonStyle}"/>
            <Button Content="Cancel" Style="{StaticResource CustomButtonStyle}"/>
        </StackPanel>
    </Grid>
</Window>

ButtonThemeViewModel.cs (提供執行階段繫結值的 DataContext):

using System.ComponentModel;
using Microsoft.UI;
using Microsoft.UI.Xaml.Media;

namespace ExampleNamespace
{
    public class ButtonThemeViewModel : INotifyPropertyChanged
    {
        private SolidColorBrush _buttonBackgroundBrush = new SolidColorBrush(Colors.LightBlue);
        private SolidColorBrush _buttonForegroundBrush = new SolidColorBrush(Colors.DarkBlue);
        private SolidColorBrush _buttonHoverBrush = new SolidColorBrush(Colors.LightCyan);

        public SolidColorBrush ButtonBackgroundBrush
        {
            get => _buttonBackgroundBrush;
            set
            {
                _buttonBackgroundBrush = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ButtonBackgroundBrush)));
            }
        }

        public SolidColorBrush ButtonForegroundBrush
        {
            get => _buttonForegroundBrush;
            set
            {
                _buttonForegroundBrush = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ButtonForegroundBrush)));
            }
        }

        public SolidColorBrush ButtonHoverBrush
        {
            get => _buttonHoverBrush;
            set
            {
                _buttonHoverBrush = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ButtonHoverBrush)));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }
}

在此範例中:

  • {Binding} 用於相依於 DataContext (ButtonBackgroundBrush、ButtonForegroundBrush、ButtonHoverBrush) 的屬性
  • {x:Bind} 用於編譯階段已知且屬於 ResourceDictionary 本身的屬性 (DefaultIndicatorBrush、DefaultPressedBrush)
  • 該樣式是可重複使用的,您可以將其應用於任何按鈕
  • 執行階段主題可透過 DataContext 進行,同時仍受益於靜態元素的 {x:Bind} 效能

事件繫結和 ICommand

{x:Bind} 支援稱為事件繫結的功能。 透過這項功能,您可以使用繫結來指定事件的處理常式。 此功能是處理事件的附加選項,除了使用程式碼後置檔案中的方法處理事件之外。 假設您的MainWindow類別中有一個ListViewDoubleTapped事件處理器。

public sealed partial class MainWindow : Window
{
    ...
    public void ListViewDoubleTapped()
    {
        // Handle double-tapped logic
    }
}

您可以將 ListView 的 DoubleTapped 事件繫結至 MainWindow 中的方法,如下所示。

<ListView DoubleTapped="{x:Bind ListViewDoubleTapped}" />

您無法使用此技術處理事件的重載方法。 此外,如果處理事件的方法具有參數,則所有參數都必須分別從所有事件參數的類型中指派。 在此情況下, ListViewDoubleTapped 不會重載,也沒有參數 (但即使採用兩個 object 參數,它仍然有效)。

事件繫結技術類似於實現和使用命令。 命令是傳回實作 ICommand 介面之物件的屬性。 {x:Bind}{Binding} 都適用於命令。 因此,您不需要多次實作命令模式,您可以使用 DelegateCommand 您在 QuizGame UWP 範例中找到的協助程式類別 (在 “Common” 資料夾中) 。

繫結至資料夾或檔案的集合

您可以使用 Windows.Storage 命名空間中的 API,擷取封裝的 Windows 應用程式 SDK 應用程式中的資料夾和檔案資料。 不過,各種 GetFilesAsyncGetFoldersAsyncGetItemsAsync 方法不會傳回適合系結至清單控制項的值。 相反地,您必須系結至 FileInformationFactory 類別的 GetVirtualizedFilesVectorGetVirtualizedFoldersVectorGetVirtualizedItemsVector 方法的傳回值。 下列來自 StorageDataSource 和 GetVirtualizedFilesVector UWP 範例 的程式碼範例顯示一般使用模式。 請記得在應用程式套件資訊清單中宣告 picturesLibrary 功能,並確認 Pictures 文件庫資料夾中有圖片。

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    var library = Windows.Storage.KnownFolders.PicturesLibrary;
    var queryOptions = new Windows.Storage.Search.QueryOptions();
    queryOptions.FolderDepth = Windows.Storage.Search.FolderDepth.Deep;
    queryOptions.IndexerOption = Windows.Storage.Search.IndexerOption.UseIndexerWhenAvailable;

    var fileQuery = library.CreateFileQueryWithOptions(queryOptions);

    var fif = new Windows.Storage.BulkAccess.FileInformationFactory(
        fileQuery,
        Windows.Storage.FileProperties.ThumbnailMode.PicturesView,
        190,
        Windows.Storage.FileProperties.ThumbnailOptions.UseCurrentScale,
        false
        );

    var dataSource = fif.GetVirtualizedFilesVector();
    this.PicturesListView.ItemsSource = dataSource;
}

您通常會使用此方法來建立檔案和資料夾資訊的唯讀檢視。 您可以建立檔案和資料夾屬性的雙向繫結,例如讓使用者在音樂檢視中對歌曲進行評分。 不過,在您呼叫適當的 SavePropertiesAsync 方法 (例如 MusicProperties.SavePropertiesAsync) 之前,不會保留任何變更。 當項目失去焦點時,您應該提交變更,因為此動作會觸發重設選擇。

請注意,使用此技術的雙向繫結僅適用於索引位置,例如 音樂。 您可以呼叫 FolderInformation.GetIndexedStateAsync 方法來判斷位置是否已編製索引。

另請注意,虛擬化向量可以在填入某些專案的值之前傳回 null 。 例如,您應該先檢查 , null 然後才能使用繫結至虛擬向量之清單控制項的 SelectedItem 值,或改用 SelectedIndex

繫結至依索引鍵分組的資料

如果您採用平面的項目集合(例如由 BookSku 類別表示的書籍),並使用通用屬性作為索引鍵(例如 BookSku.AuthorName 屬性)來將項目分組,則結果稱為分組資料。 當您將資料分組時,它不再是平面集合。 分組資料是群組物件的集合,其中每個群組物件都有:

  • 一個鍵,以及
  • 屬性符合該索引鍵的專案集合。

再次以書籍為例,依作者姓名將書籍分組的結果會產生作者姓名群組的集合,其中每個群組具有:

  • 鍵,即作者名稱,以及
  • 其屬性符合群組索引鍵的BookSku物件集合AuthorName

一般而言,若要顯示集合,您可以將專案控制項的 ItemsSource (例如 ListViewGridView) 直接系結至傳回集合的屬性。 如果這是一個平坦的項目集合,那麼你不需要做任何特別的事情。 但是,如果它是群組物件的集合 (,就像繫結至群組資料時一樣),則您需要稱為 CollectionViewSource 的中繼物件的服務,該物件位於專案控制項和繫結來源之間。 您可以將 繫 CollectionViewSource 結至傳回分組資料的屬性,並將 items 控制項繫結至 CollectionViewSource。 a CollectionViewSource 的額外增值是它會追蹤目前的項目,因此您可以將多個項目控制項都綁定到相同的 CollectionViewSource來保持同步。 您也可以透過 CollectionViewSource.View 屬性所傳回物件的 ICollectionView.CurrentItem 屬性,以程式設計方式存取目前的專案。

若要啟用 CollectionViewSource 的分組工具,請將 IsSourceGrouped 設定為 true。 您是否也需要設定 ItemsPath 屬性,取決於您撰寫群組物件的確切方式。 有兩種方式可以編寫群組物件:「is-a-group」模式和「has-a-group」模式。 在「is-a-group」模式中,群組物件衍生自集合類型(例如, List<T>),因此群組物件本身本身就是專案群組。 使用此模式,您不需要設定 ItemsPath。 在「has-a-group」模式中,群組物件具有一或多個集合類型的屬性(例如 List<T>),因此群組「具有」屬性形式的專案群組(或數個屬性形式的數組專案)。 使用此模式時,您必須設定 ItemsPath 為包含專案群組的屬性名稱。

下列範例說明「has-a-group」模式。 視窗類別具有名為 DataContext 的屬性,它會傳回檢視模型的實例。 CollectionViewSource 系結至Authors檢視模型的屬性 (Authors是群組物件的集合),也指定它是Author.BookSkus包含群組專案的屬性。 最後, GridView 系結至 CollectionViewSource,並定義其群組樣式,讓它可以轉譯群組中的專案。

<Window.Resources>
    <CollectionViewSource
    x:Name="AuthorHasACollectionOfBookSku"
    Source="{x:Bind ViewModel.Authors}"
    IsSourceGrouped="true"
    ItemsPath="BookSkus"/>
</Window.Resources>
...
<GridView
ItemsSource="{x:Bind AuthorHasACollectionOfBookSku}" ...>
    <GridView.GroupStyle>
        <GroupStyle
            HeaderTemplate="{StaticResource AuthorGroupHeaderTemplateWide}" ... />
    </GridView.GroupStyle>
</GridView>

您可以使用以下兩種方式之一實作「is-a-group」模式。 一種方法是編寫您自己的小組課程。 衍生類別 List<T> (其中 T 是專案的類型)。 例如: public class Author : List<BookSku> 。 第二種方式是使用 LINQ 運算式,從 BookSku 專案的類似屬性值動態建立群組物件 (和群組類別) 。 這種方法(僅維護項目的平面清單並將它們動態分組在一起)是從雲端服務存取資料的應用程式的典型做法。 您可以靈活地按作者或類型(例如)對書籍進行分組,而無需特殊的組類別,例如 作者流派

下列範例說明使用 LINQ 的「is-a-group」模式。 這次我們按類型對書籍進行分組,並在組標題中顯示類型名稱。 此群組由參考群組 Key 值的 “Key” 屬性路徑指示。

using System.Linq;
...
private IOrderedEnumerable<IGrouping<string, BookSku>> genres;

public IOrderedEnumerable<IGrouping<string, BookSku>> Genres
{
    get
    {
        if (genres == null)
        {
            genres = from book in bookSkus
                     group book by book.genre into grp
                     orderby grp.Key
                     select grp;
        }
        return genres;
    }
}

請記住,當將 {x:Bind} 與資料範本搭配使用時,您需要透過設定 x:DataType 值來指示要綁定的類型。 如果類型是泛型,則您無法在標記中表達,因此您需要在群組樣式標頭範本中使用 {Binding} 來代替。

    <Grid.Resources>
        <CollectionViewSource x:Name="GenreIsACollectionOfBookSku"
        Source="{x:Bind Genres}"
        IsSourceGrouped="true"/>
    </Grid.Resources>
    <GridView ItemsSource="{x:Bind GenreIsACollectionOfBookSku}">
        <GridView.ItemTemplate x:DataType="local:BookTemplate">
            <DataTemplate>
                <TextBlock Text="{x:Bind Title}"/>
            </DataTemplate>
        </GridView.ItemTemplate>
        <GridView.GroupStyle>
            <GroupStyle>
                <GroupStyle.HeaderTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding Key}"/>
                    </DataTemplate>
                </GroupStyle.HeaderTemplate>
            </GroupStyle>
        </GridView.GroupStyle>
    </GridView>

SemanticZoom 控制項是使用者檢視和流覽分組資料的絕佳方式。 Bookstore2 UWP 範例應用程式說明如何使用 SemanticZoom. 在該應用程式中,您可以檢視依作者分組的書籍清單(放大視圖),也可以縮小以查看作者跳躍清單(縮小視圖)。 跳轉列表提供的導航速度比滾動瀏覽書籍列表要快得多。 放大和縮小的視圖實際上 ListView 是或 GridView 綁定到相同 CollectionViewSource的控制項。

SemanticZoom 的圖解

當您繫結至階層式資料 (例如類別內的子類別) 時,您可以選擇在 UI 中顯示階層層級,其中包含一系列專案控制項。 一個項目控制項中的選取範圍會決定後續項目控制項的內容。 您可以藉由將每個清單繫結至自己的 CollectionViewSource ,並將實例繫結 CollectionViewSource 在鏈結中,以保持清單同步處理。 此設定稱為主要/詳細資料 (或清單/詳細資料) 檢視。 如需詳細資訊,請參閱 如何系結至階層式資料並建立主要/詳細資料檢視

診斷及除錯資料繫結問題

繫結標記包含屬性名稱 (對於 C#,有時還包含欄位和方法)。 因此,當您重新命名屬性時,您也需要變更參考它的任何繫結。 如果您忘記這樣做,就會建立資料繫結錯誤,而且您的應用程式無法編譯或無法正確執行。

{x:Bind}{Binding} 建立的繫結物件在功能上大致相同。 但 {x:Bind} 具有繫結來源的類型資訊,而且會在編譯階段產生原始程式碼。 使用 {x:Bind},您可以獲得與其餘程式碼相同的問題偵測。 偵測過程包括對繫結運算式的編譯時期驗證,以及透過在生成為頁面部分類別的原始程式碼中設定中斷點來進行偵錯。 您可以在資料夾中的 obj 檔案中找到這些類別,名稱類似於 (對於 C#) <view name>.g.cs)。 如果您有繫結問題,請在 Microsoft Visual Studio 偵錯工具中開啟 [中斷未處理的例外狀況 ]。 偵錯工具會在該點中斷執行,然後您可以偵錯發生的問題。 所 {x:Bind} 產生的程式碼會針對系結來源節點圖形的每個部分遵循相同的模式,而且您可以使用 [呼叫堆疊 ] 視窗中的資訊來協助判斷導致問題的呼叫順序。

{Binding} 沒有繫結來源的類型資訊。 但是,當您在附加偵錯工具的情況下執行應用程式時,任何繫結錯誤都會出現在 Visual Studio 的 [輸出 ] 和 [XAML 系結失敗] 視窗中。 如需在 Visual Studio 中偵錯繫結錯誤的詳細資訊,請參閱 XAML 資料繫結診斷

在程式碼中建立繫結

備註

本節僅適用於 {Binding},因為您無法在程式碼中建立 {x:Bind} 繫結。 不過,您可以使用 {x:Bind} 來取得一些相同的優點,這可讓您註冊任何相依性屬性的變更通知。

您也可以使用程序程式碼 (而非 XAML) 將 UI 元素連線到資料。 若要這樣做,請建立新的 Binding 物件、設定適當的屬性,然後呼叫 FrameworkElement.SetBindingBindingOperations.SetBinding。 當您想要在執行階段選擇繫結屬性值,或在多個控制項之間共用單一繫結時,以程式設計方式建立繫結很有用。 不過,您無法在呼叫 SetBinding之後變更繫結屬性值。

下列範例示範如何在程式碼中實作繫結。

<TextBox x:Name="MyTextBox" Text="Text"/>
// Create an instance of the MyColors class 
// that implements INotifyPropertyChanged.
var textcolor = new MyColors();

// Brush1 is set to be a SolidColorBrush with the value Red.
textcolor.Brush1 = new SolidColorBrush(Colors.Red);

// Set the DataContext of the TextBox MyTextBox.
MyTextBox.DataContext = textcolor;

// Create the binding and associate it with the text box.
var binding = new Binding { Path = new PropertyPath("Brush1") };
MyTextBox.SetBinding(TextBox.ForegroundProperty, binding);

{x:Bind} 和 {Binding} 功能比較

特徵 / 功能 {x:Bind} 與 {Binding} 註釋
路徑是預設屬性 {x:Bind a.b.c}
-
{Binding a.b.c}
Path 屬性 {x:Bind Path=a.b.c}
-
{Binding Path=a.b.c}
x:Bind中, Path 預設以 Window 為根目錄,而不是 DataContext。
Indexer {x:Bind Groups[2].Title}
-
{Binding Groups[2].Title}
繫結至集合中的指定專案。 僅支援整數型索引。
附加屬性 {x:Bind Button22.(Grid.Row)}
-
{Binding Button22.(Grid.Row)}
附加屬性是使用括弧指定。 如果未在 XAML 命名空間中宣告屬性,請以 XML 命名空間為前置詞,該命名空間應該對應至文件頭頭的程式碼命名空間。
選角 {x:Bind groups[0].(data:SampleDataGroup.Title)}
-
不需要 {Binding}
使用括號指定轉換。 如果未在 XAML 命名空間中宣告屬性,請以 XML 命名空間為前置詞,該命名空間應該對應至文件頭頭的程式碼命名空間。
轉換器 {x:Bind IsShown, Converter={StaticResource BoolToVisibility}}
-
{Binding IsShown, Converter={StaticResource BoolToVisibility}}
在 Window、Control、ResourceDictionary 的根目錄或 App.xaml 中宣告轉換器。
轉換器參數、轉換器語言 {x:Bind IsShown, Converter={StaticResource BoolToVisibility}, ConverterParameter=One, ConverterLanguage=fr-fr}
-
{Binding IsShown, Converter={StaticResource BoolToVisibility}, ConverterParameter=One, ConverterLanguage=fr-fr}
在 Window、Control、ResourceDictionary 的根目錄或 App.xaml 中宣告轉換器。
TargetNull值 {x:Bind Name, TargetNullValue=0}
-
{Binding Name, TargetNullValue=0}
當繫結運算式的分葉為 null 時使用。 對字串值使用單引號。
後援值 {x:Bind Name, FallbackValue='empty'}
-
{Binding Name, FallbackValue='empty'}
當繫結路徑的任何部分 (分葉除外) 為 Null 時使用。
元素名稱 {x:Bind slider1.Value}
-
{Binding Value, ElementName=slider1}
使用 {x:Bind} 繫結到欄位。Path 預設是以視窗為根,因此您可以透過其欄位存取任何具名元素。
RelativeSource:自我 <Rectangle x:Name="rect1" Width="200" Height="{x:Bind rect1.Width}" ... />
-
<Rectangle Width="200" Height="{Binding Width, RelativeSource={RelativeSource Self}}" ... />
使用 ,為 {x:Bind}元素命名,並在 中使用 Path其名稱。
RelativeSource:TemplatedParent 不需要 {x:Bind}
-
{Binding <path>, RelativeSource={RelativeSource TemplatedParent}}
搭配 {x:Bind}TargetType 開啟 ControlTemplate 表示繫結至範本父系。 對於 {Binding},一般範本繫結可用於大部分用途的控制項範本中。 但請在需要使用轉換器或雙向綁定的地方使用 TemplatedParent
來源 不需要 {x:Bind}
-
<ListView ItemsSource="{Binding Orders, Source={StaticResource MyData}}"/>
因為 {x:Bind} 你可以直接使用命名元素,使用屬性,或者靜態路徑。
Mode {x:Bind Name, Mode=OneWay}
-
{Binding Name, Mode=TwoWay}
Mode 可以是 OneTimeOneWayTwoWay{x:Bind} 預設為 OneTime; {Binding} 預設為 OneWay
UpdateSource觸發程式 {x:Bind Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}
-
{Binding UpdateSourceTrigger=PropertyChanged}
UpdateSourceTrigger 可以是 DefaultLostFocusPropertyChanged{x:Bind} 不支援 UpdateSourceTrigger=Explicit{x:Bind} PropertyChanged對所有情況使用 behavior,但 TextBox.Text除外,其中它使用LostFocus行為。

另請參閱