通过


深入探讨 Windows 数据绑定

本文介绍使用 Microsoft.UI.Xaml.Data 命名空间中的 API 的 WinUI 3 数据绑定功能。

注释

本主题详细介绍了数据绑定功能。 有关简短的实际简介,请参阅 数据绑定概述

重要 API

介绍

数据绑定是一种技术,允许应用的 UI 高效显示和同步数据。 通过将数据问题与 UI 关注点分开,它简化了应用设计,增强了可读性,并提高了可维护性。

首次显示 UI 时,可以使用数据绑定来仅显示数据源中的值,但不能响应这些值中的更改。 此绑定模式称为 一次性,它适用于在运行时不会更改的值。 或者,你可以选择“观察”这些值,并在它们发生变化时更新UI。 此模式称为 单向模式,适用于只读数据。 最终,可以选择同时观察和更新,以便用户对 UI 中的值所做的更改会自动推送回数据源。 此模式称为 双向模式,适用于读写数据。 下面是一些示例。

  • 可以使用一次性模式将 图像 绑定到当前用户的照片。
  • 可以使用单向模式将 ListView 绑定到按报纸分区分组的实时新闻文章集合。
  • 可以使用双向模式将 TextBox 绑定到表单中的客户名称。

与模式无关,有两种类型的绑定,通常在 UI 标记中声明这两种绑定。 可以选择使用 {x:Bind} 标记扩展{Binding} 标记扩展。 甚至可以在同一应用中混合使用这两个元素,即使在同一 UI 元素上也是如此。 {x:Bind}是 UWP 中用于Windows 10的新增功能,性能更佳。 本主题中所述的所有详细信息都适用于这两种类型的绑定,除非我们明确说明。

演示 {x:Bind} 的 UWP 示例应用

演示 {Binding} 的 UWP 示例应用

每个绑定都涉及这些部分

  • 绑定源。 此源提供绑定的数据。 它可以是任何包含有需在 UI 中显示的值的成员的类的实例。
  • 绑定目标。 此目标是在您的 UI 中用于显示数据的 FrameworkElementDependencyProperty
  • 绑定对象。 此对象将数据从源传输到目标,也可以从目标传回源。 绑定对象是在 XAML 加载时从 {x:Bind}{Binding} 标记扩展创建的。

在以下部分中,你将仔细了解绑定源、绑定目标和绑定对象。 这些节与将按钮的内容绑定到名为 NextButtonText字符串属性的示例链接在一起,该属性属于名为 HostViewModel的类。

绑定源

您可以使用下面实现的一个类作为绑定源。

public class HostViewModel
{
    public HostViewModel()
    {
        NextButtonText = "Next";
    }

    public string NextButtonText { get; set; }
}

该实现 HostViewModel及其属性 NextButtonText仅适用于一次性绑定。 但是单向绑定和双向绑定非常常见。 在这些类型的绑定中,UI 会自动更新以响应绑定源的数据值更改。 若要使这些类型的绑定正常工作,需要使绑定源对绑定对象 可见 。 因此,在我们的示例中,如果要单向或双向绑定到 NextButtonText 属性,那么该属性值在运行时发生的任何更改都需要被绑定对象观察到。

一种方法是将表示绑定源的类继承自DependencyObject,并通过DependencyProperty*公开数据值。 这就是 FrameworkElement 的可观测性。 A FrameworkElement 是开箱即用的优秀绑定源。

实现 System.ComponentModel.INotifyPropertyChanged 是使一个类可观察的更轻量的方法,并且对于那些已经有基类的类来说,这也是必要的。 此方法涉及实现名为 PropertyChanged 的单个事件。 以下代码显示了一个使用 HostViewModel 的示例。

...
using System.ComponentModel;
using System.Runtime.CompilerServices;
...
public class HostViewModel : INotifyPropertyChanged
{
    private string nextButtonText;

    public event PropertyChangedEventHandler PropertyChanged = delegate { };

    public HostViewModel()
    {
        NextButtonText = "Next";
    }

    public string NextButtonText
    {
        get { return nextButtonText; }
        set
        {
            nextButtonText = value;
            OnPropertyChanged();
        }
    }

    public void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        // Raise the PropertyChanged event, passing the name of the property whose value has changed.
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

现在,该 NextButtonText 属性是可观测到的。 当你创作到该属性的单向绑定或双向绑定时(稍后将演示如何),生成的绑定对象会订阅该 PropertyChanged 事件。 引发该事件时,绑定对象的处理程序会收到一个参数,其中包含已更改的属性的名称。 这就是绑定对象知道要再次读取的属性的值的方式。

因此,如果使用的是 C#,则无需实现前面显示的模式,可以从 BindableBase 示例(位于“Common”文件夹中)中找到的 基类派生。 以下是这样的示例。

public class HostViewModel : BindableBase
{
    private string nextButtonText;

    public HostViewModel()
    {
        NextButtonText = "Next";
    }

