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

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

這很重要

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

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

假設一個名為 BookSku 的執行時類別有一個名為 Title 的屬性。 如果 BookSkuTitle 值改變時觸發 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.hBookSku.cpp

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

實作 BookSku

現在讓我們打開 \Bookstore\Bookstore\BookSku.hBookSku.cpp 實作我們的執行時類別。 首先,你會在 BookSku.hBookSku.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.idlMidl 檔案 (.idl) 項目。 但也可以參考將執行時類別分解成 Midl 檔案(.idl)。

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

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

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

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

Note

在下方的 BookstoreViewModel.hBookstoreViewModel.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.idlimport 指令。
  • 新增一個名為 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.hMainPage.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} 標記擴充功能,你需要實作 ICustomPropertyProviderICustomProperty 介面。

元素對元素繫結

您可以將一個 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 中會顯示 truefalse;但在 C++/WinRT 中則會顯示 Windows.Foundation.IReference`1<Boolean>

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

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

使用 Windows 實作函式庫(WIL)

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

重要 API