maui-data-binding

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

.NET MAUI Data Binding

.NET MAUI 数据绑定

Wire UI controls to ViewModel properties with compile-time safety, correct change notification, and minimal overhead. Prefer compiled bindings everywhere and treat binding warnings as build errors.
将UI控件与ViewModel属性绑定,实现编译期安全、正确的变更通知和最小开销。所有场景优先使用编译绑定,并将绑定警告视为构建错误。

When to Use

适用场景

  • Adding
    x:DataType
    compiled bindings to a new or existing page
  • Implementing
    INotifyPropertyChanged
    or CommunityToolkit
    ObservableObject
  • Creating or consuming
    IValueConverter
    /
    IMultiValueConverter
  • Choosing the correct
    BindingMode
    for a control property
  • Setting
    BindingContext
    in XAML or code-behind
  • Using relative bindings (
    Self
    ,
    AncestorType
    ,
    TemplatedParent
    )
  • Applying
    StringFormat
    ,
    FallbackValue
    , or
    TargetNullValue
  • Writing AOT-safe code bindings with
    SetBinding
    and lambdas (.NET 9+)
  • 为新页面或现有页面添加
    x:DataType
    编译绑定
  • 实现
    INotifyPropertyChanged
    或CommunityToolkit
    ObservableObject
  • 创建或使用
    IValueConverter
    /
    IMultiValueConverter
  • 为控件属性选择正确的
    BindingMode
  • 在XAML或代码后置中设置
    BindingContext
  • 使用相对绑定(
    Self
    AncestorType
    TemplatedParent
  • 应用
    StringFormat
    FallbackValue
    TargetNullValue
  • 使用
    SetBinding
    和lambda编写AOT安全的代码绑定(.NET 9及以上版本)

When Not to Use

不适用场景

  • CollectionView layouts / templates — use the
    maui-collectionview
    skill
  • Shell navigation parameters — use the
    maui-shell-navigation
    skill
  • Service registration / DI — use the
    maui-dependency-injection
    skill
  • Property-change-triggered animations — use built-in .NET MAUI animation APIs
  • CollectionView布局/模板 —— 请使用
    maui-collectionview
    技能
  • Shell导航参数 —— 请使用
    maui-shell-navigation
    技能
  • 服务注册/DI —— 请使用
    maui-dependency-injection
    技能
  • 属性变更触发的动画 —— 请使用内置的.NET MAUI 动画API

Inputs

前置条件

  • A .NET MAUI project targeting .NET 8 or later
  • XAML pages or C# code-behind where bindings are declared
  • A ViewModel class (or plan to create one)

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

Compiled Bindings — x:DataType Placement

编译绑定——x:DataType放置规则

Compiled bindings are 8–20× faster than reflection-based bindings and are required for NativeAOT / trimming. Enable them with
x:DataType
.
编译绑定比基于反射的绑定快8-20倍,是NativeAOT/裁剪的必选项。通过
x:DataType
启用编译绑定。

Placement rules

放置规则

Set
x:DataType
only where
BindingContext
is set
:
  1. Page / View root — where you assign
    BindingContext
    .
  2. DataTemplate — which creates a new binding scope.
Do not scatter
x:DataType
on arbitrary child elements. Adding
x:DataType="x:Object"
on children to escape compiled bindings is an anti-pattern — it disables compile-time checking and reintroduces reflection.
xml
<!-- ✅ Correct: x:DataType at the page root -->
<ContentPage xmlns:vm="clr-namespace:MyApp.ViewModels"
             x:DataType="vm:MainViewModel">
    <StackLayout>
        <Label Text="{Binding Title}" />
        <Slider Value="{Binding Progress}" />
    </StackLayout>
</ContentPage>

<!-- ❌ Wrong: x:DataType scattered on children -->
<ContentPage x:DataType="vm:MainViewModel">
    <StackLayout>
        <Label Text="{Binding Title}" />
        <Slider x:DataType="x:Object" Value="{Binding Progress}" />
    </StackLayout>
</ContentPage>
仅在设置了
BindingContext
的位置
设置
x:DataType
  1. 页面/视图根节点 —— 你赋值
    BindingContext
    的位置
  2. DataTemplate —— 会创建新的绑定作用域
不要在任意子元素上随意添加
x:DataType
。在子元素上添加
x:DataType="x:Object"
来绕过编译绑定是反模式——它会禁用编译期检查并重新引入反射机制。
xml
<!-- ✅ 正确:在页面根节点设置x:DataType -->
<ContentPage xmlns:vm="clr-namespace:MyApp.ViewModels"
             x:DataType="vm:MainViewModel">
    <StackLayout>
        <Label Text="{Binding Title}" />
        <Slider Value="{Binding Progress}" />
    </StackLayout>
</ContentPage>

<!-- ❌ 错误:在子元素上零散设置x:DataType -->
<ContentPage x:DataType="vm:MainViewModel">
    <StackLayout>
        <Label Text="{Binding Title}" />
        <Slider x:DataType="x:Object" Value="{Binding Progress}" />
    </StackLayout>
</ContentPage>

DataTemplate always needs its own x:DataType

DataTemplate始终需要单独设置x:DataType

xml
<CollectionView ItemsSource="{Binding People}">
    <CollectionView.ItemTemplate>
        <DataTemplate x:DataType="model:Person">
            <Label Text="{Binding FullName}" />
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>
xml
<CollectionView ItemsSource="{Binding People}">
    <CollectionView.ItemTemplate>
        <DataTemplate x:DataType="model:Person">
            <Label Text="{Binding FullName}" />
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>

Enforce binding warnings as errors

将绑定警告设为错误强制执行

WarningMeaning
XC0022Binding path not found on the declared
x:DataType
XC0023Property is not bindable
XC0024
x:DataType
type not found
XC0025Binding used without
x:DataType
(non-compiled fallback)
Add to the
.csproj
:
xml
<WarningsAsErrors>XC0022;XC0025</WarningsAsErrors>

警告编号含义
XC0022在声明的
x:DataType
上找不到绑定路径
XC0023属性不可绑定
XC0024找不到
x:DataType
指定的类型
XC0025未使用
x:DataType
的绑定(非编译回退)
.csproj
中添加配置:
xml
<WarningsAsErrors>XC0022;XC0025</WarningsAsErrors>

Binding Modes

绑定模式

Set
Mode
explicitly only when overriding the default. Most properties already have the correct default:
ModeDirectionUse case
OneWay
Source → TargetDisplay-only (default for most properties)
TwoWay
Source ↔ TargetEditable controls (
Entry.Text
,
Switch.IsToggled
)
OneWayToSource
Target → SourceRead user input without pushing back to UI
OneTime
Source → Target (once)Static values; no change-tracking overhead
xml
<!-- ✅ Defaults — omit Mode -->
<Label Text="{Binding Score}" />
<Entry Text="{Binding UserName}" />
<Switch IsToggled="{Binding DarkMode}" />

<!-- ✅ Override only when needed -->
<Label Text="{Binding Title, Mode=OneTime}" />
<Entry Text="{Binding SearchQuery, Mode=OneWayToSource}" />

<!-- ❌ Redundant — adds noise -->
<Label Text="{Binding Score, Mode=OneWay}" />
<Entry Text="{Binding UserName, Mode=TwoWay}" />

仅在需要覆盖默认值时显式设置
Mode
。绝大多数属性已经有正确的默认值:
模式数据流方向适用场景
OneWay
源 → 目标仅展示内容(绝大多数属性的默认值)
TwoWay
源 ↔ 目标可编辑控件(
Entry.Text
Switch.IsToggled
OneWayToSource
目标 → 源读取用户输入但不回写到UI
OneTime
源 → 目标(仅一次)静态值;无变更追踪开销
xml
<!-- ✅ 默认值——省略Mode即可 -->
<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 and Property Paths

BindingContext与属性路径

Every
BindableObject
inherits
BindingContext
from its parent unless explicitly set. Property paths support dot notation and indexers:
xml
<Label Text="{Binding Address.City}" />
<Label Text="{Binding Items[0].Name}" />
Set
BindingContext
in XAML:
xml
<ContentPage xmlns:vm="clr-namespace:MyApp.ViewModels"
             x:DataType="vm:MainViewModel">
    <ContentPage.BindingContext>
        <vm:MainViewModel />
    </ContentPage.BindingContext>
</ContentPage>
Or in code-behind (preferred with DI):
csharp
public MainPage(MainViewModel vm)
{
    InitializeComponent();
    BindingContext = vm;
}

所有
BindableObject
都会从父元素继承
BindingContext
,除非显式设置。属性路径支持点表示法和索引器:
xml
<Label Text="{Binding Address.City}" />
<Label Text="{Binding Items[0].Name}" />
在XAML中设置
BindingContext
xml
<ContentPage xmlns:vm="clr-namespace:MyApp.ViewModels"
             x:DataType="vm:MainViewModel">
    <ContentPage.BindingContext>
        <vm:MainViewModel />
    </ContentPage.BindingContext>
</ContentPage>
或者在代码后置中设置(结合DI时推荐):
csharp
public MainPage(MainViewModel vm)
{
    InitializeComponent();
    BindingContext = vm;
}

INotifyPropertyChanged and ObservableObject

INotifyPropertyChanged与ObservableObject

Manual implementation

手动实现

csharp
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)));
            }
        }
    }
}
csharp
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 (recommended)