    public string NextButtonText
    {
        get { return nextButtonText; }
        set { SetProperty(ref nextButtonText, value); }
    }
}

PropertyChanged使用 String.Empty 的参数引发事件,或null指示应重新读取对象上的所有非索引器属性。 可以引发该事件以指示对象上的索引器属性已更改,方法是对特定索引器使用“Item[indexer]”参数(其中 索引器 是索引值),或为所有索引器使用“Item[]”的值。

可以将绑定源视为其属性包含数据的单个对象,也可以视为对象的集合。 在 C# 代码中,可以一次性绑定到实现 List<T> 的对象,以显示运行时不会更改的集合。 对于可观察的集合(用于在向集合中添加和删除项时进行观察),请改为单向绑定到 ObservableCollection<T>。 若要绑定到您自己的集合类,请参考下表中的指南。

Scenario C# (CLR) C++/WinRT
绑定到对象。 可以是任何对象。 可以是任何对象。
从绑定对象获取属性更改通知。 对象必须实现 INotifyPropertyChanged 对象必须实现 INotifyPropertyChanged
绑定到集合。 列表<T> IInspectableIVector,或 IBindableObservableVector 的 IVector。 请参阅 XAML 项控件绑定到 C++/WinRT 集合C++/WinRT 集合
从绑定集合获取集合更改通知。 ObservableCollection<T> IObservableVectorIInspectable。 例如 winrt::single_threaded_observable_vector<T>
实现支持绑定的集合。 扩展 列表<T> 或实现 IListIList<对象>、 IEnumerableIEnumerable<对象>。 绑定到泛型 IList<T>IEnumerable<T> 是不被支持的。 实现 IInspectableIVector。 请参阅 XAML 项控件绑定到 C++/WinRT 集合C++/WinRT 集合
实现一个集合,该集合支持更改通知。 扩展 ObservableCollection<T> 或实现 (非泛型) IListINotifyCollectionChanged 实现 IInspectable 的 IObservableVectorIBindableObservableVector
实现支持增量加载的集合。 扩展 ObservableCollection<T> 或实现 (非泛型) IListINotifyCollectionChanged。 此外,实现 ISupportIncrementalLoading 实现 IInspectable 的 IObservableVectorIBindableObservableVector。 此外,实现 ISupportIncrementalLoading

可以使用增量加载将列表控件绑定到任意大型数据源,但仍能实现高性能。 例如,可以将列表控件绑定到Bing图像查询结果,而无需同时加载所有结果。 相反,只需立即加载一些结果,并根据需要加载其他结果。 若要支持增量加载,必须在支持集合更改通知的数据源上实现 ISupportIncrementalLoading 。 当数据绑定引擎请求更多数据时,数据源必须发出相应的请求,集成结果,然后发送相应的通知以更新 UI。

绑定目标

在以下两个示例中,该 Button.Content 属性是绑定目标。 其值被设置为一个标记扩展,用于声明绑定对象。 第一个示例显示 {x:Bind},第二个示例显示 {Binding}。 在标记中声明绑定是一种普遍做法,因为它方便、易读,并且便于工具处理。 但是,如果需要,可以避免标记和命令性地(以编程方式)创建 Binding 类的实例。

<Button Content="{x:Bind ...}" ... />
<Button Content="{Binding ...}" ... />

如果使用的是 C++/WinRT,则需要将 BindableAttribute 属性添加到要使用的 {Binding} 标记扩展的任何运行时类。

重要

如果使用 C++/WinRT,则 BindableAttribute 属性可用于Windows App SDK。 如果没有该属性,需要实现 ICustomPropertyProviderICustomProperty 接口才能使用 {Binding} 标记扩展。

使用 {x:Bind} 声明的绑定对象

在您开始编写 {x:Bind} 标记之前,需要将表示您标记页面的类中的绑定源类公开。 将属性(在本例中为类型 HostViewModel )添加到 MainWindow 窗口类。

namespace DataBindingInDepth
{
    public sealed partial class MainWindow : Window
    {
        public MainWindow()
        {
            this.InitializeComponent();
            ViewModel = new HostViewModel();
        }
    
