XAML 控制項;繫結至 C++/WinRT 屬性

一個能有效綁定於 XAML 控制項的屬性稱為 可觀察 性質。 這個想法基於一種稱為 觀察者模式的軟體設計模式。 本主題說明如何在 C++/WinRT 中實作可觀察屬性,以及如何將 XAML 控制項綁定到這些屬性(背景資訊請參見 資料綁定)。

這很重要

關於支持你如何使用 C++/WinRT 撰寫執行時類別的基本概念與術語,請參見 Consume API with C++/WinRT 及 Author API with C++/WinRT。

對於某個屬性而言,可觀察是什麼意思?

假設一個名為 BookSku 的執行時類別有一個名為 Title 的屬性。 如果 BookSku 在 Title 值改變時觸發 INotifyPropertyChanged::P ropertyChanged 事件,那就表示 Title 是一個可觀察的屬性。 BookSku 的行為(是否會引發事件)決定了其哪些屬性(如果有的話)是可觀察到的。

XAML 文字元素或控制項可以綁定並處理這些事件。 此類元素或控制項會透過取得更新後的值來處理事件,然後自行更新以顯示新值。

Note

關於安裝及使用 C++/WinRT Visual Studio 擴充套件(VSIX)及 NuGet 套件(共同提供專案範本與建置支援)的資訊,請參閱 Visual Studio 對 C++/WinRT 的支援。

建立空白應用程式(書店)

請先在 Microsoft Visual Studio 中,建立新的專案。 為 C++ 專案建立一個 空白應用程式,封裝(桌面版中的 WinUI 3),並將其命名為 Bookstore。 請確認 將方案和專案放在相同目錄中 未勾選。 鎖定最新普遍可用(非預覽版)的 Windows SDK 版本。

我們將撰寫一個新類別來表示一本具有可觀察標題屬性的書。 我們是在同一個編譯單元內撰寫和使用該類別。 但我們希望能從 XAML 綁定這個類別,因此它會是一個執行時類別。 我們會使用 C++/WinRT 來撰寫並使用這些內容。

撰寫新執行時類別的第一步是為專案新增一個 Midl 檔案(.idl) 項目。 將新項目命名為 BookSku.idl。 刪除預設內容 BookSku.idl,並貼上這個執行時類別宣告。

// BookSku.idl
namespace Bookstore
{
    runtimeclass BookSku : Microsoft.UI.Xaml.Data.INotifyPropertyChanged
    {
        BookSku(String title);
        String Title;
    }
}

Note

你的檢視模型類別——事實上,任何你在應用程式中宣告的執行時類別——不必從基底類別衍生出來。 上面提到的 BookSku 類別就是這樣的例子。 它實作了一個介面,但並非源自任何基底類別。

任何你在應用程式中宣告且 源 自基底類別的執行時類別,都稱為 可組合 類別。 可組合類別也有限制。 若要讓應用程式通過 Visual Studio 和 Microsoft Store 用來驗證提交的 Windows 應用程式認證套件 測試 (因此,若要將應用程式成功內嵌至 Microsoft Store),可組合類別最終必須衍生自 Windows 基類。 這表示繼承階層最根端的類別必須是源自 Windows.* 或 Microsoft.* 命名空間的型別。 如果你確實需要從基底類別衍生執行階段類別——例如,實作一個供所有檢視模型衍生的 BindableBase 類別——那麼你可以衍生自 Microsoft.UI.Xaml.DependencyObject。

檢視模型是檢視的抽象,因此它直接綁定到檢視(XAML 標記)上。 資料模型是資料的抽象表示,且只能透過檢視模型來使用,而非直接繫結至 XAML。 所以你可以把資料模型宣告成 C++ 結構體或類別,而不是執行時類別。 它們不需要在 MIDL 中宣告,你可以自由使用任何你喜歡的繼承階層。