CommunityToolkit.Mvvm实现(推荐)

csharp
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

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

    [RelayCommand]
    private async Task LoadDataAsync() { /* ... */ }
}
The source generator creates the
Title
property,
PropertyChanged
raise, and
LoadDataCommand
automatically.

csharp
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

Value Converters — IValueConverter

值转换器——IValueConverter

Implement
Convert
(source → target) and
ConvertBack
(target → source):
csharp
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;
}
Declare in XAML resources and consume:
xml
<ContentPage.Resources>
    <local:IntToBoolConverter x:Key="IntToBool" />
</ContentPage.Resources>

<Switch IsToggled="{Binding Count, Converter={StaticResource IntToBool}}" />
ConverterParameter
is always passed as a string — parse inside
Convert
:
xml
<Label Text="{Binding Score, Converter={StaticResource ThresholdConverter},
              ConverterParameter=50}" />

实现
Convert
(源→目标)和
ConvertBack
(目标→源)方法:
csharp
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资源中声明并使用:
xml
<ContentPage.Resources>
    <local:IntToBoolConverter x:Key="IntToBool" />
</ContentPage.Resources>

<Switch IsToggled="{Binding Count, Converter={StaticResource IntToBool}}" />
ConverterParameter
始终以字符串形式传递——请在
Convert
方法内部解析:
xml
<Label Text="{Binding Score, Converter={StaticResource ThresholdConverter},
              ConverterParameter=50}" />

