深入的 Windows 数据绑定

在本文中,我们将介绍驻留在 Microsoft.UI.Xaml.Data 命名空间中的 API 的 Windows 应用 SDK 数据绑定功能。

注释

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

重要 API

介绍

数据绑定是应用 UI 显示数据的一种方法,还可以选择与该数据保持同步。 通过数据绑定,可以将数据关注与 UI 的关注区分开,这会导致更简单的概念模型,以及应用的可读性、可测试性和可维护性。

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

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

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

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

演示 {Binding} 的 UWP 示例应用

每个绑定都涉及这些部分

  • 绑定源。 这是绑定的数据源,它可以是具有要在 UI 中显示的值的任何类的实例。
  • 绑定目标。 这是 UI 中显示数据的 FrameworkElementDependencyProperty
  • 绑定对象。 这是将数据值从源传输到目标以及从目标传回源的片段。 绑定对象是在 XAML 加载时从 {x:Bind}{Binding} 标记扩展创建的。

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

绑定源

下面是一个非常基本的类实现,我们可以将其用作绑定源。

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

    public string NextButtonText { get; set; }
}

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

这样做的一种方法是从 DependencyObject 派生表示绑定源的类,并通过 DependencyProperty 公开数据值。 这就是 FrameworkElement 的可观测性。 A FrameworkElement 是现装好的绑定源。

实现 System.ComponentModel.INotifyPropertyChanged 是使类可观察的更轻量的方法,以及已有基类的类的必要方法。 这实际上只涉及实现名为 的单个事件。 下面是使用 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#,则无需实现上面显示的模式,则可以从 BindableBaseQuizGame 示例(在“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> IInspectableIBindableObservableVectorIVector。 请参阅 XAML 项控件;使用 C++/WinRT 绑定到 C++/WinRT 集合集合
从绑定集合获取集合更改通知。 ObservableCollection<T> IInspectableIObservableVector。 例如 ,winrt::single_threaded_observable_vector<T>
实现支持绑定的集合。 扩展 列表<T> 或实现 IListIList<对象>、 IEnumerableIEnumerable<对象>。 绑定到泛型 IList<T>IEnumerable<T> 不受支持。 实现 IInspectableIVector。 请参阅 XAML 项控件;使用 C++/WinRT 绑定到 C++/WinRT 集合集合
实现支持集合更改通知的集合。 扩展 ObservableCollection<T> 或实现 (非泛型) IListINotifyCollectionChanged 实现 IInspectableIBindableObservableVector 的 IObservableVector
实现支持增量加载的集合。 扩展 ObservableCollection<T> 或实现 (非泛型) IListINotifyCollectionChanged。 此外,实现 ISupportIncrementalLoading 实现 IInspectableIBindableObservableVector 的 IObservableVector 此外,实现 ISupportIncrementalLoading

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

绑定目标

在下面的两个示例中,属性 Button.Content 是绑定目标,其值设置为声明绑定对象的标记扩展。 首先显示 {x:Bind} ,然后显示 {Binding}。 在标记中声明绑定是一种常见情况(方便、可读和可工具)。 但是,如果需要,可以避免标记和命令性地(以编程方式)创建 Binding 类的实例。

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

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

重要

如果使用 C++/WinRT,则 BindableAttribute 属性可用于 Windows 应用 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 失去焦点时,对 TextBox.Text 的更改将发送到双向绑定源,而不是在每个用户击键之后。

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 中的弱类型对象

例如,假设你有一个命名 的类型,它实现名为 的字符串属性。 并且你有一个属性,该属性 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 生成的代码包括窗口 的 Loading 事件的处理程序,该处理程序对表示窗口绑定的生成的类调用 Initialize 该方法。 Initialize 反过来,调用 Update 开始在绑定源和目标之间移动数据。 Loading 是在窗口或用户控件的第一个度量值传递之前引发的。 因此,如果数据以异步方式加载,则调用数据时可能无法准备就绪 Initialize 。 因此,加载数据后,可以通过调用 this.Bindings.Update();强制初始化一次性绑定。 如果只需要异步加载数据的一次性绑定,那么以这种方式初始化它们比采用单向绑定和侦听更改要便宜得多。 如果数据未进行精细更改,并且可能作为特定作的一部分进行更新,则可以一次性进行绑定,并随时通过调用 Update强制手动更新。

注释

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

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

如果使用 C++/WinRT,若要使用 {Binding} 标记扩展,则需要将 BindableAttribute 属性添加到要绑定到的任何运行时类。 若要使用 {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 应用 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 会自动设置为正在模板化的数据对象。 以下示例可用作 ItemTemplate 绑定到具有命名 Title 字符串属性的 Description任意类型的集合的项控件的示例。

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

注释

默认情况下,当 TextBox 失去焦点时,TextBox.Text 的更改将发送到双向绑定源。 若要在每个用户击键后发送更改,请设置为UpdateSourceTriggerPropertyChanged标记中的绑定。 还可以通过设置为 </gt; 完全控制何时将更改发送到源。 然后处理文本框(通常是 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。 转换器映射到trueVisible枚举值,falseCollapsed以便无需创建转换器即可将属性绑定到Visibility布尔值。 若要使用转换器,项目必须添加 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} 使用 {Binding}其他属性将其他属性绑定到运行时 DataContext 值时,这非常有用。

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

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} 都使用命令。 因此,无需多次实现命令模式,可以使用 DelegateCommandQuizGame UWP 示例(在“Common”文件夹中)中找到的帮助程序类。

绑定到文件夹或文件的集合

可以使用 Windows.Storage 命名空间中的 API 检索打包的 Windows 应用 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>

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

下面的示例演示了使用 LINQ 的“is-a-group”模式。 这一次,我们按流派对书籍进行分组,并在组标题中以流派名称显示。 这由对组键值的引用中的“ 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#,有时为字段和方法)。 因此,重命名属性时,还需要更改引用它的任何绑定。 忘记这样做会导致数据绑定 bug 的典型示例,你的应用要么无法编译,要么无法正确运行。

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

{Binding} 没有绑定源的类型信息。 但是,当你使用附加的调试器运行应用时,Visual Studio 的 “输出 ”和 “XAML 绑定失败 ”窗口中会显示任何绑定错误。 有关在 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:BindPath 在 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 中声明。
ConverterParameter、ConverterLanguage {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 默认位于 Window,因此可以通过其字段访问任何命名元素。
RelativeSource:Self <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所有情况使用LostFocus行为。

另请参阅