已编译的绑定

浏览示例。 浏览示例

.NET Multi-platform App UI (.NET MAUI) 数据绑定有两个主要问题:

  1. 绑定表达式不具有编译时验证。 相反,绑定在运行时解析。 因此,在应用程序未按预期运行或出现错误消息时,直到运行时才会检测到任何无效绑定。
  2. 它们不具有成本效益。 使用常规对象检查(反射)在运行时解析绑定,并且执行此操作的开销因平台而异。

已编译的绑定通过在编译时而不是运行时解析绑定表达式来改善 .NET MAUI 应用程序中的数据绑定性能。 此外,绑定表达式的这种编译时验证可以提供更好的开发人员故障排除体验,因为无效绑定会被报告为生成错误。

重要

在 NativeAOT 应用和启用了完全修整功能的应用中,需要使用编译绑定,而不是基于字符串的绑定。

XAML 中已编译的绑定

若要使用已编译的绑定,需将 VisualElement 上的 x:DataType 属性设置为 VisualElement 及其子级将绑定到的对象的类型。 建议在设置 BindingContext 的视图层次结构中将 x:DataType 属性设置为相同级别。 但可以在视图层次结构中的任意位置重新定义此属性。

注意

已编译的绑定需要使用 XAML 编译,该编译在 .NET MAUI 中默认处于启用状态。 如果已禁用 XAML 编译,则需要启用它。 有关详细信息,请参阅 XAML 编译

要在 XAML 中使用已编译的绑定,x:DataType 特性必须设置为字符串文字或使用 x:Type 标记扩展的类型。 在 XAML 编译时,会将任何无效绑定表达式报告为生成错误。 但是,XAML 编译器仅报告遇到的第一个无效绑定表达式的生成错误。 无论是在 XAML 还是代码中设置 BindingContext,都将编译在 VisualElement 或其子元素上定义的任何有效绑定表达式。 编译绑定表达式会生成编译代码,该代码将从源上的属性获取值,并将在标记中指定的目标的属性上对其进行设置。 此外,根据绑定表达式,生成的代码可能会观察到源属性值的更改并刷新目标属性,并可能会将更改从目标推送回源

重要

已针对定义 Source 属性的任何 XAML 绑定表达式禁用已编译的绑定。 这是因为 Source 属性始终使用 x:Reference 标记扩展进行设置,该设置在编译时无法解析。

此外,多绑定目前不支持 XAML 中已编译的绑定。

默认情况下,除非为应用启用了 NativeAOT,否则 .NET MAUI 不会为不使用已编译绑定的绑定生成警告。 但是,可以在应用程序的项目文件(*.csproj)中将 $(MauiStrictXamlCompilation) 生成属性设置为 true,从而选择生成编译绑定警告:

<MauiStrictXamlCompilation>true</MauiStrictXamlCompilation>

使用 XAML 中已编译的绑定

以下示例演示了如何在 .NET MAUI 视图和 viewmodel 属性之间使用已编译的绑定:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:DataBindingDemos"
             x:Class="DataBindingDemos.CompiledColorSelectorPage"
             x:DataType="local:HslColorViewModel"
             Title="Compiled Color Selector">
    <ContentPage.BindingContext>
        <local:HslColorViewModel Color="Sienna" />
    </ContentPage.BindingContext>
    ...
    <StackLayout>
        <BoxView Color="{Binding Color}"
                 ... />
        <StackLayout Margin="10, 0">
            <Label Text="{Binding Name}" />
            <Slider Value="{Binding Hue}" />
            <Label Text="{Binding Hue, StringFormat='Hue = {0:F2}'}" />
            <Slider Value="{Binding Saturation}" />
            <Label Text="{Binding Saturation, StringFormat='Saturation = {0:F2}'}" />
            <Slider Value="{Binding Luminosity}" />
            <Label Text="{Binding Luminosity, StringFormat='Luminosity = {0:F2}'}" />
        </StackLayout>
    </StackLayout>    
</ContentPage>

ContentPage 实例化 HslColorViewModel,并实例化 BindingContext 属性的属性元素标记中的 Color 属性。 ContentPage 还将 x:DataType 属性定义为 viewmodel 类型,指示将编译 ContentPage 视图层次结构中的任何绑定表达式。 通过更改任何绑定表达式以绑定到不存在的 viewmodel 属性,可对此进行验证,这将导致生成错误。 尽管此示例将 x:DataType 特性设置为字符串文字,但也可以将其设置为具有 x:Type 标记扩展的类型。 有关 x:Type 标记扩展的详细信息,请参阅 x:Type 标记扩展

