将 XAML 控件绑定到 C++/WinRT 属性

可以有效地绑定到 XAML 控件的属性称为 可观察 属性。 此想法基于称为 观察程序模式的软件设计模式。 本主题演示如何在 C++/WinRT 中实现可观测属性,以及如何将 XAML 控件绑定到它们(有关背景信息,请参阅 数据绑定)。

Important

有关有助于你了解如何使用和编写运行时类(借助 C++/WinRT)的基本概念和术语,请参阅 借助 C++/WinRT 使用 API借助 C++/WinRT 编写 API

对于属性来说,observable 是什么意思?

假设名为 BookSku 的运行时类具有名为 Title 的属性。 如果 BookSkuTitle 的值发生更改时引发 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.hBookSku.cpp

因此,现在右键单击Visual Studio中的项目节点,然后在文件资源管理器中单击“打开文件夹”。 这将在文件资源管理器中打开项目文件夹。 你现在应该正在查看 \Bookstore\Bookstore\ 文件夹的内容。 然后,进入 \Generated Files\sources\ 文件夹,并将存根文件 BookSku.hBookSku.cpp 复制到剪贴板。 导航回项目文件夹 (\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(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.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

Open MainPage.idl,它声明表示主 UI 页的运行时类。

  • 添加用于导入 BookstoreViewModel.idlimport 指令。
  • 添加名为 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.hMainPage.cpp)。 现在就开始构建吧。 在此阶段可以看到的生成错误是 “MainViewModel”:不是“winrt::Bookstore::implementation:::MainPage”的成员

如果省略了包含 BookstoreViewModel.idl 的语句(请参见上文中 MainPage.idl 的列表),就会看到错误:在“MainViewModel”附近应为 <。 另一个提示是确保将所有类型保留在同一命名空间中,即代码列表中显示的命名空间。

若要解决我们预期会看到的错误,现在需要将 MainViewModel 属性的访问器存根从生成的文件(\Bookstore\Bookstore\Generated Files\sources\MainPage.hMainPage.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&, 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} 标记扩展,需要实现 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 中添加条目;只需在实现类中将其声明为 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 中,它显示为 truefalse;但在 C++/WinRT 中,它显示为 Windows.Foundation.IReference`1<Boolean>

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

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

使用Windows实现库 (WIL)

Windows实现库(WIL)提供了帮助程序来简化可绑定属性的编写。 请参阅 WIL 文档中的 通知属性

重要 API