XAML 控件;绑定到 C++/WinRT 属性
可有效地绑定到 XAML 项目控件的属性称为可观测属性。 这一想法基于称为“观察者模式”的软件设计模式。 本主题介绍如何在 C++/WinRT 中实现可观测属性,以及如何将 XAML 控件绑定到这些属性(如需背景信息,请参阅数据绑定)。
重要
有关支持你了解如何利用 C++/WinRT 来使用和创作运行时类的基本概述和术语,请参阅通过 C++/WinRT 使用 API 和通过 C++/WinRT 创作 API。
对于属性来说,可观测意味着什么?
假设名为 BookSku 的运行时类有一个名为“标题”的属性 。 如果每当“标题”的值发生更改时,BookSku 都会引发 INotifyPropertyChanged::PropertyChanged 事件,这表示“标题”为一个可观测属性 。 BookSku 的行为(引发或未引发该事件)确定其属性是否可观测,有哪些可观测。
XAML 文本元素或控件可绑定到且可处理这些事件。 此类元素或控件会检索更新的值,然后自行更新以显示新值,从而处理事件。
注意
有关安装和使用 C++/WinRT Visual Studio 扩展 (VSIX) 和 NuGet 包(两者共同提供项目模板,并生成支持)的信息,请参阅适用于 C++/WinRT 的 Visual Studio 支持。
创建空白应用 (Bookstore)
首先在 Microsoft Visual Studio 中创建新项目。 创建“空白应用 (C++/WinRT)”项目,然后将其命名为 Bookstore。 请确保未选中“将解决方案和项目放在同一目录中”。 面向 Windows SDK 的最新正式发布(非预览)版本。
我们将创作新类来表示具有可观测标题属性的书籍。 我们要在同一编译单元内创作和使用该类。 但我们希望能够从 XAML 绑定到此类,因此,它将成为一个运行时类。 而且我们将使用 C++/WinRT 来创作和使用它。
创作新的运行时类的第一步是将新的 Midl 文件(.idl) 项添加到项目。 对新项 BookSku.idl
命名。 删除 BookSku.idl
的默认内容,然后粘贴到此运行时类声明中。
// BookSku.idl
namespace Bookstore
{
runtimeclass BookSku : Windows.UI.Xaml.Data.INotifyPropertyChanged
{
BookSku(String title);
String Title;
}
}
注意
视图模型类无需从基类派生,实际上,在应用程序中声明的任何运行时类都是如此。 上述声明的 BookSku 类就是这样一个例子。 它实现接口,但不从任何基类派生。
从基类派生的任何运行时类(在应用程序中声明)都称为可组合类 。 且可组合类存在一些限制。 若要使应用程序通过 Visual Studio 和 Microsoft Store 用于验证提交的 Windows 应用认证工具包测试,使 Microsoft Store 可成功纳入该应用程序,可组合类必须最终派生自 Windows 基类。 这意味着继承层次结构的根类必须是源于 Windows.* 名称空间的类型。 如果确实需要从基类派生运行时类(例如,为要从中派生的所有视图模型实现 BindableBase 类),则可以从 Windows.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(Windows::UI::Xaml::Data::PropertyChangedEventHandler const& value);
void PropertyChanged(winrt::event_token const& token);
private:
winrt::hstring m_title;
winrt::event<Windows::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, Windows::UI::Xaml::Data::PropertyChangedEventArgs{ L"Title" });
}
}
winrt::event_token BookSku::PropertyChanged(Windows::UI::Xaml::Data::PropertyChangedEventHandler const& handler)
{
return m_propertyChanged.add(handler);
}
void BookSku::PropertyChanged(winrt::event_token const& token)
{
m_propertyChanged.remove(token);
}
}
在“标题”转变器函数中,我们检查设置的值是否与当前值不同。 如果是,我们将更新标题并引发 INotifyPropertyChanged::PropertyChanged 事件,其中包含一个等于已更改的属性的名称的参数。 这样,用户界面 (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.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;
}
}
注意
m_bookSku
的类型是投影类型 (winrt::Bookstore::BookSku),而且你用于 winrt::make 的模板参数是实现类型 (winrt::Bookstore::implementation::BookSku) 。 即使如此,make 也会返回投影类型的实例。
现在将再次生成项目。
将类型 BookstoreViewModel 的属性添加到 MainPage
打开 MainPage.idl
,这将声明表示主 UI 页面的运行时类。
- 添加
import
指令来导入BookstoreViewModel.idl
。 - 添加一个类型为 BookstoreViewModel、名为 MainViewModel 的只读属性 。
- 删除 MyProperty 属性。
// MainPage.idl
import "BookstoreViewModel.idl";
namespace Bookstore
{
runtimeclass MainPage : Windows.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&, Windows::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 Windows::UI::Xaml;
namespace winrt::Bookstore::implementation
{
MainPage::MainPage()
{
m_mainViewModel = winrt::make<Bookstore::implementation::BookstoreViewModel>();
InitializeComponent();
}
void MainPage::ClickHandler(Windows::Foundation::IInspectable const& /* sender */, Windows::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>();
。
有关统一构造的详细信息,请参阅选择加入统一构造和直接实现访问。
将按钮绑定到“标题”属性
打开 MainPage.xaml
,其中包含主 UI 页面的 XAML 标记。 如下表所示,删除按钮中的名称,并将其 Content 属性值从文字更改为绑定表达式。 注意绑定表达式上的 Mode=OneWay
属性(从视图模型到 UI 单向)。 没有该属性,UI 将不会响应属性更改事件。
<Button Click="ClickHandler" Content="{x:Bind MainViewModel.BookSku.Title, Mode=OneWay}"/>
立即生成并运行该项目。 单击该按钮以执行 Click 事件处理程序。 该处理程序调用书籍的标题转变器函数;该转变器引发了让 UI 知道“标题”属性已发生更改的事件;而且按钮重新查询了该属性的值以更新其自己的“内容”值 。
配合使用 {Binding} 标记扩展与 C++/WinRT
对于当前发布的 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 : Windows.UI.Xaml.Controls.Page
{
MainPage();
Windows.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 : Windows.UI.Xaml.Controls.Page
{
MyPage();
// These members are consumed by binding.
void ChangeColorButton_OnClick();
Windows.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 文档中的通知属性。