重要

可以在视图层次结构中的任意点重新定义 x:DataType 属性。

BoxViewLabel 元素和 Slider 视图从 ContentPage 继承绑定上下文。 这些视图都是引用 viewmodel 中的源属性的绑定目标。 对于 BoxView.Color 属性和 Label.Text 属性,数据绑定为 OneWay - 视图中的属性根据 viewmodel 中的属性进行设置。 但是,Slider.Value 属性使用 TwoWay 绑定。 此模式允许从 viewmodel 设置每个 Slider,也允许从每个 Slider 设置 viewmodel。

首次运行该示例时,BoxViewLabel 元素和 Slider 元素均根据实例化 viewmodel 时设置的初始 Color 属性从 viewmodel 进行设置。 操作滑块时,BoxViewLabel 元素会相应更新:

已编译的颜色选择器。

有关此颜色选择器的详细信息,请参阅 viewmodel 和属性更改通知

在 DataTemplate 中,使用 XAML中已编译的绑定

DataTemplate 中的绑定在模板化的对象的上下文中进行解释。 因此,在 DataTemplate 中使用已编译的绑定时,DataTemplate 需要使用 x:DataType 属性来声明其数据对象的类型。

以下示例演示了如何在 DataTemplate 中使用已编译的绑定:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:DataBindingDemos"
             x:Class="DataBindingDemos.CompiledColorListPage"
             Title="Compiled Color List">
    <Grid>
        ...
        <ListView x:Name="colorListView"
                  ItemsSource="{x:Static local:NamedColor.All}"
                  ... >
            <ListView.ItemTemplate>
                <DataTemplate x:DataType="local:NamedColor">
                    <ViewCell>
                        <StackLayout Orientation="Horizontal">
                            <BoxView Color="{Binding Color}"
                                     ... />
                            <Label Text="{Binding FriendlyName}"
                                   ... />
                        </StackLayout>
                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
        <!-- The BoxView doesn't use compiled bindings -->
        <BoxView Color="{Binding Source={x:Reference colorListView}, Path=SelectedItem.Color}"
                 ... />
    </Grid>
</ContentPage>

ListView.ItemsSource 属性设置为静态 NamedColor.All 属性。 NamedColor 类使用 .NET 反射枚举 Colors 类中的所有静态公共字段,并将它们及其名称存储在可从静态 All 属性访问的集合中。 因此,ListView 由所有 NamedColor 实例填充。 对于 ListView 中的每个项,项的绑定上下文都被设置为 NamedColor 对象。 ViewCell 中的 BoxViewLabel 元素被绑定到 NamedColor 属性。

DataTemplatex:DataType 特性定义为 NamedColor 类型,指示将编译 DataTemplate 视图层次结构中的任何绑定表达式。 通过更改任何绑定表达式来绑定到不存在的 NamedColor 属性,可对此进行验证,这将导致生成错误。 尽管此示例将 x:DataType 特性设置为字符串文字,但也可以将其设置为具有 x:Type 标记扩展的类型。 有关 x:Type 标记扩展的详细信息,请参阅 x:Type 标记扩展

首次运行该示例时,使用 NamedColor 实例填充 ListView。 选中 ListView 中的项时,BoxView.Color 属性被设置为 ListView 中所选项的颜色:

已编译的颜色列表。

选择 ListView 中的其他项会更新 BoxView 的颜色。

在 XAML 中,将已编译的绑定与传统绑定相结合

绑定表达式仅针对定义了 x:DataType 属性的视图层次结构进行编译。 相反,未定义 x:DataType 属性的层次结构中的任何视图都将使用传统绑定。 因此,可以在页面上组合已编译的绑定和传统绑定。 例如,在之前的部分中,DataTemplate 中的视图使用已编译的绑定,而设置为 ListView 中所选颜色的 BoxView 则不使用。

因此仔细构造 x:DataType 属性可以生成使用已编译绑定和传统绑定的页面。 或者,可以使用 x:Null 标记扩展在视图层次结构中的任意点将 x:DataType 属性重新定义为 null。 执行此操作表示视图层次结构中的任何绑定表达式都将使用传统绑定。 以下详细示例演示了这种方法:

<StackLayout x:DataType="local:HslColorViewModel">
    <StackLayout.BindingContext>
        <local:HslColorViewModel Color="Sienna" />
    </StackLayout.BindingContext>
    <BoxView Color="{Binding Color}"
             VerticalOptions="FillAndExpand" />
    <StackLayout x:DataType="{x:Null}"
                 Margin="10, 0">
        <Label Text="{Binding Name}" />
        <Slider Value="{Binding Hue}" />
        <Label Text="{Binding Hue, StringFormat='Hue = {0:F2}'}" />
        <Slider Value="{Binding Saturation}" />
        <Label Text="{Binding Saturation, StringFormat='Saturation = {0:F2}'}" />
        <Slider Value="{Binding Luminosity}" />
        <Label Text="{Binding Luminosity, StringFormat='Luminosity = {0:F2}'}" />
    </StackLayout>
</StackLayout>   

StackLayoutx:DataType 属性设置为 HslColorViewModel 类型,表示将编译根 StackLayout 视图结构中的任意绑定表达式。 但是,内部 StackLayout 使用 x:Null 标记表达式将 x:DataType 属性重新定义为 null。 因此,内部 StackLayout 中的绑定表达式使用传统绑定。 只有根 StackLayout 视图结构层次中的 BoxView 使用已编译的绑定。

有关 x:Null 标记表达式的详细信息,请参阅 x:Null 标记扩展

代码中的已编译绑定

用代码编写的绑定通常使用在运行时通过反射解析的字符串路径。 但是,SetBinding 扩展方法还有一个重载,该重载使用 Func 参数而不是字符串路径定义绑定:

MyLabel.SetBinding(Label.TextProperty, static (Entry entry) => entry.Text);

并非所有方法都可用于定义已编译的绑定。 表达式必须是简单的属性访问表达式。 以下示例显示了有效和无效的绑定表达式:

// Valid: Property access
static (PersonViewModel vm) => vm.Name;
static (PersonViewModel vm) => vm.Address?.Street;

// Valid: Array and indexer access
static (PersonViewModel vm) => vm.PhoneNumbers[0];
static (PersonViewModel vm) => vm.Config["Font"];

// Valid: Casts
static (Label label) => (label.BindingContext as PersonViewModel).Name;
static (Label label) => ((PersonViewModel)label.BindingContext).Name;

// Invalid: Method calls
static (PersonViewModel vm) => vm.GetAddress();
static (PersonViewModel vm) => vm.Address?.ToString();

// Invalid: Complex expressions
static (PersonViewModel vm) => vm.Address?.Street + " " + vm.Address?.City;
static (PersonViewModel vm) => $"Name: {vm.Name}";

此外,Binding.Create 方法使用 Func 直接在对象上设置绑定,并返回绑定对象实例:

myEntry.SetBinding(Entry.TextProperty, new MultiBinding
{
    Bindings = new Collection<BindingBase>
    {
        Binding.Create(static (Entry entry) => entry.FontFamily, source: RelativeBindingSource.Self),
        Binding.Create(static (Entry entry) => entry.FontSize, source: RelativeBindingSource.Self),
        Binding.Create(static (Entry entry) => entry.FontAttributes, source: RelativeBindingSource.Self),
    },
    Converter = new StringConcatenationConverter()
});

这些已编译的绑定方法具有以下优势:

  • 通过在编译时而不是运行时解析绑定表达式,提升了数据绑定性能。
  • 更好的开发人员故障排除体验,因为无效的绑定将被报告为生成错误。
  • 编辑时的 Intellisense。

性能

已编译的绑定可提升数据绑定性能,但性能优势各不相同:

  • 使用属性更改通知(即 OneWayOneWayToSourceTwoWay 绑定)的已编译绑定的解析速度比传统绑定快约 8 倍。
  • 不使用属性更改通知(例如 OneTime 绑定)的已编译绑定的解析速度比传统绑定快约 20 倍。
  • 在使用属性更改通知(即 OneWayOneWayToSourceTwoWay 绑定)的已编译绑定上设置 BindingContext 比在经典绑定上设置 BindingContext 快大约 5 倍。
  • 在不使用属性更改通知(即 OneTime 绑定)的已编译绑定上设置 BindingContext 比在经典绑定上设置 BindingContext 快大约 7 倍。

这些性能差异在移动设备上更为明显,具体取决于所使用的平台、正在使用的操作系统的版本以及运行应用程序的设备。