mcpField
M
maui-data-binding

maui-data-binding

github.com/dotnet开发5.8k3.4k 次安装1 天前更新
代码仓库

关于 maui-data-binding

用于在 .NET MAUI 项目中实现和排查数据绑定的技能,侧重编译时绑定、正确的变更通知,以及 XAML 和 C# 中的绑定行为。

主要功能

  1. 01使用 x:DataType 设置编译时绑定
  2. 02指导 BindingContext 与绑定模式
  3. 03涵盖属性变更通知
  4. 04说明转换器与相对绑定
  5. 05提供 .NET 9+ 的 AOT 安全 C# 绑定

适用场景

  1. 01为 MAUI 页面添加类型化绑定
  2. 02排查绑定未更新的问题
  3. 03通过多重绑定组合多个值
  4. 04避免裁剪或 AOT 构建中的反射

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dotnet/skills --skill maui-data-binding

安装后在 Claude Code 中输入/maui-data-binding确认已加载。

附带文件references/data-binding-api.md

详细介绍

.NET MAUI 数据绑定

将 UI 控件连接到 ViewModel 属性,确保编译时安全、变更通知正确且开销极低。尽可能使用编译绑定,并将绑定警告视为构建错误。

适用场景

  • 为新页面或现有页面添加 x:DataType 编译绑定
  • 实现 INotifyPropertyChanged 或 CommunityToolkit ObservableObject
  • 创建或使用 IValueConverter / IMultiValueConverter
  • 为控件属性选择正确的 BindingMode
  • 在 XAML 或代码隐藏中设置 BindingContext
  • 使用相对绑定(Self、AncestorType、TemplatedParent)
  • 应用 StringFormat、FallbackValue 或 TargetNullValue
  • 使用 SetBinding 和 lambda 编写 AOT 安全的代码绑定(.NET 9+)

不适用场景

  • CollectionView 布局 / 模板 — 使用 maui-collectionview 技能
  • Shell 导航参数 — 使用 maui-shell-navigation 技能
  • 服务注册 / DI — 使用 maui-dependency-injection 技能
  • 由属性变更触发的动画 — 使用内置的 .NET MAUI 动画 API

输入

  • 一个面向 .NET 8 或更高版本的 .NET MAUI 项目
  • 声明绑定的 XAML 页面或 C# 代码隐藏
  • 一个 ViewModel 类(或创建该类的计划)

会影响答案的规则

每个绑定问题都应遵循以下规则——这些规则决定了代码是“能编译”还是“能实际更新 UI”。

情况应这样做不要这样做
确定 x:DataType 的位置在绑定作用域开始处设置——页面 / 视图根元素,以及每个 DataTemplate随意添加到与父级共用 BindingContext 的子元素上
绑定回退到反射(XC0022 / XC0023)为该绑定作用域添加正确的 x:DataType;对于 XC0023,移除显式的 x:DataType="{x:Null}"使用 x:DataType="x:Object" 来消除警告——这会禁用编译时检查
DataTemplate 从外层作用域继承 x:DataType(XC0024)为 DataTemplate 设置其自己的 x:DataType任其根据错误的类型进行解析
ViewModel 变更通知使用 ObservableObject + [ObservableProperty],或实现 INotifyPropertyChanged使用普通 POCO 基类——绑定将永远不会更新
绑定显示为空白检查是否确实设置了 BindingContext直接假定绑定路径有误
强制使用编译绑定将 MauiEnableXamlCBindingWithSourceCompilation 设为 true,然后添加 <WarningsAsErrors>XC0022;XC0025</WarningsAsErrors>如果项目使用 Source= / RelativeSource 绑定,却未启用该开关就提升 XC0025 的严重级别

不要重构用户未要求重构的 ViewModel,也不要添加无法修复实际缺陷的转换器。添加 x:DataType 则有所不同:如果你已经在编辑页面的绑定,推荐使用编译绑定属于合理范围。


编译绑定 — x:DataType 的设置位置

编译绑定的速度比基于反射的绑定快 8–20 倍,且 NativeAOT / 裁剪要求使用编译绑定。使用 x:DataType 启用编译绑定。

设置位置规则

仅在设置 BindingContext 的位置设置 x:DataType:

  1. 页面 / 视图根元素 — 在此处设置 BindingContext。
  2. DataTemplate — 此处会创建新的绑定作用域。