        public HostViewModel ViewModel { get; set; }
    }
}

添加属性后,可以仔细查看声明绑定对象的标记。 以下示例使用前面“绑定目标”部分中看到的相同 Button.Content 绑定目标。 所显示的内容是绑定到HostViewModel.NextButtonText属性的绑定目标。

<!-- MainWindow.xaml -->
<Window x:Class="DataBindingInDepth.MainWindow" ... >
    <Button Content="{x:Bind Path=ViewModel.NextButtonText, Mode=OneWay}" ... />
</Window>

请注意你为其 Path指定的值。 该窗口在其自己的上下文中解释此值。 在这种情况下,路径首先引用 ViewModel 刚刚添加到 MainWindow 页面的属性。 该属性返回一个 HostViewModel 实例,因此可以通过该对象的点语法来访问 HostViewModel.NextButtonText 属性。 指定Mode以覆盖{x:Bind}的默认一次绑定值。

Path 属性支持各种语法选项,用于绑定到嵌套属性、附加属性和整数和字符串索引器。 有关详细信息,请参阅 属性路径语法。 通过绑定到字符串索引器,可以实现绑定到动态属性的效果,而无需实现 ICustomPropertyProvider。 有关其他设置,请参阅 {x:Bind} 标记扩展

为了说明 HostViewModel.NextButtonText 属性是可观测的,请将 Click 事件处理程序添加到按钮,并更新 HostViewModel.NextButtonText 的值。 生成、运行并单击按钮以查看按钮 Content 更新的值。

// MainWindow.xaml.cs
private void Button_Click(object sender, RoutedEventArgs e)
{
    ViewModel.NextButtonText = "Updated Next button text";
}

注释

TextBox.Text 失去焦点时,对 TextBox 的更改将发送到双向绑定源,而不是在每个用户击键之后。

DataTemplate 和 x:DataType

DataTemplate 中(无论是将其用作项模板、内容模板还是标头模板),窗口上下文中不会解释该值 Path 。 相反,它适用于要模板化的数据对象的上下文。 在数据模板中使用 {x:Bind} 时,可以在编译时验证其绑定并为其生成有效的代码。 要实现这一点,DataTemplate需要使用x:DataType来声明其数据对象的类型。 以下示例可用作项控件ItemTemplate,绑定到SampleDataGroup对象集合。

<DataTemplate x:Key="SimpleItemTemplate" x:DataType="data:SampleDataGroup">
    <StackPanel Orientation="Vertical" Height="50">
      <TextBlock Text="{x:Bind Title}"/>
      <TextBlock Text="{x:Bind Description}"/>
    </StackPanel>
  </DataTemplate>

Path 中的弱类型对象

假设你有一个类型SampleDataGroup,它实现了名为Title的字符串属性。 您还有一个属性MainWindow.SampleDataGroupAsObject,它的类型是object,但实际上返回的是SampleDataGroup的实例。 绑定 <TextBlock Text="{x:Bind SampleDataGroupAsObject.Title}"/> 会导致编译错误,因为在类型 Title 上找不到属性 object。 若要修复此错误,请将类型转换添加到Path语法中,如下所示:<TextBlock Text="{x:Bind ((data:SampleDataGroup)SampleDataGroupAsObject).Title}"/> 下面是另一个示例,其中 Element 声明为 object 但实际上是一个 TextBlock<TextBlock Text="{x:Bind Element.Text}"/>。 类型转换修复了此问题: <TextBlock Text="{x:Bind ((TextBlock)Element).Text}"/>

如果数据以异步方式加载

Windows 的分部类在编译时生成代码以支持 {x:Bind} 。 可以在文件夹中obj找到这些文件,其名称类似于 (对于 C#)。 <view name>.g.cs 生成的代码包括一个用于窗体加载事件的处理程序。 该处理程序在表示窗口绑定的生成类上调用 Initialize 方法。 Initialize 调用 Update 以开始在绑定源和目标之间移动数据。 Loading 是在窗口或用户控件的第一次测量过程开始前引发的。 如果数据以异步方式加载,则调用数据时可能尚未准备就绪 Initialize 。 加载数据后,可以通过调用 this.Bindings.Update();强制一次性绑定进行初始化。 如果只需要异步加载数据的一次性绑定,则以这种方式初始化它们比使用单向绑定和侦听更改要便宜得多。 如果您的数据不会发生细粒度的更改,并且可能作为特定操作的一部分进行更新,您可以使用一次性绑定,并随时通过调用Update来强制手动更新。

注释

{x:Bind} 不适合后期绑定方案,例如导航 JSON 对象的字典结构,也不适合鸭子键入。 “鸭子类型”是一种基于属性名称词法匹配的弱类型(如,“如果它走路、游泳和呱呱叫都像鸭子,那么它就是鸭子”)。 使用鸭子键入时,对 Age 属性的绑定同样满足于某个 Person 或对象 Wine (假设这些类型每个类型都有一个 Age 属性)。 对于这些方案,请使用 {Binding} 标记扩展。

使用 {Binding} 声明的绑定对象

如果使用 C++/WinRT,请将 BindableAttribute 属性添加到使用 {Binding} 标记扩展时要绑定到的任何运行时类。 若要使用 {x:Bind},不需要该属性。

// HostViewModel.idl
// Add this attribute:
[Microsoft.UI.Xaml.Data.Bindable]
runtimeclass HostViewModel : Microsoft.UI.Xaml.Data.INotifyPropertyChanged
{
    HostViewModel();
    String NextButtonText;
}

重要

如果使用 C++/WinRT,则 BindableAttribute 属性可用于Windows App SDK。 如果没有该属性,需要实现 ICustomPropertyProviderICustomProperty 接口才能使用 {Binding} 标记扩展。

默认情况下, {Binding} 假定你正在绑定到标记窗口的 DataContext 。 因此,将 DataContext 窗口设置为绑定源类的实例(在本例中为类型 HostViewModel )。 以下示例演示声明绑定对象的标记。 它使用与前面“绑定目标”部分相同的 Button.Content 绑定目标,并绑定到 HostViewModel.NextButtonText 属性。

<Window xmlns:viewmodel="using:DataBindingInDepth" ... >
    <Window.DataContext>
        <viewmodel:HostViewModel x:Name="viewModelInDataContext"/>
    </Window.DataContext>
    ...
    <Button Content="{Binding Path=NextButtonText}" ... />
</Window>
// MainWindow.xaml.cs
private void Button_Click(object sender, RoutedEventArgs e)
{
    viewModelInDataContext.NextButtonText = "Updated Next button text";
}

请注意为 Path 指定的值。 窗口的 DataContext 解释此值,在此示例中,该值设置为实例 HostViewModel。 路径引用属性 HostViewModel.NextButtonText 。 你可以省略 Mode,因为 {Binding} 的默认行为是单向绑定,这在这里可以使用。

UI 元素的 DataContext 默认值是其父元素的继承值。 可以通过显式设置 DataContext 来替代该默认设置,后者又由子级默认继承。 设置DataContext显式应用于一个元素时是很有用的,特别是在你想要多个绑定使用同一源的情况下。

绑定对象具有一个 Source 属性,该属性默认为声明绑定的 UI 元素的 DataContext 。 可以通过设置SourceRelativeSourceElementName或显式在绑定上替代此默认值(有关详细信息,请参阅 {Binding} )。

DataTemplate 中, DataContext 会自动设置为正在模板化的数据对象。 以下示例可用作绑定到具有字符串属性名为 ItemTemplateTitle 的任意类型集合的项控件。

<DataTemplate x:Key="SimpleItemTemplate">
    <StackPanel Orientation="Vertical" Height="50">
      <TextBlock Text="{Binding Title}"/>
      <TextBlock Text="{Binding Description"/>
    </StackPanel>
  </DataTemplate>

注释

默认情况下,当 TextBox 失去焦点时,TextBox.Text 的更改将发送到双向绑定源。 若要在每个用户击键后导致更改被发送,请在标记中的绑定中将UpdateSourceTrigger设置为PropertyChanged。 您还可以通过将UpdateSourceTrigger设置为Explicit,完全掌握何时向源发送更改。 然后处理文本框(通常是 TextBox.TextChanged)上的事件,对目标调用 GetBindingExpression 以获取 BindingExpression 对象,最后调用 BindingExpression.UpdateSource 以编程方式更新数据源。

Path 属性支持各种语法选项,用于绑定到嵌套属性、附加属性和整数和字符串索引器。 有关详细信息,请参阅 属性路径语法。 绑定到字符串索引器可以实现绑定到动态属性的效果,而无需实现 ICustomPropertyProvider ElementName 属性对于元素到元素绑定非常有用。 RelativeSource 属性有多个用途,其中一个是 ControlTemplate 中模板绑定的更强大的替代方法。 有关其他设置,请参阅 {Binding} 标记扩展Binding 类。

如果源和目标的类型不相同,该怎么办?

如果要基于布尔属性的值控制 UI 元素的可见性,或者想要呈现具有数值范围或趋势函数的颜色的 UI 元素,或者如果要在需要字符串的 UI 元素属性中显示日期和时间值,则 然后,需要将值从一种类型转换为另一种类型。 在某些情况下,正确的解决方案是从您的绑定源类中公开一个具有正确类型的属性,并将转换逻辑封装好、便于测试。 但是,当你包含大量或多种源和目标属性组合时,该解决方案并不灵活或可扩展。 在这种情况下,可以选择以下几个选项:

  • 如果使用{x:Bind},则可以直接绑定到执行该转换的函数。
  • 或者,可以指定一个值转换器,该转换器是一个旨在执行转换的对象

值转换器

下面是一个值转换器,适合一次性绑定或单向绑定,可将 DateTime 值转换为 string 包含月份的值。 该类实现 IValueConverter

public class DateToStringConverter : IValueConverter
{
    // Define the Convert method to convert a DateTime value to 
    // a month string.
    public object Convert(object value, Type targetType, 
        object parameter, string language)
    {
        // value is the data from the source object.
        DateTime thisDate = (DateTime)value;
        int monthNum = thisDate.Month;
        string month;
        switch (monthNum)
        {
            case 1:
                month = "January";
                break;
            case 2:
                month = "February";
                break;
            default:
                month = "Month not found";
                break;
        }
        // Return the value to pass to the target.
        return month;
    }

    // ConvertBack is not implemented for a OneWay binding.
    public object ConvertBack(object value, Type targetType, 
        object parameter, string language)
    {
        throw new NotImplementedException();
    }
}

以下是如何在绑定对象标记中使用该值转换器的方法。

<UserControl.Resources>
  <local:DateToStringConverter x:Key="Converter1"/>
</UserControl.Resources>
...
<TextBlock Grid.Column="0" 
  Text="{x:Bind ViewModel.Month, Converter={StaticResource Converter1}}"/>
<TextBlock Grid.Column="0" 
  Text="{Binding Month, Converter={StaticResource Converter1}}"/>

如果为绑定定义了 Converter 参数,绑定引擎将调用 ConvertConvertBack 方法。 从源传递数据时,绑定引擎调用 Convert 并将返回的数据传递到目标。 从目标(双向绑定)传递数据时,绑定引擎将调用 ConvertBack 并传递返回的数据到源。

转换器还具有可选参数: ConverterLanguage,它允许指定转换中使用的语言,以及允许传递转换逻辑的参数的 ConverterParameter。 有关使用转换器参数的示例,请参阅 IValueConverter

注释

如果转换中有错误,请不要引发异常。 相反,返回 DependencyProperty.UnsetValue,这将停止数据传输。

若要在无法解析绑定源时显示要使用的默认值,请在标记中设置 FallbackValue 绑定对象的属性。 这可用于处理转换和格式设置错误。 将绑定到可能在异类类型集合中某些对象不存在的源属性也很有用。

如果将文本控件绑定到不是字符串的值,数据绑定引擎会将该值转换为字符串。 如果值为引用类型,数据绑定引擎将通过调用 ICustomPropertyProvider.GetStringRepresentation 或 IStringable.ToString 来检索字符串值(如果可用),否则将调用 Object.ToString 但是,请注意,绑定引擎将忽略隐藏基类实现的任何 ToString 实现。 子类实现应替代基类 ToString 方法。 同样,在本机语言中,所有托管对象似乎都实现了 ICustomPropertyProviderIStringable。 但是,所有对GetStringRepresentationIStringable.ToString的调用都被路由到Object.ToString或该方法的重写,而不会路由到隐藏基类实现的新ToString实现。

注释

Windows 社区工具包提供 BoolToVisibilityConverter。 转换器将true映射为Visible枚举值,并将false映射为Collapsed,这样您无需创建转换器即可将Visibility属性绑定到布尔值。 若要使用转换器,project必须添加 CommunityToolkit.WinUI.Converters NuGet 包。

{x:Bind} 中的函数绑定

{x:Bind} 使绑定路径中的最后一步成为函数。 使用此功能执行转换或创建依赖于多个属性的绑定。 有关详细信息,请参阅 x:Bind 中的函数

元素之间的绑定

可以将一个 XAML 元素的属性绑定到另一个 XAML 元素的属性。 以下是在标记中该绑定的示例。

<TextBox x:Name="myTextBox" />
<TextBlock Text="{x:Bind myTextBox.Text, Mode=OneWay}" />

具有 {x:Bind} 的资源字典

{x:Bind} 标记扩展依赖于代码生成,因此它需要一个代码隐藏文件,其中包含调用InitializeComponent的构造函数(初始化生成的代码)。 若要重用资源字典,请实例化其类型(以便调用InitializeComponent),而不是引用其文件名。 这里有一个示例,说明如果您有现有的资源字典并想在其中使用 {x:Bind},应该怎么做。

<!-- TemplatesResourceDictionary.xaml -->
<ResourceDictionary
    x:Class="ExampleNamespace.TemplatesResourceDictionary"
    .....
    xmlns:examplenamespace="using:ExampleNamespace">
    
    <DataTemplate x:Key="EmployeeTemplate" x:DataType="examplenamespace:IEmployee">
        <Grid>
            <TextBlock Text="{x:Bind Name}"/>
        </Grid>
    </DataTemplate>
</ResourceDictionary>
// TemplatesResourceDictionary.xaml.cs
using Microsoft.UI.Xaml.Data;
 
namespace ExampleNamespace
{
    public partial class TemplatesResourceDictionary
    {
        public TemplatesResourceDictionary()
        {
            InitializeComponent();
        }
    }
}
<!-- MainWindow.xaml -->
<Window x:Class="ExampleNamespace.MainWindow"
    ....
    xmlns:examplenamespace="using:ExampleNamespace">

    <Window.Resources>
        <ResourceDictionary>
            .... 
            <ResourceDictionary.MergedDictionaries>
                <examplenamespace:TemplatesResourceDictionary/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Window.Resources>
</Window>

在可重用样式中混合 {x:Bind} 和 {Binding}

前面的示例演示了如何在 DataTemplates 中使用 {x:Bind} 。 还可以创建组合 {x:Bind}{Binding} 标记扩展的可重用样式。 如果需要将某些属性绑定到编译时已知的值,请使用 {x:Bind},而将其他属性绑定到运行时 DataContext 的值,请使用 {Binding}

以下示例演示如何创建使用两种绑定方法的可重用按钮样式:

TemplatesResourceDictionary.xaml

<!-- TemplatesResourceDictionary.xaml -->
<ResourceDictionary
    x:Class="ExampleNamespace.TemplatesResourceDictionary"
    .....
    xmlns:examplenamespace="using:ExampleNamespace">
    
    <!-- DataTemplate using x:Bind -->
    <DataTemplate x:Key="EmployeeTemplate" x:DataType="examplenamespace:IEmployee">
        <Grid>
            <TextBlock Text="{x:Bind Name}"/>
        </Grid>
    </DataTemplate>
    
    <!-- Style that mixes x:Bind and Binding -->
    <Style x:Key="CustomButtonStyle" TargetType="Button">
        <Setter Property="Background" Value="{Binding ButtonBackgroundBrush}"/>
        <Setter Property="Foreground" Value="{Binding ButtonForegroundBrush}"/>
        <Setter Property="FontSize" Value="16"/>
        <Setter Property="Margin" Value="4"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="Button">
                    <Border x:Name="RootBorder"
                            Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}"
                            CornerRadius="4">
                        <StackPanel Orientation="Horizontal" 
                                    HorizontalAlignment="Center"
                                    VerticalAlignment="Center">
                            <!-- x:Bind to a static property or page-level property -->
                            <Ellipse Width="8" Height="8" 
                                     Fill="{x:Bind DefaultIndicatorBrush}" 
                                     Margin="0,0,8,0"/>
                            <!-- Binding to DataContext -->
                            <ContentPresenter x:Name="ContentPresenter"
                                              Content="{TemplateBinding Content}"
                                              Foreground="{TemplateBinding Foreground}"
                                              FontSize="{TemplateBinding FontSize}"/>
                        </StackPanel>
                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup x:Name="CommonStates">
                                <VisualState x:Name="Normal"/>
                                <VisualState x:Name="PointerOver">
                                    <VisualState.Setters>
                                        <!-- Binding to DataContext for hover color -->
                                        <Setter Target="RootBorder.Background" 
                                                Value="{Binding ButtonHoverBrush}"/>
                                    </VisualState.Setters>
                                </VisualState>
                                <VisualState x:Name="Pressed">
                                    <VisualState.Setters>
                                        <!-- x:Bind to a compile-time known resource -->
                                        <Setter Target="RootBorder.Background" 
                                                Value="{x:Bind DefaultPressedBrush}"/>
                                    </VisualState.Setters>
                                </VisualState>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

TemplatesResourceDictionary.xaml.cs

// TemplatesResourceDictionary.xaml.cs
using Microsoft.UI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Media;
 
namespace ExampleNamespace
{
    public partial class TemplatesResourceDictionary
    {
        public TemplatesResourceDictionary()
        {
            InitializeComponent();
        }
        
