資料繫結簡介

已完成

Tech logo of U W P and W P F.

在此課程中,您了解如何建立會顯示目前時間的應用程式。 此課程會介紹資料繫結的基本概念,將資料從程式碼傳送到您應用程式的 UI,然後重新整理它以更新 UI 上的時鐘顯示。 此課程構成後續課程中更複雜資料繫結工作的基礎。 那麼,現在就開始吧!

Tech logo of U W P and W P F. W P F appears dimmed.

1. 建立專案

如果尚未執行,請開啟 Visual Studio。 使用 [空白應用程式 (通用 Windows)] 範本建立新的 C# Windows 通用專案。 將它命名為 DatabindingSample。 這是您在整個 UI 和資料課程模組中使用的專案。

Screenshot of the Visual Studio Create a new project dialog box.

當您按一下 [確定] 時,Visual Studio 會提示您輸入目標和最低 Windows 版本。 這只是練習專案,且您並不準備將它部署到執行舊版 Windows 的電腦上。 因此針對最低和目標版本,您可以選取最新的 Windows 版本,然後按一下 [確定]。

2. 新增用於顯示時鐘的 TextBlock

將專案完整初始化並載入之後,請在 [方案總管] 中按兩下 MainPage.xaml 來開啟它。

提示

如果您的螢幕空間不足,請使用左上角的下拉式清單來切換編輯器,以模擬較低解析度的螢幕。 針對此課程模組,我們建議使用 [13.3" 桌面 (1280x720) 縮放比例 100%],但請選擇您感覺最舒適的選項。

Grid 元素的開頭和結尾標籤之間,新增以下這一行。

<TextBlock HorizontalAlignment="Right" 
           Margin="10" 
           Text="{x:Bind CurrentTime}" />

這會在視窗的右上方建立新的 TextBlock,其與邊緣之間具有 10 單位的邊界。 接下來,讓我們處理 TextBlockText

Text={x:Bind CurrentTime} 部分是您第一個遇到資料繫結的地方。 x:Bind 是 XAML 標記延伸,其會與您應用程式的其餘部分一起編譯成 C# 程式碼。 在這裡,它會將 TextBlockText 屬性連接至 CurrentTime 屬性。 如果您現在嘗試編譯專案,就會收到下列錯誤訊息:

XamlCompiler 錯誤 WMC1110:無效的繫結路徑「CurrentTime」:「MainPage」類型中找不到「CurrentTime」屬性

這表示編譯器缺少來自 MainPageCurrentTime 屬性。 在我們建立該屬性之後,內容即會顯示在右上角的 TextBlock 中。

注意

UWP 也支援較舊的資料繫結方法,看起來就像這樣:Text={Bind CurrentTime}。 這種較舊的方法與 {x:Bind} 的運作方式有些不同。 最明顯的就是,如果有錯字,它並不會提供編譯時期錯誤。 在此課程模組中,我們將焦點全部放在新的 {x:Bind} 繫結方式,其確實會提供編譯時期錯誤檢查。 不過,與 {Bind} 相比,{x:Bind} 較不成熟,且能收到新功能,這也是為何我們會選擇搭配最新的 Windows 版本來建立專案。

3. 建立 CurrentTime 屬性

請開啟 MainPage.xaml.cs,然後將下列屬性定義新增至 MainPage 類別。

public string CurrentTime => DateTime.Now.ToLongTimeString();

如果您不熟悉上述語法,它被稱為「運算式主體成員」。 它是在 C# 6.0 中導入,並且為下列項目的速記:

public string CurrentTime 
{
    get { return DateTime.Now.ToLongTimeString(); }
}

4. 執行應用程式

如果您現在啟動應用程式 (使用 F5 鍵或功能表中的 [偵錯] / [開始偵錯] 命令),應用程式就會進行編譯並執行。 更好的是,它似乎可以運作! 目前的時間顯示在右上角。

Screenshot of the running app with the clock.

不過,因為時鐘不會更新,所以有些內容不正確。 它停在應用程式一開始啟動時的時間。 應用程式怎麼會知道何時該重新整理 TextBlock 中的值? 我們必須告訴 UWP 執行階段每秒更新它一次。

5. 指定繫結模式

{x:Bind} 已針對效能進行高度最佳化。 意謂著它們不會執行開發人員未明確要求的任何操作。 因此,{x:Bind} 繫結預設只會評估繫結來源 (在我們的案例中為 CurrentTime 屬性) 一次。 這種繫結稱為 OneTime 繫結。 如果我們希望 UWP 架構持續更新 UI,就必須明確指定另一種繫結模式:OneWayTwoWay

TwoWay 繫結模式指出我們 C# 程式碼 (邏輯) 與 UI 之間具有雙向繫結。 當我們繫結至使用者可以操作的控制項時,這種類型的繫結將會十分有用。 但對於 TextBlock 而言,OneWay 繫結是最好的方式,因為資料變更只會源自程式碼,而永遠不會源自 UI。

若要為我們的 TextBlock 指定 OneWay 繫結模式,請將 {x:Bind CurrentTime} 變更為 {x:Bind CurrentTime, Mode=OneWay}Grid 內的整個 TextBlock 標籤現在看起來應該像此標記。

<TextBlock HorizontalAlignment="Right" 
           Margin="10" 
           Text="{x:Bind CurrentTime, Mode=OneWay}" />

此繫結會指示 UWP 執行階段建置必要的基礎結構,以追蹤對 CurrentTime 屬性進行的變更,然後將其反映在 TextBlockText 中。 這個額外的基礎結構會佔用少量的記憶體和 CPU 週期,這就是為何它不是預設值的原因。

如果您現在執行應用程式,時鐘仍然不會更新。 我們必須通知系統 CurrentTime 屬性已經變更。

6. 實作 INotifyPropertyChanged 介面

此通知會透過 INotifyPropertyChanged 介面進行。 這是個具有單一事件的簡單介面。

public interface INotifyPropertyChanged
{
    event PropertyChangedEventHandler PropertyChanged;
}

任何以簡單 C# 屬性作為資料繫結來源的類別都必須實作 INotifyPropertyChanged 介面。 該類別必須在應該更新 UI 時觸發 PropertyChanged 事件。 讓我們開始將該介面新增至 MainPage 類別宣告。

public sealed partial class MainPage : Page, INotifyPropertyChanged

我們也必須將 PropertyChanged 事件新增至類別來實作該介面。

public event PropertyChangedEventHandler PropertyChanged;

7. 每秒叫用 PropertyChanged 事件

剩下要做的,就是每當我們想要更新時鐘 (也就是每秒) 時,都叫用 PropertyChanged 事件。 若要開始,讓我們在 MainPage 類別中宣告 DispatcherTimer 物件。

private DispatcherTimer _timer;

現在,讓我們在建構函式 (在 InitializeComponent 呼叫之後) 中設定它,使它每秒都引發。

_timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };

_timer.Tick += (sender, o) =>
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentTime)));

_timer.Start();

上面的第一行會建立一個間隔為一秒的計時器,而最後一行則會啟動它。 讓我們探討一下當計時器引發時 (在第二行),會發生什麼事。

PropertyChanged?.Invoke 是用於檢查事件是否為 Null 的速記,如果不是 Null,就會叫用它。 像大多數事件一樣,第一個引數是傳送者 (this)。 PropertyChanged 事件的第二個引數是新建立的 PropertyChangedEventArgs 物件,其建構函式會預期有字串作為屬性名稱。 因此,PropertyChanged 事件的訂閱者 (在此案例中為 UWP 系統) 會接收所更新屬性的名稱,並可相應地採取動作。

提示

請不要使用字串常值 (例如 "CurrentTime") 作為屬性名稱。 使用字串本身容易有錯字,這在 UI 不是更新對象時可能導致產生難以偵錯的問題。 此外,在不更新字串常數的情況下,無害的屬性重新命名也可能導入錯誤。 最佳做法是一律使用 nameof 運算式,其不受錯字的影響,也能追蹤重新命名。

整個 MainPage.xaml.cs 應該看起來如下:

namespace DatabindingSample
{
    public sealed partial class MainPage : Page, INotifyPropertyChanged
    {
        private DispatcherTimer _timer;

