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 compiled bindings to a new or existing page
x:DataType - Implementing or CommunityToolkit
INotifyPropertyChangedObservableObject - Creating or consuming /
IValueConverterIMultiValueConverter - Choosing the correct for a control property
BindingMode - Setting in XAML or code-behind
BindingContext - Using relative bindings (,
Self,AncestorType)TemplatedParent - Applying ,
StringFormat, orFallbackValueTargetNullValue - Writing AOT-safe code bindings with and lambdas (.NET 9+)
SetBinding
- 为新页面或现有页面添加编译绑定
x:DataType - 实现或CommunityToolkit
INotifyPropertyChangedObservableObject - 创建或使用/
IValueConverterIMultiValueConverter - 为控件属性选择正确的
BindingMode - 在XAML或代码后置中设置
BindingContext - 使用相对绑定(、
Self、AncestorType)TemplatedParent - 应用、
StringFormat或FallbackValueTargetNullValue - 使用和lambda编写AOT安全的代码绑定(.NET 9及以上版本)
SetBinding
When Not to Use
不适用场景
- CollectionView layouts / templates — use the skill
maui-collectionview - Shell navigation parameters — use the skill
maui-shell-navigation - Service registration / DI — use the skill
maui-dependency-injection - 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:DataTypePlacement rules
放置规则
Set only where is set:
x:DataTypeBindingContext- Page / View root — where you assign .
BindingContext - DataTemplate — which creates a new binding scope.
Do not scatter on arbitrary child elements. Adding
on children to escape compiled bindings is an
anti-pattern — it disables compile-time checking and reintroduces reflection.
x:DataTypex:DataType="x:Object"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>仅在设置了的位置设置:
BindingContextx:DataType- 页面/视图根节点 —— 你赋值的位置
BindingContext - DataTemplate —— 会创建新的绑定作用域
不要在任意子元素上随意添加。在子元素上添加来绕过编译绑定是反模式——它会禁用编译期检查并重新引入反射机制。
x:DataTypex: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
将绑定警告设为错误强制执行
| Warning | Meaning |
|---|---|
| XC0022 | Binding path not found on the declared |
| XC0023 | Property is not bindable |
| XC0024 | |
| XC0025 | Binding used without |
Add to the :
.csprojxml
<WarningsAsErrors>XC0022;XC0025</WarningsAsErrors>| 警告编号 | 含义 |
|---|---|
| XC0022 | 在声明的 |
| XC0023 | 属性不可绑定 |
| XC0024 | 找不到 |
| XC0025 | 未使用 |
在中添加配置:
.csprojxml
<WarningsAsErrors>XC0022;XC0025</WarningsAsErrors>Binding Modes
绑定模式
Set explicitly only when overriding the default. Most properties
already have the correct default:
Mode| Mode | Direction | Use case |
|---|---|---|
| Source → Target | Display-only (default for most properties) |
| Source ↔ Target | Editable controls ( |
| Target → Source | Read user input without pushing back to UI |
| 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| 模式 | 数据流方向 | 适用场景 |
|---|---|---|
| 源 → 目标 | 仅展示内容(绝大多数属性的默认值) |
| 源 ↔ 目标 | 可编辑控件( |
| 目标 → 源 | 读取用户输入但不回写到UI |
| 源 → 目标(仅一次) | 静态值;无变更追踪开销 |
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 inherits from its parent unless
explicitly set. Property paths support dot notation and indexers:
BindableObjectBindingContextxml
<Label Text="{Binding Address.City}" />
<Label Text="{Binding Items[0].Name}" />Set in XAML:
BindingContextxml
<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;
}所有都会从父元素继承,除非显式设置。属性路径支持点表示法和索引器:
BindableObjectBindingContextxml
<Label Text="{Binding Address.City}" />
<Label Text="{Binding Items[0].Name}" />在XAML中设置:
BindingContextxml
<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 property, raise,
and automatically.
TitlePropertyChangedLoadDataCommandcsharp
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
public partial class MainViewModel : ObservableObject
{
[ObservableProperty]
private string _title = string.Empty;
[RelayCommand]
private async Task LoadDataAsync() { /* ... */ }
}源生成器会自动创建属性、触发逻辑和。
TitlePropertyChangedLoadDataCommandValue Converters — IValueConverter
值转换器——IValueConverter
Implement (source → target) and (target → source):
ConvertConvertBackcsharp
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}}" />ConverterParameterConvertxml
<Label Text="{Binding Score, Converter={StaticResource ThresholdConverter},
ConverterParameter=50}" />实现(源→目标)和(目标→源)方法:
ConvertConvertBackcsharp
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}}" />ConverterParameterConvertxml
<Label Text="{Binding Score, Converter={StaticResource ThresholdConverter},
ConverterParameter=50}" />Multi-Binding
多绑定
Combine multiple source values with :
IMultiValueConverterxml
<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();
}使用合并多个源的值:
IMultiValueConverterxml
<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
相对绑定
| Source | Syntax | Use case |
|---|---|---|
| Self | | Bind to own properties |
| Ancestor | | Reach parent BindingContext |
| TemplatedParent | | Inside ControlTemplate |
xml
<!-- Square box: Height = Width -->
<BoxView WidthRequest="100"
HeightRequest="{Binding Source={RelativeSource Self}, Path=WidthRequest}" />| 源类型 | 语法 | 适用场景 |
|---|---|---|
| 自身 | | 绑定到自身属性 |
| 祖先元素 | | 访问父元素的BindingContext |
| 模板父级 | | 在ControlTemplate内部使用 |
xml
<!-- 正方形盒子:高度等于宽度 -->
<BoxView WidthRequest="100"
HeightRequest="{Binding Source={RelativeSource Self}, Path=WidthRequest}" />StringFormat
StringFormat
Use for simple display formatting without a converter:
Binding.StringFormatxml
<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.StringFormatxml
<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 to the UI thread — you can raise
it from any thread. However, direct mutations
(Add / Remove) from background threads may crash:
PropertyChangedObservableCollectioncsharp
// ✅ Safe — PropertyChanged is auto-marshalled
await Task.Run(() => Title = "Loaded");
// ⚠️ ObservableCollection.Add — dispatch to UI thread
MainThread.BeginInvokeOnMainThread(() => Items.Add(newItem));MAUI会自动将封送到UI线程——你可以在任意线程触发该事件。但是,在后台线程直接修改(增/删操作)可能会导致崩溃:
PropertyChangedObservableCollectioncsharp
// ✅ 安全——PropertyChanged会自动封送
await Task.Run(() => Title = "Loaded");
// ⚠️ ObservableCollection.Add操作——需要调度到UI线程
MainThread.BeginInvokeOnMainThread(() => Items.Add(newItem));Common Pitfalls
常见问题
| Mistake | Fix |
|---|---|
Missing | Add |
Forgetting to set | Set in XAML ( |
Specifying redundant | Omit |
ViewModel does not implement | Use |
Mutating | Wrap mutations in |
| Complex converter chains in hot paths | Pre-compute values in the ViewModel instead |
Using | Restructure bindings; keep compile-time safety |
| Binding to non-public properties | Binding targets must be |
| 错误 | 修复方案 |
|---|---|
缺失 | 在页面根节点和每个 |
忘记设置 | 在XAML中通过 |
指定冗余的 | 使用控件默认值时省略 |
ViewModel未实现 | 使用CommunityToolkit.Mvvm提供的 |
在非UI线程修改 | 将修改逻辑包裹在 |
| 热路径存在复杂的转换器链 | 改为在ViewModel中预计算值 |
使用 | 重构绑定逻辑;保留编译期安全校验 |
| 绑定到非公开属性 | 绑定目标必须是 |