        // Properties for x:Bind - these are compile-time bound
        public SolidColorBrush DefaultIndicatorBrush { get; } = 
            new SolidColorBrush(Colors.Green);
            
        public SolidColorBrush DefaultPressedBrush { get; } = 
            new SolidColorBrush(Colors.DarkGray);
    }
}

MainWindow.xaml 中的用法,其中包含提供运行时值的 ViewModel:

<!-- MainWindow.xaml -->
<Window x:Class="ExampleNamespace.MainWindow"
    ....
    xmlns:examplenamespace="using:ExampleNamespace">

    <Window.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <examplenamespace:TemplatesResourceDictionary/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Window.Resources>

    <Grid>
        <Grid.DataContext>
            <examplenamespace:ButtonThemeViewModel/>
        </Grid.DataContext>
        
        <StackPanel Margin="20">
            <!-- These buttons use the mixed binding style -->
            <Button Content="Save" Style="{StaticResource CustomButtonStyle}"/>
            <Button Content="Cancel" Style="{StaticResource CustomButtonStyle}"/>
        </StackPanel>
    </Grid>
</Window>

ButtonThemeViewModel.cs(提供运行时绑定值的 DataContext):

using System.ComponentModel;
using Microsoft.UI;
using Microsoft.UI.Xaml.Media;

namespace ExampleNamespace
{
    public class ButtonThemeViewModel : INotifyPropertyChanged
    {
        private SolidColorBrush _buttonBackgroundBrush = new SolidColorBrush(Colors.LightBlue);
        private SolidColorBrush _buttonForegroundBrush = new SolidColorBrush(Colors.DarkBlue);
        private SolidColorBrush _buttonHoverBrush = new SolidColorBrush(Colors.LightCyan);