Multi-Binding

多绑定

Combine multiple source values with
IMultiValueConverter
:
xml
<Label>
    <Label.Text>
        <MultiBinding Converter="{StaticResource FullNameConverter}">
            <Binding Path="FirstName" />
            <Binding Path="LastName" />
        </MultiBinding>
    </Label.Text>
</Label>
csharp
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();
}

使用
IMultiValueConverter
合并多个源的值:
xml
<Label>
    <Label.Text>
        <MultiBinding Converter="{StaticResource FullNameConverter}">
            <Binding Path="FirstName" />
            <Binding Path="LastName" />
        </MultiBinding>
    </Label.Text>
</Label>
csharp
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();
}

Relative Bindings

相对绑定

SourceSyntaxUse case
Self
{Binding Source={RelativeSource Self}, Path=WidthRequest}
Bind to own properties
Ancestor
{Binding BindingContext.Title, Source={RelativeSource AncestorType={x:Type ContentPage}}}
Reach parent BindingContext
TemplatedParent
{Binding Source={RelativeSource TemplatedParent}, Path=Padding}
Inside ControlTemplate
xml
<!-- Square box: Height = Width -->
<BoxView WidthRequest="100"
         HeightRequest="{Binding Source={RelativeSource Self}, Path=WidthRequest}" />

源类型语法适用场景
自身
{Binding Source={RelativeSource Self}, Path=WidthRequest}
绑定到自身属性
祖先元素
{Binding BindingContext.Title, Source={RelativeSource AncestorType={x:Type ContentPage}}}
访问父元素的BindingContext
模板父级
{Binding Source={RelativeSource TemplatedParent}, Path=Padding}
在ControlTemplate内部使用
xml
<!-- 正方形盒子:高度等于宽度 -->
<BoxView WidthRequest="100"
         HeightRequest="{Binding Source={RelativeSource Self}, Path=WidthRequest}" />