不要在任意子元素上随意添加 x:DataType。在子元素上添加 x:DataType="x:Object" 来绕过编译绑定是一种反模式——它会禁用编译时检查,并重新引入反射。


<ContentPage xmlns:vm="clr-namespace:MyApp.ViewModels"
             x:DataType="vm:MainViewModel">
    <StackLayout>
        <Label Text="{Binding Title}" />
        <Slider Value="{Binding Progress}" />
    </StackLayout>
</ContentPage>


<ContentPage x:DataType="vm:MainViewModel">
    <StackLayout>
        <Label Text="{Binding Title}" />
        <Slider x:DataType="x:Object" Value="{Binding Progress}" />
    </StackLayout>
</ContentPage>
DataTemplate 始终需要自己的 x:DataType
<CollectionView ItemsSource="{Binding People}">
    <CollectionView.ItemTemplate>
        <DataTemplate x:DataType="model:Person">
            <Label Text="{Binding FullName}" />
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>
将绑定警告视为错误
警告含义
XC0022绑定使用时作用域内没有 x:DataType — 未编译,回退到反射
XC0023绑定未编译,因为 x:DataType 被显式设为 null
XC0024x:DataType 来自外层作用域 — 为 DataTemplate 添加其自身的 x:DataType
XC0025绑定未编译,因为它具有显式的 Source — 启用 <MauiEnableXamlCBindingWithSourceCompilation>

这四个代码已针对 .NET 10 / .NET 11 MAUI 进行验证 (Build.Tasks/BuildException.cs、ErrorMessages.resx)。诊断编号 可能因 SDK 版本段而异 — 在较新的 SDK 上依赖这些编号之前,请重新检查 BuildException.cs。

添加到 .csproj:


<MauiEnableXamlCBindingWithSourceCompilation>true</MauiEnableXamlCBindingWithSourceCompilation>
<WarningsAsErrors>XC0022;XC0025</WarningsAsErrors>

如果在未启用该开关的情况下将 XC0025 提升为错误,请确保项目中没有 Source= / RelativeSource 绑定 — 否则这些绑定会被报告为错误。


绑定模式

仅在需要覆盖默认值时才显式设置 Mode。大多数属性 已有合适的默认值:

模式方向使用场景
OneWay源 → 目标仅用于显示(大多数属性的默认值)
TwoWay源 ↔ 目标可编辑控件(Entry.Text、Switch.IsToggled)
OneWayToSource目标 → 源读取用户输入,但不将更改推送回 UI
OneTime源 → 目标(一次)静态值;无变更跟踪开销

<Label Text="{Binding Score}" />
<Entry Text="{Binding UserName}" />
<Switch IsToggled="{Binding DarkMode}" />


<Label Text="{Binding Title, Mode=OneTime}" />
<Entry Text="{Binding SearchQuery, Mode=OneWayToSource}" />


<Label Text="{Binding Score, Mode=OneWay}" />
<Entry Text="{Binding UserName, Mode=TwoWay}" />

BindingContext 和属性路径

除非显式设置,所有 BindableObject 都会从其父级继承 BindingContext。属性路径支持点号表示法和索引器:

<Label Text="{Binding Address.City}" />
<Label Text="{Binding Items[0].Name}" />

在 XAML 中设置 BindingContext:

<ContentPage xmlns:vm="clr-namespace:MyApp.ViewModels"
             x:DataType="vm:MainViewModel">
    <ContentPage.BindingContext>
        <vm:MainViewModel />
    </ContentPage.BindingContext>
</ContentPage>

或在代码隐藏中设置(使用 DI 时推荐):

public MainPage(MainViewModel vm)
{
    InitializeComponent();
    BindingContext = vm;
}

INotifyPropertyChanged 和 ObservableObject

手动实现
public class MainViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    private string _title = string.Empty;
    public string Title
    {
        get => _title;
        set
        {
            if (_title != value)
            {
                _title = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Title)));
            }
        }
    }
}
CommunityToolkit.Mvvm(推荐)
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

public partial class MainViewModel : ObservableObject
{
    [ObservableProperty]
    private string _title = string.Empty;

    [RelayCommand]
    private async Task LoadDataAsync() { /* ... */ }
}

源生成器会自动创建 Title 属性、触发 PropertyChanged,以及生成 LoadDataCommand。


值转换器 — IValueConverter

实现 Convert(源 → 目标)和 ConvertBack(目标 → 源):

public class IntToBoolConverter : IValueConverter
{
    public object? Convert(object? value, Type targetType,
        object? parameter, CultureInfo culture)
        => value is int i && i != 0;