        public SolidColorBrush ButtonBackgroundBrush
        {
            get => _buttonBackgroundBrush;
            set
            {
                _buttonBackgroundBrush = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ButtonBackgroundBrush)));
            }
        }

        public SolidColorBrush ButtonForegroundBrush
        {
            get => _buttonForegroundBrush;
            set
            {
                _buttonForegroundBrush = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ButtonForegroundBrush)));
            }
        }

        public SolidColorBrush ButtonHoverBrush
        {
            get => _buttonHoverBrush;
            set
            {
                _buttonHoverBrush = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ButtonHoverBrush)));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }
}

在本示例中:

  • {Binding} 用于依赖于 DataContext 的属性(ButtonBackgroundBrush、ButtonForegroundBrush、ButtonHoverBrush)
  • {x:Bind} 用于编译时已知且属于 ResourceDictionary 本身的属性(DefaultIndicatorBrush、DefaultPressedBrush)
  • 该样式是可重用的,你可以将其应用于任何按钮
  • 运行时主题化可以通过 DataContext 实现,同时仍可以从静态元素的性能中受益{x:Bind}

事件绑定和ICommand

{x:Bind} 支持称为事件绑定的功能。 使用此功能,可以使用绑定为事件指定处理程序。 此功能是用于处理事件的一个额外选项,除了在后台代码文件中使用方法处理事件之外。 假设你在ListViewDoubleTapped类中有一个MainWindow事件处理程序。