StringFormat

StringFormat

Use
Binding.StringFormat
for simple display formatting without a converter:
xml
<Label Text="{Binding Price, StringFormat='Total: {0:C2}'}" />
<Label Text="{Binding DueDate, StringFormat='{0:MMM dd, yyyy}'}" />
Wrap the format string in single quotes when it contains commas or braces.

简单的展示格式化无需使用转换器,直接用
Binding.StringFormat
即可:
xml
<Label Text="{Binding Price, StringFormat='Total: {0:C2}'}" />
<Label Text="{Binding DueDate, StringFormat='{0:MMM dd, yyyy}'}" />
当格式字符串包含逗号或大括号时,请用单引号包裹格式字符串。

Binding Fallbacks

绑定回退

  • FallbackValue — used when the binding path cannot be resolved or the converter throws.
  • TargetNullValue — used when the bound value is
    null
    .
xml
<Label Text="{Binding MiddleName, TargetNullValue='(none)',
              FallbackValue='unavailable'}" />
<Image Source="{Binding AvatarUrl, TargetNullValue='default_avatar.png'}" />

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

.NET 9+ Code Bindings (AOT-safe)

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

Fully AOT-safe, no reflection:
csharp
label.SetBinding(Label.TextProperty,
    static (PersonViewModel vm) => vm.FullName);

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

完全AOT安全,无反射依赖:
csharp
label.SetBinding(Label.TextProperty,
    static (PersonViewModel vm) => vm.FullName);

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

Threading

线程处理

MAUI automatically marshals
PropertyChanged
to the UI thread — you can raise it from any thread. However, direct
ObservableCollection
mutations (Add / Remove) from background threads may crash:
csharp
// ✅ Safe — PropertyChanged is auto-marshalled
await Task.Run(() => Title = "Loaded");

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

MAUI会自动将
PropertyChanged
封送到UI线程——你可以在任意线程触发该事件。但是,在后台线程直接修改
ObservableCollection
(增/删操作)可能会导致崩溃:
csharp
// ✅ 安全——PropertyChanged会自动封送
await Task.Run(() => Title = "Loaded");

// ⚠️ ObservableCollection.Add操作——需要调度到UI线程
MainThread.BeginInvokeOnMainThread(() => Items.Add(newItem));

Common Pitfalls

常见问题

MistakeFix
Missing
x:DataType
— bindings silently fall back to reflection
Add
x:DataType
at page root and every
DataTemplate
; enable
XC0025
as error
Forgetting to set
BindingContext
Set in XAML (
<Page.BindingContext>
) or inject via constructor
Specifying redundant
Mode=OneWay
/
Mode=TwoWay
Omit
Mode
when using the control's default
ViewModel does not implement
INotifyPropertyChanged
Use
ObservableObject
from CommunityToolkit.Mvvm or implement manually
Mutating
ObservableCollection
off the UI thread
Wrap mutations in
MainThread.BeginInvokeOnMainThread
Complex converter chains in hot pathsPre-compute values in the ViewModel instead
Using
x:DataType="x:Object"
to escape compiled bindings
Restructure bindings; keep compile-time safety
Binding to non-public propertiesBinding targets must be
public
properties (fields are ignored)

错误修复方案
缺失
x:DataType
——绑定静默回退到反射模式
在页面根节点和每个
DataTemplate
上添加
x:DataType
;开启
XC0025
错误提示
忘记设置
BindingContext
在XAML中通过
<Page.BindingContext>
设置,或通过构造函数注入
指定冗余的
Mode=OneWay
/
Mode=TwoWay
使用控件默认值时省略
Mode
即可
ViewModel未实现
INotifyPropertyChanged
使用CommunityToolkit.Mvvm提供的
ObservableObject
或手动实现接口
在非UI线程修改
ObservableCollection
将修改逻辑包裹在
MainThread.BeginInvokeOnMainThread
热路径存在复杂的转换器链改为在ViewModel中预计算值
使用
x:DataType="x:Object"
绕过编译绑定
重构绑定逻辑;保留编译期安全校验
绑定到非公开属性绑定目标必须是
public
属性(字段会被忽略)

References

参考资料