一個能有效綁定於 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 文件中的 「通知屬性 」。