public sealed partial class MainWindow : Window
{
    ...
    public void ListViewDoubleTapped()
    {
        // Handle double-tapped logic
    }
}

可以将 ListView 的 DoubleTapped 事件绑定到 MainWindow 中的方法,如下所示。

<ListView DoubleTapped="{x:Bind ListViewDoubleTapped}" />

不能使用重载的方法处理具有此技术的事件。 此外,如果处理事件的方法具有参数,则必须从事件的所有参数的类型中分别分配所有这些参数。 在这种情况下, ListViewDoubleTapped 不会重载,并且它没有参数(但即使它采取了两 object 个参数,它仍然有效)。

事件绑定技术类似于实现和使用命令。 命令是返回实现 ICommand 接口的对象的属性。 {x:Bind}{Binding} 都使用命令。 因此,无需多次实现命令模式,可以使用在 DelegateCommand UWP 示例(位于“Common”文件夹中)中找到的 帮助程序类。

绑定到一组文件夹或文件

可以使用 Windows.Storage 命名空间中的 API 来检索打包的 Windows App SDK 应用中的文件夹和文件数据。 但是,各种GetFilesAsyncGetFoldersAsync方法和GetItemsAsync方法不返回适合绑定到列表控件的值。 相反,必须绑定到 FileInformationFactory 类的 GetVirtualizedFilesVectorGetVirtualizedFoldersVectorGetVirtualizedItemsVector 方法的返回值。 StorageDataSource 和 GetVirtualizedFilesVector UWP 示例的以下代码示例显示了典型的使用模式。 请记住在应用包清单中声明 picturesLibrary 功能,并确认图片库文件夹中有图片。

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    var library = Windows.Storage.KnownFolders.PicturesLibrary;
    var queryOptions = new Windows.Storage.Search.QueryOptions();
    queryOptions.FolderDepth = Windows.Storage.Search.FolderDepth.Deep;
    queryOptions.IndexerOption = Windows.Storage.Search.IndexerOption.UseIndexerWhenAvailable;

    var fileQuery = library.CreateFileQueryWithOptions(queryOptions);

    var fif = new Windows.Storage.BulkAccess.FileInformationFactory(
        fileQuery,
        Windows.Storage.FileProperties.ThumbnailMode.PicturesView,
        190,
        Windows.Storage.FileProperties.ThumbnailOptions.UseCurrentScale,
        false
        );

    var dataSource = fif.GetVirtualizedFilesVector();
    this.PicturesListView.ItemsSource = dataSource;
}

通常使用此方法创建文件和文件夹信息的只读视图。 可以创建对文件和文件夹属性的双向绑定,例如让用户在音乐视图中对歌曲进行评分。 但是,在调用适当的 SavePropertiesAsync 方法(例如,MusicProperties.SavePropertiesAsync)之前,不会保留任何更改。 当项目失去焦点时,应提交更改,因为此作会触发选择重置。

请注意,使用此技术的双向绑定仅适用于索引位置,如 音乐。 可以通过调用 FolderInformation.GetIndexedStateAsync 方法来确定位置是否已编制索引。

另请注意,虚拟化向量在填充某些项目的值之前,可以返回 null。 例如,在使用绑定到虚拟化向量的列表控件的null值之前,您应该检查,或者改用SelectedIndex

绑定到按关键分组的数据

如果您采用一个平面集合的项目(例如用 BookSku 类表示的书籍),并使用一个公共属性(例如 BookSku.AuthorName 属性)作为键来对项目进行分组,那么结果称为分组数据。 对数据进行分组时,它不再是平面集合。 分组数据是组对象的集合,其中每个组对象都有:

  • 关键和
  • 其属性与该键匹配的项的集合。

若要再次以书籍为例,按作者名称对书籍进行分组的结果将生成每个组具有的作者名称组的集合:

  • 一个关键,即作者名,以及
  • 一个对象的集合 BookSku ,其 AuthorName 属性与组的键匹配。

通常,若要显示集合,可以将项控件(如 ListViewGridView)的 ItemsSource 直接绑定到返回集合的属性。 如果这是一组平面的项,则无需做任何特殊的事情。 但是,如果它是组对象的集合(就像绑定到分组数据时一样),则需要一个名为 CollectionViewSource 的中间对象的服务,该对象位于项控件和绑定源之间。 您将CollectionViewSource绑定到返回分组数据的属性,并将项控件绑定到CollectionViewSource。 附加价值 CollectionViewSource 是它记录当前项,因此可以通过将它们全部绑定到相同的 CollectionViewSource 来使多个项目保持同步。 还可以通过CollectionViewSource.View 属性返回对象的ICollectionView.CurrentItem 属性以编程方式访问当前项。