儲存檔案,然後建立專案。 這次建置目前還無法(完全)成功完成,但它會替我們完成一些必要的工作。 具體來說,在建置過程中會執行 midl.exe 工具,以建立描述執行階段類別的 Windows 執行階段 中繼資料檔案(該檔案會儲存在磁碟上的 \Bookstore\Debug\Bookstore\Unmerged\BookSku.winmd)。 接著, cppwinrt.exe 工具會被執行產生原始碼檔案,以支援你撰寫和使用執行時類別。 這些檔案包含存根,幫助你開始實作你在 IDL 中宣告的 BookSku 執行時類別。 我們稍後就會在磁碟上找到它們,而那些 stub 就是 \Bookstore\Bookstore\Generated Files\sources\BookSku.h 和 BookSku.cpp。

現在在 Visual Studio 裡右鍵點擊專案節點,然後在檔案總管裡點選「開啟資料夾」。 這樣會在檔案總管裡打開專案資料夾。 你現在應該會看到資料夾 \Bookstore\Bookstore\ 的內容。 接著,進入 \Generated Files\sources\ 資料夾,並將存根檔案 BookSku.h 和 BookSku.cpp 複製到剪貼簿。 回到project資料夾(\Bookstore\Bookstore\),然後貼上剛複製的兩個檔案。 最後,在 方案總管 選擇專案節點時,確保開啟 顯示所有檔案。 右鍵點擊你複製的存根檔案,然後點選 Include In Project。

實作 BookSku

現在讓我們打開 \Bookstore\Bookstore\BookSku.h 並 BookSku.cpp 實作我們的執行時類別。 首先,你會在 BookSku.h 和 BookSku.cpp 的頂端看到一個 static_assert,你需要將其移除。

接著,在 BookSku.h中進行這些修改。

  • 在預設建構子中,將 改 = default 為 = delete。 那是因為我們不想要預設的建構子。
  • 新增一個私人成員來儲存標題字串。 請注意,我們有一個構造子取 winrt::hstring 值。 這個值就是標題字串。
  • 在活動中新增另一位私人會員,等職稱改變時我們會再提出。

完成這些改變後,你的 BookSku.h 外觀會是這樣。

// BookSku.h
#pragma once
#include "BookSku.g.h"

namespace winrt::Bookstore::implementation
{
    struct BookSku : BookSkuT<BookSku>
    {
        BookSku() = delete;
        BookSku(winrt::hstring const& title);

        winrt::hstring Title();
        void Title(winrt::hstring const& value);
        winrt::event_token PropertyChanged(Microsoft::UI::Xaml::Data::PropertyChangedEventHandler const& value);
        void PropertyChanged(winrt::event_token const& token);
    
    private:
        winrt::hstring m_title;
        winrt::event<Microsoft::UI::Xaml::Data::PropertyChangedEventHandler> m_propertyChanged;
    };
}
namespace winrt::Bookstore::factory_implementation
{
    struct BookSku : BookSkuT<BookSku, implementation::BookSku>
    {
    };
}

在 BookSku.cpp中,這些函數是這樣實作的。

// BookSku.cpp
#include "pch.h"
#include "BookSku.h"
#include "BookSku.g.cpp"

namespace winrt::Bookstore::implementation
{
    BookSku::BookSku(winrt::hstring const& title) : m_title{ title }
    {
    }

    winrt::hstring BookSku::Title()
    {
        return m_title;
    }

    void BookSku::Title(winrt::hstring const& value)
    {
        if (m_title != value)
        {
            m_title = value;
            m_propertyChanged(*this, Microsoft::UI::Xaml::Data::PropertyChangedEventArgs{ L"Title" });
        }
    }

    winrt::event_token BookSku::PropertyChanged(Microsoft::UI::Xaml::Data::PropertyChangedEventHandler const& handler)
    {
        return m_propertyChanged.add(handler);
    }

    void BookSku::PropertyChanged(winrt::event_token const& token)
    {
        m_propertyChanged.remove(token);
    }
}

在 Title 變異器函式中,我們會檢查是否設定的值與目前的值不同。 如果是這樣,我們會更新標題,並用等於變更物件名稱的參數來觸發 INotifyPropertyChanged::P ropertyChanged 事件。 這是為了讓使用者介面(UI)知道要重新查詢哪個屬性的值。

如果你想檢查,專案現在會重新建構。

宣告並實作 BookstoreViewModel

