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.hBookSku.cpp

右键单击项目节点,然后单击“打开文件资源管理器中的文件夹”。 执行此操作,将在文件资源管理器中打开项目文件夹。 将这些存根文件 BookSku.hBookSku.cpp\Bookstore\Bookstore\Generated Files\sources\ 文件夹复制到项目文件夹,即 \Bookstore\Bookstore\。 在“解决方案资源管理器”中选中项目节点,确保将“显示所有文件”打开 。 右键单击已复制的存根文件,然后单击“包括在项目中”。

实现 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(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。 在此步骤中,我们将声明并实现主视图模型运行时类。

添加名为 的新的 Midl 文件 (.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::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

打开 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 运行时类的源代码文件(MainPage.cpp)。 因此,现在请继续生成。 此阶段可能会发生的生成错误是“MainViewModel”:不是“winrt::Bookstore::implementation::MainPage”的成员。

如果未包含 BookstoreViewModel.idl(请参阅上述 MainPage.idl 的列表),在“MainViewModel”附近预期 <将会发生此错误。 另一个小提示是确保所有类型都保留在同一命名空间中:代码列表中所显示的命名空间。

若要解决预期发生的错误,则现在需要将 MainViewModel 属性的访问器存根从生成的文件(MainPage.cpp)复制到 \Bookstore\Bookstore\MainPage.hMainPage.cpp。 操作步骤如下所示。

\Bookstore\Bookstore\MainPage.h 中,执行以下步骤。

  • 包含 BookstoreViewModel.h,它为 BookstoreViewModel 声明实现类型(即 winrt::Bookstore::implementation::BookstoreViewModel)。
  • 添加私有成员以存储视图模型。 注意,属性访问器函数(以及成员 m_mainViewModel)根据 BookstoreViewModel 的投影类型(即 Bookstore::BookstoreViewModel)实现 。
  • 实现类型与应用程序位于同一项目(编译单元),因此我们通过采用 std::nullptr_t 的构造函数重载来构造 m_mainViewModel。
  • 删除 MyProperty 属性。

注意

在下面的 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&, Windows::UI::Xaml::RoutedEventArgs const&);

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

如以下列表所示,在 \Bookstore\Bookstore\MainPage.cpp 中进行以下更改。

  • 调用 winrt::make(具有 BookstoreViewModel 实现类型)将投影的 BookstoreViewModel 类型的新实例分配到 m_mainViewModel 。 正如前文所述,BookstoreViewModel 构造函数会创建一个新的 BookSku 对象作为专用数据成员,并在一开始将其标题设置为
  • 在按钮的事件处理程序 (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,请在 中声明和初始化 m_mainViewModel;此操作只需一步,如下所示。

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

接下来,在 中的 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} 标记扩展,需要实现 ICustomPropertyProviderICustomProperty 接口。

元素间的绑定

可以将一个 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 也有一个 点击事件处理程序,该程序也必须进入 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 代码生成器会为你这样做。

绑定到布尔值

可以在诊断模式下这样做。

<syntaxhighlight lang="xml"><TextBlock Text="{Binding CanPair}"/></syntaxhighlight>

这会在 C++/CX 中会显示 truefalse,但在 C++/WinRT 中会显示 Windows.Foundation.IReference`1<布尔值>。

绑定到布尔值时使用 x:Bind

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

重要的 API