若要激活 CollectionViewSource 的分组工具,请将 IsSourceGrouped 设置为 true。 是否需要设置ItemsPath 属性取决于您如何定义组对象。 可通过两种方式创建组对象:“is-a-group”模式和“has-a-group”模式。 在“is-a-group”模式中,组对象派生自集合类型(例如 List<T>),因此组对象实际上是项组本身。 使用此模式,无需设置 ItemsPath。 在“has-a-group”模式中,组对象具有集合类型的一个或多个属性(例如 List<T>),因此组“具有”一组属性形式的项(或多个属性形式的多个项组)。 使用此模式时,需要将 ItemsPath 设置为包含项目组的属性名称。

以下示例演示了“has-a-group”模式。 窗口类具有名为 DataContext 的属性,该属性返回视图模型的实例。 CollectionViewSource 绑定到Authors视图模型的属性(Authors是组对象的集合),并指定它是Author.BookSkus包含分组项的属性。 最后,GridView 绑定到 CollectionViewSource,并定义了其组样式,这样它可以在组中呈现项。

<Window.Resources>
    <CollectionViewSource
    x:Name="AuthorHasACollectionOfBookSku"
    Source="{x:Bind ViewModel.Authors}"
    IsSourceGrouped="true"
    ItemsPath="BookSkus"/>
</Window.Resources>
...
<GridView
ItemsSource="{x:Bind AuthorHasACollectionOfBookSku}" ...>
    <GridView.GroupStyle>
        <GroupStyle
            HeaderTemplate="{StaticResource AuthorGroupHeaderTemplateWide}" ... />
    </GridView.GroupStyle>
</GridView>

可以通过两种方式之一实现“属于一个组”的模式。 一种方法是编写您自己的群组类。 从 List<T> 中派生类(其中 T 是项的类型)。 例如,public class Author : List<BookSku>。 第二种方法是使用 LINQ 表达式,从 BookSku 项目中具有相似属性值的项目动态创建组对象(以及组类)。 此方法(仅维护一个平面列表并动态将它们分组在一起)是访问云服务数据的应用的典型方法。 你可以灵活地按作者或流派(例如)对书籍进行分组,而无需特殊的组类,例如 作者流派

以下示例演示了使用 LINQ 的“is-a-group”模式。 这一次,我们按流派对书籍进行分组,并在组标题中以流派名称显示。 此分组通过引用组Key值的 "Key" 属性路径来指示。

using System.Linq;
...
private IOrderedEnumerable<IGrouping<string, BookSku>> genres;

public IOrderedEnumerable<IGrouping<string, BookSku>> Genres
{
    get
    {
        if (genres == null)
        {
            genres = from book in bookSkus
                     group book by book.genre into grp
                     orderby grp.Key
                     select grp;
        }
        return genres;
    }
}

请记住,将 {x:Bind} 与数据模板配合使用时,需要通过设置 x:DataType 值来指示要绑定到的类型。 如果类型为泛型,则不能在标记中表示,因此需要在组样式标头模板中使用 {Binding}

    <Grid.Resources>
        <CollectionViewSource x:Name="GenreIsACollectionOfBookSku"
        Source="{x:Bind Genres}"
        IsSourceGrouped="true"/>
    </Grid.Resources>
    <GridView ItemsSource="{x:Bind GenreIsACollectionOfBookSku}">
        <GridView.ItemTemplate x:DataType="local:BookTemplate">
            <DataTemplate>
                <TextBlock Text="{x:Bind Title}"/>
            </DataTemplate>
        </GridView.ItemTemplate>
        <GridView.GroupStyle>
            <GroupStyle>
                <GroupStyle.HeaderTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding Key}"/>
                    </DataTemplate>
                </GroupStyle.HeaderTemplate>
            </GroupStyle>
        </GridView.GroupStyle>
    </GridView>

SemanticZoom 控件是用户查看和导航分组数据的好方法。 Bookstore2 UWP 示例应用演示了如何使用 SemanticZoom。 在该应用中,可以查看按作者分组的书籍列表(放大视图),也可以缩小以查看作者的跳转列表(缩小视图)。 跳转列表提供比滚动浏览书籍列表更快得多的导航。 放大和缩小视图实际上是 ListViewGridView 控件,绑定到相同的 CollectionViewSource

SemanticZoom 的插图

绑定到分层数据(如类别中的子类别)时,可以选择使用一系列项控件在 UI 中显示分层级别。 一个项目控件中的选定内容决定了后续项控件的内容。 可以通过将每个列表绑定到其自己的CollectionViewSource,并将CollectionViewSource实例按链式结构绑定在一起,从而保持列表同步。 此设置称为大纲/详细信息(或列表/详细信息)视图。 有关详细信息,请参阅 如何绑定到分层数据并创建主视图/详细信息视图

诊断和调试数据绑定问题