我們的主 XAML 頁面會綁定到主視圖模型。 而這個視圖模型會有幾個屬性,其中有一個屬於 BookSku 類型的屬性。 在此階段,我們將宣告並實作主視圖模型執行時類別。

新增一個名為 BookstoreViewModel.idl 的 Midl 檔案 (.idl) 項目。 但也可以參考將執行時類別分解成 Midl 檔案(.idl)。

// BookstoreViewModel.idl
import "BookSku.idl";

namespace Bookstore
{
    runtimeclass BookstoreViewModel
    {
        BookstoreViewModel();
        BookSku BookSku{ get; };
    }
}

儲存並建置(建置還不會完全成功,但我們建置的原因是為了再次產生存根檔案)。

將 BookstoreViewModel.h 和 BookstoreViewModel.cpp 從 Generated Files\sources 資料夾複製到專案資料夾中,並將它們納入專案。 打開這些檔案(再次移除 static_assert),並如下所示實作執行階段類別。 注意,在 BookstoreViewModel.h中,我們包含 BookSku.h了 ,宣告了 BookSku 的實作類型( winrt::Bookstore::實現::BookSku)。 我們正在從預設建構子中移除 = default 。

Note

在下方的 BookstoreViewModel.h 和 BookstoreViewModel.cpp 列表中,程式碼示範了 m_bookSku 資料成員的預設建構方式。 這是從 C++/WinRT 第一版開始就有的方法,至少熟悉這個模式是個好主意。 在 C++/WinRT 2.0 及以上版本中,有一種優化的構造形式稱為 統一構造 (參見 C++/WinRT 2.0 的新聞及變更)。 在本主題後面,我們將展示一個均勻構造的例子。

// BookstoreViewModel.h
#pragma once
#include "BookstoreViewModel.g.h"
#include "BookSku.h"

namespace winrt::Bookstore::implementation
{
    struct BookstoreViewModel : BookstoreViewModelT<BookstoreViewModel>
    {
        BookstoreViewModel();

        Bookstore::BookSku BookSku();

    private:
        Bookstore::BookSku m_bookSku{ nullptr };
    };
}
namespace winrt::Bookstore::factory_implementation
{
    struct BookstoreViewModel : BookstoreViewModelT<BookstoreViewModel, implementation::BookstoreViewModel>
    {
    };
}
// BookstoreViewModel.cpp
#include "pch.h"
#include "BookstoreViewModel.h"
#include "BookstoreViewModel.g.cpp"

namespace winrt::Bookstore::implementation
{
    BookstoreViewModel::BookstoreViewModel()
    {
        m_bookSku = winrt::make<Bookstore::implementation::BookSku>(L"Atticus");
    }

    Bookstore::BookSku BookstoreViewModel::BookSku()
    {
        return m_bookSku;
    }
}

Note

的 m_bookSku 類型是投影型別(winrt::Bookstore::BookSku),而你用 winrt::make 的模板參數是實作型別(winrt::Bookstore::實作::BookSku)。 即便如此,make 會傳回投影類型的實例。

這個計畫現在將重新興建。

在主頁面新增 BookstoreViewModel 類型的屬性

開啟 MainPage.idl,宣告代表我們主介面頁面的執行時類別。

  • 新增用於匯入 BookstoreViewModel.idl 的 import 指令。
  • 新增一個名為 MainViewModel 的唯讀屬性,類型為 BookstoreViewModel。
  • 移除 MyProperty 屬性。
// MainPage.idl
import "BookstoreViewModel.idl";

namespace Bookstore
{
    runtimeclass MainPage : Microsoft.UI.Xaml.Controls.Page
    {
        MainPage();
        BookstoreViewModel MainViewModel{ get; };
    }
}

儲存檔案。 專案目前還無法完全成功建置,但現在建置是有用的,因為它會重新生成實作主 頁 執行類別的原始碼檔案(\Bookstore\Bookstore\Generated Files\sources\MainPage.h 和 MainPage.cpp)。 所以現在就開始建造吧。 此階段你預期會遇到的建置錯誤是 「MainViewModel」:不是 'winrt::Bookstore::implementation::MainPage' 的成員。