        public MainPage()
        {
            this.InitializeComponent();
            _timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };

            _timer.Tick += (sender, o) =>
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentTime)));

            _timer.Start();
        }

        public string CurrentTime => DateTime.Now.ToLongTimeString();
        public event PropertyChangedEventHandler PropertyChanged;
    }
}

8. 執行應用程式

如果您現在執行應用程式,就會更新時鐘。 恭喜!您已建立您的第一個資料繫結!

9. 摘要

您現在已知道如何使用 {x:Bind} 來建立能快速且自動地將資料從程式碼傳送到 UWP 應用程式 UI 的方式。 此技巧會在編譯時期檢查。 您也已熟悉 INotifyPropertyChanged 介面。 此介面可讓您的應用程式在繫結資料的屬性已變更且應該更新 UI 時,通知 UWP 架構。

Tech logo of U W P and W P F. U W P appears dimmed.

1. 建立專案

如果尚未執行,請開啟 Visual Studio。 使用 WPF 應用程式 範本建立新的 C# WPF 專案。 將它命名為 DatabindingSampleWPF,然後選取 [確定]。 這是您在整個 UI 和資料課程模組中使用的專案。

Screenshot of the Visual Studio Create a new WPF project dialog box.

2. 建立 Clock 類別

由於我們的工作是要顯示目前的時間,因此請先建立 Clock 類別。 在 [方案總管] 中,以滑鼠右鍵按一下 DatabindingSampleWPF 專案、選取 [新增] / [類別],然後輸入 Clock 作為類別的名稱。

將下列程式碼複製到新建立的檔案:

using System;

namespace DatabindingSampleWPF
{
    public class Clock
    {
        public string CurrentTime => DateTime.Now.ToLongTimeString();
    }
}

如果您不熟悉上述用於 CurrentTime 屬性的語法,它被稱為「運算式主體成員」。 它是在 C# 6.0 中導入,並且為下列項目的速記:

public string CurrentTime 
{
    get { return DateTime.Now.ToLongTimeString(); }
}

如您所見,Clock 類別目前只擁有簡單的 string 屬性,其能以完整時間格式傳回目前的時間。 下一步就是在應用程式本身內顯示時間。

3. 新增用於顯示時鐘的 TextBlock

若在 Visual Studio 中已開啟 MainWindow.xaml,請選取其索引標籤。若無開啟,您可以在方案總管中按兩下以開啟。

Grid 元素的開頭和結尾標籤之間,新增以下這一行。

<TextBlock HorizontalAlignment="Right" 
           VerticalAlignment="Top"
           Margin="10" 
           Text="{Binding CurrentTime}">
    <TextBlock.DataContext>
        <local:Clock/>
    </TextBlock.DataContext>
</TextBlock>

此標記會在視窗的右上方建立一個新的 TextBlock,其與邊緣之間具有 10 單位的邊界。

Text="{Binding CurrentTime}" 部分是您第一個遇到資料繫結的地方。 {Binding} 是 XAML 標記延伸。 在這裡,它會將 TextBlockText 屬性連接至 CurrentTime 屬性,但 CurrentTime 屬性又是屬於哪個物件?

資料繫結所參考的物件會在 TextBlockDataContext 中具現化。 因此,上述 XAML 程式碼不僅會建立 TextBlock 控制項,也會具現化 Clock 物件。 此外,該程式碼會將 TextBlockText 屬性繫結至所建立之 Clock 物件的 CurrentTime 屬性。 CurrentTime 屬性稱為繫結的來源,而 Text 屬性則稱為繫結的目的地

4. 執行應用程式