绑定标记包含属性的名称(对于 C#,有时为字段和方法)。 因此,重命名属性时,还需要更改引用它的任何绑定。 如果你忘记执行该操作,你就会导致数据绑定错误,结果可能是应用程序无法编译或运行不正常。

{x:Bind}{Binding} 创建的绑定对象基本上等效。 但 {x:Bind} 具有绑定源的类型信息,并在编译时生成源代码。 使用 {x:Bind} 时,可以获得与代码的其他部分相同类型的问题检测。 该检测包括对绑定表达式的编译时验证,以及通过在生成的作为页面分部类的源代码中设置断点来进行调试。 可以在文件夹中的文件 obj 中找到这些类,其名称如 (对于 C#) <view name>.g.cs)。 如果绑定出现问题,请在 Microsoft Visual Studio 调试器中打开 Break On Unhandled Exceptions。 调试器在该时间点中断执行,然后可以调试出问题所在。 生成的 {x:Bind} 代码遵循绑定源节点图的每个部分的相同模式,你可以使用 “调用堆栈” 窗口中的信息来帮助确定导致问题的调用序列。

{Binding} 没有绑定源的类型信息。 在使用附加的调试器运行应用时,任何绑定错误都会显示在 Visual Studio 的OutputXAML 绑定失败窗口中。 有关在 Visual Studio 中调试绑定错误的详细信息,请参阅 XAML 数据绑定诊断

在代码中创建绑定

注释

此部分仅适用于 {Binding},因为无法在代码中创建 {x:Bind} 绑定。 不过,可以使用 {x:Bind} 来实现 的某些相同优势,这样您就可以在任何依赖属性上注册更改通知。

还可以使用过程代码而不是 XAML 将 UI 元素连接到数据。 要做到这一点,请创建一个新的 Binding 对象,设置相应的属性,然后调用 FrameworkElement.SetBindingBindingOperations.SetBinding。 如果要在运行时选择绑定属性值或在多个控件之间共享单个绑定,则以编程方式创建绑定非常有用。 调用SetBinding后无法更改绑定属性值。

以下示例演示如何在代码中实现绑定。

<TextBox x:Name="MyTextBox" Text="Text"/>
// Create an instance of the MyColors class 
// that implements INotifyPropertyChanged.
var textcolor = new MyColors();

// Brush1 is set to be a SolidColorBrush with the value Red.
textcolor.Brush1 = new SolidColorBrush(Colors.Red);

// Set the DataContext of the TextBox MyTextBox.
MyTextBox.DataContext = textcolor;

// Create the binding and associate it with the text box.
var binding = new Binding { Path = new PropertyPath("Brush1") };
MyTextBox.SetBinding(TextBox.ForegroundProperty, binding);

{x:Bind} 和 {Binding} 功能比较

功能 / 特点 {x:Bind} 与 {Binding} 注释
路径是默认属性 {x:Bind a.b.c}
-
{Binding a.b.c}
Path 属性 {x:Bind Path=a.b.c}
-
{Binding Path=a.b.c}
默认情况下,x:Bind 是以 Window 为根节点,而不是以 DataContext。
Indexer {x:Bind Groups[2].Title}
-
{Binding Groups[2].Title}
绑定到集合中的指定项。 仅支持基于整数的索引。
附加属性 {x:Bind Button22.(Grid.Row)}
-
{Binding Button22.(Grid.Row)}
附加属性是使用括号指定的。 如果未在 XAML 命名空间中声明该属性,请使用 XML 命名空间作为前缀,该命名空间应映射到文档头的代码命名空间。
铸造 {x:Bind groups[0].(data:SampleDataGroup.Title)}
-
{Binding}不需要。
类型转换是使用括号指定的。 如果未在 XAML 命名空间中声明该属性,请使用 XML 命名空间作为前缀,该命名空间应映射到文档头的代码命名空间。
转换器 {x:Bind IsShown, Converter={StaticResource BoolToVisibility}}
-
{Binding IsShown, Converter={StaticResource BoolToVisibility}}
在 Window、Control、ResourceDictionary 或 App.xaml 的根目录中声明转换器。
转换器参数、转换器语言 {x:Bind IsShown, Converter={StaticResource BoolToVisibility}, ConverterParameter=One, ConverterLanguage=fr-fr}
-
{Binding IsShown, Converter={StaticResource BoolToVisibility}, ConverterParameter=One, ConverterLanguage=fr-fr}
在 Window、Control、ResourceDictionary 或 App.xaml 的根目录中声明转换器。
TargetNullValue {x:Bind Name, TargetNullValue=0}
-
{Binding Name, TargetNullValue=0}
当绑定表达式的叶为 null 时使用。 对字符串值使用单引号。
FallbackValue {x:Bind Name, FallbackValue='empty'}
-
{Binding Name, FallbackValue='empty'}
当绑定路径的任何部分(叶除外)为 null 时使用。
ElementName {x:Bind slider1.Value}
-
{Binding Value, ElementName=slider1}
使用{x:Bind}绑定到字段;默认情况下,Path根植于窗口,因此可以通过其字段访问任何命名的元素。
相对来源:自身 <Rectangle x:Name="rect1" Width="200" Height="{x:Bind rect1.Width}" ... />
-
<Rectangle Width="200" Height="{Binding Width, RelativeSource={RelativeSource Self}}" ... />
使用 {x:Bind} 命名元素,并在 Path 中使用其名称。
RelativeSource:TemplatedParent 不需要 {x:Bind}
-
{Binding <path>, RelativeSource={RelativeSource TemplatedParent}}
使用 {x:Bind}时, TargetType 指示 ControlTemplate 绑定到模板父级。 {Binding} 的控件模板中,大多数情况下可以使用常规模板绑定。 但在需要使用转换器或双向绑定的地方使用 TemplatedParent
来源 不需要 {x:Bind}
-
<ListView ItemsSource="{Binding Orders, Source={StaticResource MyData}}"/>
对于 {x:Bind},你可以直接使用命名元素、属性或静态路径。
Mode {x:Bind Name, Mode=OneWay}
-
{Binding Name, Mode=TwoWay}
Mode 可以是 OneTimeOneWay也可以 TwoWay{x:Bind} 默认为 OneTime{Binding} 默认为 OneWay
UpdateSourceTrigger {x:Bind Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}
-
{Binding UpdateSourceTrigger=PropertyChanged}
UpdateSourceTrigger 可以是 DefaultLostFocus也可以 PropertyChanged{x:Bind} 不支持 UpdateSourceTrigger=Explicit{x:Bind}在除PropertyChanged的所有情况下使用TextBox.Text行为,而在PropertyChanged的情况下使用行为。

另请参阅