可以有效地绑定到 XAML 控件的属性称为 可观察 属性。 此想法基于称为 观察程序模式的软件设计模式。 本主题演示如何在 C++/WinRT 中实现可观测属性,以及如何将 XAML 控件绑定到它们(有关背景信息,请参阅 数据绑定)。
Important
有关有助于你了解如何使用和编写运行时类(借助 C++/WinRT)的基本概念和术语,请参阅 借助 C++/WinRT 使用 API 和 借助 C++/WinRT 编写 API。
对于属性来说,observable 是什么意思?
假设名为 BookSku 的运行时类具有名为 Title 的属性。 如果 BookSku 在 Title 的值发生更改时引发 INotifyPropertyChanged::PropertyChanged 事件,则意味着 Title 是可观察属性。 由 BookSku 的行为(是否引发该事件)决定它有哪些属性(如果有的话)是可观察的。
XAML 文本元素或控件可以绑定到和处理这些事件。 此类元素或控件通过检索更新的值来处理事件,然后更新自身以显示新值。
注释
有关安装和使用 C++/WinRT Visual Studio 扩展(VSIX)和 NuGet 包(一起提供项目模板和生成支持)的信息,请参阅Visual Studio对 C++/WinRT 的支持。
创建空白应用(书店)
首先,在 Microsoft Visual Studio 中创建新项目。 为 C++ 项目创建一个空白应用(已打包,桌面版 WinUI 3),并将其命名为 Bookstore。 确保 将解决方案和项目放在同一目录中 未被选中。 以最新版且正式发布的 Windows SDK(即非预览版)为目标。
我们将创作一个新类来表示具有可观察标题属性的书籍。 我们在同一个编译单元中编写并使用该类。 但我们希望能够从 XAML 绑定到此类,因此它将是运行时类。 我们将使用 C++/WinRT 来编写和使用它。
创作新运行时类的第一步是向项目添加新 的 Midl File (.idl) 项。 对新项 BookSku.idl 命名。 删除 BookSku.idl 的默认内容,并粘贴入此运行时类声明。
// BookSku.idl
namespace Bookstore
{
runtimeclass BookSku : Microsoft.UI.Xaml.Data.INotifyPropertyChanged
{
BookSku(String title);
String Title;
}
}
注释
视图模型类(事实上,在应用程序中声明的任何运行时类)不需要从基类派生。 上面声明的 BookSku 类就是其中的示例。 它实现接口,但它不派生自任何基类。
在从基 类派生 的应用程序中声明的任何运行时类称为 可组合 类。 而且,可组合类也受到一些限制。 要使应用程序通过 Visual Studio 和 Microsoft 应用商店使用的 Windows 应用认证工具包 测试来验证提交(因此,应用程序能够成功引入到 Microsoft 应用商店),可组合类最终必须派生自 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 运行时类。 稍后我们会在磁盘上找到它们,但这些存根分别是 \Bookstore\Bookstore\Generated Files\sources\BookSku.h 和 BookSku.cpp。
因此,现在右键单击Visual Studio中的项目节点,然后在文件资源管理器中单击“打开文件夹”。 这将在文件资源管理器中打开项目文件夹。 你现在应该正在查看 \Bookstore\Bookstore\ 文件夹的内容。 然后,进入 \Generated Files\sources\ 文件夹,并将存根文件 BookSku.h 和 BookSku.cpp 复制到剪贴板。 导航回项目文件夹 (\Bookstore\Bookstore\),然后粘贴刚刚复制的两个文件。 最后,在解决方案资源管理器中选中项目节点后,请确保显示所有文件已打开。 右键单击复制的存根文件,然后单击加入项目。
实现 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 mutator 函数中,我们检查是否正在设置与当前值不同的值。 如果是,则更新标题,并引发 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::implementation::BookSku)。 我们正在从默认构造函数中删除 = default 。
注释
在下面的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;
}
}
注释
m_bookSku 的类型是投影类型(winrt::Bookstore::BookSku),而与 winrt::make 配合使用的模板参数则是实现类型(winrt::Bookstore::implementation::BookSku)。 即便如此, Make 也会返回投影类型的实例。
该项目现在将再次构建。
将 BookstoreViewModel 类型的属性添加到 MainPage
Open MainPage.idl,它声明表示主 UI 页的运行时类。
- 添加用于导入
BookstoreViewModel.idl的import指令。 - 添加名为 MainViewModel 的只读属性,类型为 BookstoreViewModel。
- 删除 MyProperty 属性。
// MainPage.idl
import "BookstoreViewModel.idl";
namespace Bookstore
{
runtimeclass MainPage : Microsoft.UI.Xaml.Controls.Page
{
MainPage();
BookstoreViewModel MainViewModel{ get; };
}
}
保存文件。 该项目目前尚无法完全成功生成,但现在进行生成仍然很有用,因为这样会重新生成实现了 MainPage 运行时类的源代码文件(\Bookstore\Bookstore\Generated Files\sources\MainPage.h 和 MainPage.cpp)。 现在就开始构建吧。 在此阶段可以看到的生成错误是 “MainViewModel”:不是“winrt::Bookstore::implementation:::MainPage”的成员。
如果省略了包含 BookstoreViewModel.idl 的语句(请参见上文中 MainPage.idl 的列表),就会看到错误:在“MainViewModel”附近应为 <。 另一个提示是确保将所有类型保留在同一命名空间中,即代码列表中显示的命名空间。
若要解决我们预期会看到的错误,现在需要将 MainViewModel 属性的访问器存根从生成的文件(\Bookstore\Bookstore\Generated Files\sources\MainPage.h 和 MainPage.cpp)中复制出来,并复制到 \Bookstore\Bookstore\MainPage.h 和 MainPage.cpp 中。 接下来将介绍要执行的步骤。
在 \Bookstore\Bookstore\MainPage.h中,执行这些步骤。
- 包括
BookstoreViewModel.h,它声明 BookstoreViewModel 的实现类型(即 winrt::Bookstore::implementation::BookstoreViewModel)。 - 添加一个私有成员来存储视图模型。 请注意,属性访问器函数(以及成员 m_mainViewModel)是基于 BookstoreViewModel 的投影类型实现的(即 Bookstore::BookstoreViewModel)。
- 实现类型与应用程序位于同一项目(编译单元)中,因此我们通过接受 std::nullptr_t 的构造函数重载来构造 m_mainViewModel。
- 删除 MyProperty 属性。
注释
在下面针对 MainPage.h 和 MainPage.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,只需在一个步骤中 MainPage.h 声明和初始化 m_mainViewModel ,如下所示。
// 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 标记。 如以下列表所示,从按钮中删除名称,并将其 Content 属性值从文本更改为绑定表达式。 请注意绑定表达式上的 Mode=OneWay 属性(从视图模型到用户界面的单向绑定)。 如果没有该属性,UI 将不会响应属性更改事件。
<Button Click="ClickHandler" Content="{x:Bind MainViewModel.BookSku.Title, Mode=OneWay}"/>
现在构建并运行项目。 单击该按钮以执行 Click 事件处理程序。 该处理程序会调用该书标题的修改器函数;该修改器会引发一个事件,让 UI 知道 Title 属性已更改;而按钮则会重新查询该属性的值,以更新其 Content 值。
在 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 中添加条目;只需在实现类中将其声明为 public。 另一方面,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 文档中的 通知属性 。