如果你省略引入 BookstoreViewModel.idl(請參見上方的 MainPage.idl 列表),就會看到錯誤訊息 在 "MainViewModel" 附近預期應為 <。 另一個建議是確保所有類型都放在同一個命名空間——也就是程式碼列表中顯示的命名空間。

為了解決我們預期會看到的錯誤,你現在需要將 MainViewModel 屬性的 accessor stub 從產生的檔案\Bookstore\Bookstore\Generated Files\sources\MainPage.h ( 和 MainPage.cpp)中複製出來,再複製到 \Bookstore\Bookstore\MainPage.h and MainPage.cpp。 接下來會說明執行這些步驟。

在 \Bookstore\Bookstore\MainPage.h中執行以下步驟。

  • 包含 BookstoreViewModel.h,其宣告了 BookstoreViewModel 的實作類型(也就是 winrt::Bookstore::implementation::BookstoreViewModel)。
  • 新增一個私有成員來儲存檢視模型。 請注意,屬性存取函數(以及成員 m_mainViewModel)是以 BookstoreViewModel 的投影型別( Bookstore::BookstoreViewModel)來實作。
  • 實作類型與應用程式位於同一個專案(編譯單元)中,因此我們透過接受 std::nullptr_t 的建構函式多載來建構 m_mainViewModel。
  • 移除 MyProperty 屬性。

Note

在下方兩組 與 MainPage.hMainPage.cpp的列表中,程式碼說明了 m_mainViewModel 資料成員的預設構造方式。 在接下來的章節中,我們將展示一個改用均勻構造的版本。

// MainPage.h
...
#include "BookstoreViewModel.h"
...
namespace winrt::Bookstore::implementation
{
    struct MainPage : MainPageT<MainPage>
    {
        MainPage();

        Bookstore::BookstoreViewModel MainViewModel();

        void ClickHandler(Windows::Foundation::IInspectable const&, Microsoft::UI::Xaml::RoutedEventArgs const&);

    private:
        Bookstore::BookstoreViewModel m_mainViewModel{ nullptr };
    };
}
...

在 \Bookstore\Bookstore\MainPage.cpp 中,如下方清單所示,請進行以下變更。

  • 呼叫 winrt::make(使用 BookstoreViewModel 實作類型),將投影的 BookstoreViewModel 類型的新執行個體指派給 m_mainViewModel。 如前所述, BookstoreViewModel 建構器會建立一個新的 BookSku 物件作為私人資料成員,並最初將其標題設定為 L"Atticus"。
  • 在按鈕的事件處理程序(ClickHandler)中,將書名更新為已出版的書名。
  • 為 MainViewModel 屬性實作存取器。
  • 移除 MyProperty 屬性。
// MainPage.cpp
#include "pch.h"
#include "MainPage.h"
#include "MainPage.g.cpp"

using namespace winrt;
using namespace Microsoft::UI::Xaml;

namespace winrt::Bookstore::implementation
{
    MainPage::MainPage()
    {
        m_mainViewModel = winrt::make<Bookstore::implementation::BookstoreViewModel>();
        InitializeComponent();
    }

    void MainPage::ClickHandler(Windows::Foundation::IInspectable const& /* sender */, Microsoft::UI::Xaml::RoutedEventArgs const& /* args */)
    {
        MainViewModel().BookSku().Title(L"To Kill a Mockingbird");
    }

    Bookstore::BookstoreViewModel MainPage::MainViewModel()
    {
        return m_mainViewModel;
    }
}

均勻構造

若要使用統一構造而非 winrt::make,請在 m_mainViewModel MainPage.h 中宣告並初始化,只需一步即可,如下所示。

// MainPage.h
...
#include "BookstoreViewModel.h"
...
struct MainPage : MainPageT<MainPage>
{
    ...
private:
    Bookstore::BookstoreViewModel m_mainViewModel;
};
...

然後,在 MainPage.cpp 中的 MainPage 建構函式裡,不需要 m_mainViewModel = winrt::make<Bookstore::implementation::BookstoreViewModel>(); 這段程式碼。

欲了解更多關於統一建構及程式碼範例的資訊,請參閱 「選擇加入統一建構」及「直接實作存取」。

將按鈕綁定到 Title 屬性