    public object? ConvertBack(object? value, Type targetType,
        object? parameter, CultureInfo culture)
        => value is true ? 1 : 0;
}

在 XAML 资源中声明并使用:

<ContentPage.Resources>
    <local:IntToBoolConverter x:Key="IntToBool" />
</ContentPage.Resources>

<Switch IsToggled="{Binding Count, Converter={StaticResource IntToBool}}" />

ConverterParameter 始终以字符串形式传入 — 在 Convert 中进行解析:

<Label Text="{Binding Score, Converter={StaticResource ThresholdConverter},
              ConverterParameter=50}" />

多重绑定

使用 IMultiValueConverter 组合多个源值:

<Label>
    <Label.Text>
        <MultiBinding Converter="{StaticResource FullNameConverter}">
            <Binding Path="FirstName" />
            <Binding Path="LastName" />
        </MultiBinding>
    </Label.Text>
</Label>
public class FullNameConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType,
        object parameter, CultureInfo culture)
    {
        if (values.Length == 2 && values[0] is string first
            && values[1] is string last)
            return $"{first} {last}";
        return string.Empty;
    }

    public object[] ConvertBack(object value, Type[] targetTypes,
        object parameter, CultureInfo culture)
        => throw new NotSupportedException();
}

相对绑定

源语法使用场景
自身{Binding Source={RelativeSource Self}, Path=WidthRequest}绑定到自身属性
祖先{Binding BindingContext.Title, Source={RelativeSource AncestorType={x:Type ContentPage}}}访问父级的 BindingContext
模板化父级{Binding Source={RelativeSource TemplatedParent}, Path=Padding}在 ControlTemplate 内部使用

<BoxView WidthRequest="100"
         HeightRequest="{Binding Source={RelativeSource Self}, Path=WidthRequest}" />

字符串格式

对于简单的显示格式化,可使用 Binding.StringFormat,无需转换器:

<Label Text="{Binding Price, StringFormat='Total: {0:C2}'}" />
<Label Text="{Binding DueDate, StringFormat='{0:MMM dd, yyyy}'}" />

如果格式字符串包含逗号或花括号,请用单引号括起。


绑定回退值

  • FallbackValue — 当无法解析绑定路径或转换器引发异常时使用。
  • TargetNullValue — 当绑定值为 null 时使用。
<Label Text="{Binding MiddleName, TargetNullValue='(none)',
              FallbackValue='unavailable'}" />
<Image Source="{Binding AvatarUrl, TargetNullValue='default_avatar.png'}" />

.NET 9+ 代码绑定(AOT 安全)

完全支持 AOT,无反射:

label.SetBinding(Label.TextProperty,
    static (PersonViewModel vm) => vm.FullName);

entry.SetBinding(Entry.TextProperty,
    static (PersonViewModel vm) => vm.Age,
    mode: BindingMode.TwoWay,
    converter: new IntToStringConverter());

线程处理

MAUI 会自动将 PropertyChanged 调度到 UI 线程 — 你可以从任何线程触发它。但是,在后台线程中直接修改 ObservableCollection(添加 / 删除)可能导致崩溃:

// ✅ Safe — PropertyChanged is auto-marshalled
await Task.Run(() => Title = "Loaded");

// ⚠️ ObservableCollection.Add — dispatch to UI thread
MainThread.BeginInvokeOnMainThread(() => Items.Add(newItem));

常见陷阱

错误修复方法
缺少 x:DataType — 绑定会静默退回到反射在页面根节点和每个 DataTemplate 中添加 x:DataType;提升 XC0022(参见将绑定警告视为错误)
忘记设置 BindingContext在 XAML 中设置(<Page.BindingContext>),或通过构造函数注入
指定多余的 Mode=OneWay / Mode=TwoWay使用控件默认值时,省略 Mode
ViewModel 未实现 INotifyPropertyChanged使用 CommunityToolkit.Mvvm 中的 ObservableObject,或手动实现
在 UI 线程之外修改 ObservableCollection将修改封装在 MainThread.BeginInvokeOnMainThread 中
在高频执行路径中使用复杂的转换器链改为在 ViewModel 中预先计算值
使用 x:DataType="x:Object" 绕过编译绑定重构绑定;保留编译时安全性
绑定到非公开属性绑定目标必须是 public 属性(字段会被忽略)

参考资料

评论

支持 Markdown