如果您現在啟動應用程式 (使用 F5 鍵或功能表中的 [偵錯] / [開始偵錯] 命令),應用程式就會進行編譯並執行。 更好的是,它似乎可以運作! 目前的時間顯示在右上角。

Screenshot of the running app with the clock.

不過,因為時鐘不會更新,所以有些內容不正確。 它停在應用程式一開始啟動時的時間。 應用程式怎麼會知道何時該重新整理 TextBlock 中的值? 我們必須告訴 WPF 執行階段每秒更新它一次。

換句話說,我們必須通知系統 CurrentTime 屬性已經變更。

5. 實作 INotifyPropertyChanged 介面

此通知會透過 INotifyPropertyChanged 介面進行。 這是個具有單一事件的簡單介面。

public interface INotifyPropertyChanged
{
    event PropertyChangedEventHandler PropertyChanged;
}

任何以簡單 C# 屬性作為資料繫結來源的類別都必須實作 INotifyPropertyChanged 介面。 該類別必須在應該更新 UI 時觸發 PropertyChanged 事件。 讓我們開始將該介面新增至 Clock 類別宣告。

using System.ComponentModel;

public class Clock : INotifyPropertyChanged
{

我們也必須將 PropertyChanged 事件新增至類別來實作該介面。

public event PropertyChangedEventHandler? PropertyChanged;

6. 每秒叫用 PropertyChanged 事件

剩下要做的,就是每當我們想要更新時鐘 (也就是每秒) 時,都叫用 PropertyChanged 事件。 若要開始,讓我們將 System.Windows.Threading 命名空間新增至 using,然後在 Clock 類別中宣告 DispatcherTimer 物件。

private DispatcherTimer _timer;

現在,在建構函式中設定它,使它每秒都引發。

public Clock()
{
    _timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };

    _timer.Tick += (sender, o) => PropertyChanged?.Invoke(this,
            new PropertyChangedEventArgs(nameof(CurrentTime)));

    _timer.Start();
}

建構函式中第一行會建立一個間隔為一秒的計時器,而最後一行則會啟動它。 讓我們探討一下當計時器引發時 (在第二行),會發生什麼事。

PropertyChanged?.Invoke 是用於檢查事件是否為 Null 的速記,如果不是 Null,就會叫用它。 像大多數事件一樣,第一個引數是傳送者 (this)。 PropertyChanged 事件的第二個引數是新建立的 PropertyChangedEventArgs 物件,其建構函式會預期有字串作為屬性名稱。 因此,PropertyChanged 事件的訂閱者 (在此案例中為 WPF 系統) 會接收所更新屬性的名稱,並可相應地採取動作。

提示

請不要使用字串常值 (例如 "CurrentTime") 作為屬性名稱。 使用字串本身容易有錯字,這在 UI 不是更新對象時可能導致產生難以偵錯的問題。 此外,在不更新字串常數的情況下,無害的屬性重新命名也可能導入錯誤。 最佳做法是一律使用 nameof 運算式,其不受錯字的影響,也能追蹤重新命名作業。

整個 Clock.cs 看起來應該如下所示:

namespace DatabindingSampleWPF
{
    using System;
    using System.ComponentModel;
    using System.Windows.Threading;

    public class Clock : INotifyPropertyChanged
    {
        private DispatcherTimer _timer;

        public string CurrentTime => DateTime.Now.ToLongTimeString();

        public event PropertyChangedEventHandler PropertyChanged;

        public Clock()
        {
            // setup _timer to refresh CurrentTime
            _timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
            _timer.Tick += (sender, o) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentTime)));
            _timer.Start();
        }
    }
}

7. 執行應用程式

如果您現在執行應用程式,就會更新時鐘。 您已建立您的第一個資料繫結!

8. 摘要

您現在已知道如何使用 {Binding} 來建立能快速且自動地將資料從程式碼傳送到 WPF 應用程式 UI 的方式。 您也已熟悉 INotifyPropertyChanged 介面。 此介面可讓您的應用程式在繫結資料的屬性已變更且應該更新 UI 時,通知 WPF 架構。