開啟 MainPage.xaml,其中包含我們主要UI頁面的 XAML 標記。 如下方列表所示,將按鈕上的名稱移除,並將其 內容 屬性值從字面值改為綁定表達式。 請注意繫結運算式中的 Mode=OneWay 屬性(從檢視模型到 UI 的單向繫結)。 沒有該屬性,UI 將無法回應屬性變更事件。

<Button Click="ClickHandler" Content="{x:Bind MainViewModel.BookSku.Title, Mode=OneWay}"/>

現在建置並執行專案。 點擊按鈕執行 點擊 事件處理程序。 那個操作者呼叫了書名的變異器功能;該變異器會觸發事件,告知使用者介面 標題屬性已 變更;按鈕則重新查詢該屬性的值以更新其 自身的內容 值。

在 C++/WinRT 中使用 {Binding} 標記延伸模組

目前已發行的 C++/WinRT 版本,若要使用 {Binding} 標記擴充功能,你需要實作 ICustomPropertyProvider 和 ICustomProperty 介面。

元素對元素繫結

您可以將一個 XAML 元素的屬性系結至另一個 XAML 元素的屬性。 以下示範其在標記語法中的呈現方式。

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

你需要在 Midl 檔案(.idl)中宣告該命名的 XAML 實體 myTextBox 為唯讀屬性。

// MainPage.idl
runtimeclass MainPage : Microsoft.UI.Xaml.Controls.Page
{
    MainPage();
    Microsoft.UI.Xaml.Controls.TextBox myTextBox{ get; };
}

這是這個必要性的原因。 XAML 編譯器需要驗證的所有類型(包括 {x:Bind} 中使用的類型)都會從 Windows 元資料(WinMD)讀取。 你只需要把唯讀屬性加入你的 Midl 檔案。 不要實作,因為自動生成的 XAML 程式碼會幫你提供實作。

從 XAML 標記中消費物件

所有由 XAML {x:Bind} 標記延伸模組 使用的實體,都必須在 IDL 中公開宣告。 此外,如果 XAML 標記包含另一個同樣在標記中的元素的參考,那麼該標記的 getter 必須存在於 IDL 中。

<Page x:Name="MyPage">
    <StackPanel>
        <CheckBox x:Name="UseCustomColorCheckBox" Content="Use custom color"
             Click="UseCustomColorCheckBox_Click" />
        <Button x:Name="ChangeColorButton" Content="Change color"
            Click="{x:Bind ChangeColorButton_OnClick}"
            IsEnabled="{x:Bind UseCustomColorCheckBox.IsChecked.Value, Mode=OneWay}"/>
    </StackPanel>
</Page>

ChangeColorButton 元素透過綁定指向 UseCustomColorCheckBox 元素。 因此,這個頁面的 IDL 必須宣告一個名為 UseCustomColorCheckBox 的唯讀屬性,才能被綁定存取。

UseCustomColorCheckBox 的點擊事件處理代理使用經典的 XAML 代理語法,因此不需要在 IDL 中設置條目;只要在你的實作課程中公開即可。 另一方面,ChangeColorButton 也有一個 {x:Bind} 點擊事件處理常式,也必須放入 IDL。

runtimeclass MyPage : Microsoft.UI.Xaml.Controls.Page
{
    MyPage();

    // These members are consumed by binding.
    void ChangeColorButton_OnClick();
    Microsoft.UI.Xaml.Controls.CheckBox UseCustomColorCheckBox{ get; };
}

你不需要提供 UseCustomColorCheckBox 屬性的實作。 XAML 程式碼產生器就能幫你做到這點。

綁定至布林值

你可能會在診斷模式下進行此操作:

<TextBlock Text="{Binding CanPair}"/>

在 C++/CX 中會顯示 true 或 false;但在 C++/WinRT 中則會顯示 Windows.Foundation.IReference`1<Boolean>。

而是,在繫結至布林值時使用 x:Bind。

<TextBlock Text="{x:Bind CanPair}"/>

使用 Windows 實作函式庫(WIL)

Windows 實作函式庫(WIL)提供輔助工具,方便撰寫可綁定屬性。 請參閱 WIL 文件中的 「通知屬性 」。

重要 API