已编译的绑定

浏览示例。 浏览示例

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

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

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

重要

在 NativeAOT 应用和启用了完全修整功能的应用中,需要使用编译的绑定,而不是基于字符串的绑定。 有关详细信息,请参阅 剪裁 .NET MAUI 应用本机 AOT 部署

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 中已编译的绑定。

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

<MauiStrictXamlCompilation>true</MauiStrictXamlCompilation>

默认情况下,.NET MAUI 为不使用已编译绑定的 XAML 绑定生成警告。

有关 XAML 编译绑定警告的详细信息,请参阅 XAML 编译绑定警告

使用 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 从其父范围继承错误 x:DataType

<ContentPage ...
             x:DataType="local:AnimalsPageViewModel">
    <!-- Binding to AnimalsPageViewModel.Animals -->
    <CollectionView ItemsSource="{Binding Animals}">
        <CollectionView.ItemTemplate>
            <DataTemplate>
                <!-- incorrect: compiler thinks you want to bind to AnimalsPageViewModel.Name -->  
                <Label Text="{Binding Name}" />
            </DataTemplate>
        </CollectionView.ItemTemplate>
    </CollectionView>
</ContentPage>

以下示例演示如何正确设置 x:DataType on 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 的颜色。

编译定义属性的 Source 绑定

在 .NET MAUI 9 之前,XAML 编译器将跳过定义 Source 属性而不是该属性的 BindingContext绑定的编译。 从 .NET MAUI 9 开始,可以编译这些绑定,以利用更好的运行时性能。 但是,默认情况下不会启用此优化,以避免中断现有的应用代码。 若要启用此优化,请在应用的项目文件中将 $(MauiEnableXamlCBindingWithSourceCompilation) 生成属性设置为 true

<MauiEnableXamlCBindingWithSourceCompilation>true</MauiEnableXamlCBindingWithSourceCompilation>

然后,请确保所有绑定都使用正确的 x:DataType 注释,并且它们不会从其父范围继承不正确的数据类型:

<HorizontalStackLayout BindingContext="{x:Reference slider}" x:DataType="Slider">
  <Label Text="{Binding Value}" />
  <Label Text="{Binding Text, Source={x:Reference entry}, x:DataType=Entry}" />
</HorizontalStackLayout>

注意

如果绑定具有 aSource,但它继承x:DataType自父级的绑定,则两者的类型x:DataTypeSource之间可能存在不匹配的情况。 在此方案中,将生成警告,并回退到在运行时解析绑定路径的基于反射的绑定。

在 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 标记扩展

XAML 编译绑定警告

下表列出了已编译绑定的编译器警告,以及如何解析它们:

代码 消息 Fix
XC0022 可以编译绑定以提高运行时性能(如果 x:DataType 已指定)。 添加到 x:DataType XAML 以指定当前 BindingContext类型。 最佳做法是添加到 x:DataType 绑定上下文更改的所有元素。
XC0023 如果不是显式null的,可以编译绑定以提高运行时性能x:DataType 替换为 x:DataType="{x:Null}" 正确的类型。
代码 消息
XC0022 可以编译绑定以提高运行时性能(如果 x:DataType 已指定)。

若要修复此警告,请添加到 x:DataType XAML 以指定当前 BindingContext类型。 最佳做法是添加到 x:DataType 绑定上下文更改的所有元素。
XC0023 如果不是显式null的,可以编译绑定以提高运行时性能x:DataType

若要修复此警告,请替换为 x:DataType="{x:Null}" 正确的类型。
XC0024 由于批注来自外部范围, x:DataType 因此绑定可能编译不正确。 请确保使用正确的x:DataType批注所有 DataTemplate XAML 元素。

若要修复此警告,请确保所有 DataTemplate 元素都用正确的 x:DataType批注。
XC0025 未编译绑定,因为它具有显式设置 Source 的属性,并且未启用绑定 Source 编译。 请考虑通过在项目文件中设置 <MauiEnableXamlCBindingWithSourceCompilation>true</MauiEnableXamlCBindingWithSourceCompilation> 来启用此优化,并确保为此绑定指定了正确的值 x:DataType

若要修复此警告,请在项目文件中启用 $(MauiEnableXamlCBindingWithSourceCompilation) 生成属性,并使用相应的 x:DataType绑定批注所有绑定。

若要确保不会忽略这些警告,请考虑更改特定警告以生成 $(WarningsAsErrors) 生成生成属性的错误:

<WarningsAsErrors>$(WarningsAsErrors);XC0022;XC0023</WarningsAsErrors>

若要忽略这些警告,请使用具有特定警告代码的 $(NoWarn) 生成属性:

<NoWarn>$(NoWarn);XC0022;XC0023</NoWarn>

重要

XC0022 除非 XC0023 生成 $(MauiStrictXamlCompilation) 属性设置为 true,否则将始终禁止显示警告。

如果将生成属性设置为$(TreatWarningsAsErrors)true应用的项目文件中,但你想要忽略某些 XAML 编译器警告,请使用$(NoWarn)生成属性来压制这些警告或$(WarningsNotAsErrors)生成属性以减少某些特定代码的严重性。

默认情况下,.NET MAUI 为不使用已编译绑定的 XAML 绑定生成警告。 通过将应用的项目文件(*.csproj)中设置 $(MauiStrictXamlCompilation)$(TreatWarningsAsErrors) 生成属性设置为 true 错误,可以选择加入编译的绑定警告:

<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<MauiStrictXamlCompilation>true</MauiStrictXamlCompilation>

注意

默认情况下, $(MauiStrictXamlCompilation) 生成属性是 false 除非使用完全修整或 NativeAOT 发布应用。

代码中的已编译绑定

用代码编写的绑定通常使用在运行时通过反射解析的字符串路径。 但是,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}";

此外,BindingBase.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 倍。

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