maui-data-binding
Guidance for .NET MAUI XAML and C# data bindings — compiled bindings, INotifyPropertyChanged / ObservableObject, value converters, binding modes, multi-binding, relative bindings, fallbacks, and MVVM best practices. USE FOR: setting up compiled bindings with x:DataType, implement
Install
npx skills add https://github.com/dotnet/skills/tree/main/plugins/dotnet-maui/skills/maui-data-binding
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install dotnet-skills@llmmart
git clone https://github.com/dotnet/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole dotnet/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
.NET MAUI Data Binding
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.
When to Use
- Adding
x:DataTypecompiled bindings to a new or existing page - Implementing
INotifyPropertyChangedor CommunityToolkitObservableObject - Creating or consuming
IValueConverter/IMultiValueConverter - Choosing the correct
BindingModefor a control property - Setting
BindingContextin XAML or code-behind - Using relative bindings (
Self,AncestorType,TemplatedParent) - Applying
StringFormat,FallbackValue, orTargetNullValue - Writing AOT-safe code bindings with
SetBindingand lambdas (.NET 9+)
When Not to Use
- CollectionView layouts / templates — use the
maui-collectionviewskill - Shell navigation parameters — use the
maui-shell-navigationskill - Service registration / DI — use the
maui-dependency-injectionskill - Property-change-triggered animations — use built-in .NET MAUI animation APIs
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)
Rules That Change the Answer
Apply these to every binding answer — they are the differences between "it compiles" and "it actually updates the UI".
| Situation | Do this | Not this |
|---|---|---|
Deciding where x:DataType goes |
Put it wherever a binding scope starts — the page/view root, and each DataTemplate |
Scattering it on arbitrary children that share the parent's BindingContext |
| A binding falls back to reflection (XC0022 / XC0023) | Add the right x:DataType for that binding scope; for XC0023 remove the explicit x:DataType="{x:Null}" |
x:DataType="x:Object" to silence it — this disables compile-time checking |
A DataTemplate inherits x:DataType from an outer scope (XC0024) |
Give the DataTemplate its own x:DataType |
Leaving it to resolve against the wrong type |
| ViewModel change notification | ObservableObject + [ObservableProperty], or implement INotifyPropertyChanged |
A plain POCO base class — bindings will never update |
| Bindings show blank | Check BindingContext is actually set |
Assuming the binding path is wrong |
| Enforcing compiled bindings | Set MauiEnableXamlCBindingWithSourceCompilation to true, then <WarningsAsErrors>XC0022;XC0025</WarningsAsErrors> |
Promoting XC0025 without the switch if the project uses Source= / RelativeSource bindings |
Do not restructure a ViewModel or add a converter that the user did not ask for
and that fixes no real defect. Adding x:DataType is different: when you are
already editing a page's bindings, recommending compiled bindings is in scope.
Compiled Bindings — x:DataType Placement
Compiled bindings are 8–20× faster than reflection-based bindings and are
required for NativeAOT / trimming. Enable them with x:DataType.
Placement rules
Set x:DataType only where BindingContext is set:
- Page / View root — where you assign
BindingContext. - 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.
<!-- ✅ 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>
DataTemplate always needs its own x:DataType
<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 used without x:DataType in scope — not compiled, falls back to reflection |
| XC0023 | Binding not compiled because x:DataType is explicitly null |
| XC0024 | x:DataType came from an outer scope — annotate the DataTemplate with its own x:DataType |
| XC0025 | Binding not compiled because it has an explicit Source — enable <MauiEnableXamlCBindingWithSourceCompilation> |
These four codes are verified against .NET 10 / .NET 11 MAUI (
Build.Tasks/BuildException.cs,ErrorMessages.resx). Diagnostic numbering is SDK-band-sensitive — re-check againstBuildException.csbefore relying on it on a newer SDK.
Add to the .csproj:
<!-- Compile bindings that use Source= as well; otherwise XC0025 fires on every
Source= / RelativeSource binding. As of .NET 10/11 this is on by default
only for AOT / full-trim builds. -->
<MauiEnableXamlCBindingWithSourceCompilation>true</MauiEnableXamlCBindingWithSourceCompilation>
<WarningsAsErrors>XC0022;XC0025</WarningsAsErrors>
If you promote XC0025 without enabling that switch, make sure the project has no
Source= / RelativeSource bindings — otherwise they will be reported.
Binding Modes
Set Mode explicitly only when overriding the default. Most properties
already have the correct default:
| Mode | Direction | Use case |
|---|---|---|
OneWay |
Source → Target | Display-only (default for most properties) |
TwoWay |
Source ↔ Target | Editable controls (Entry.Text, Switch.IsToggled) |
OneWayToSource |
Target → Source | Read user input without pushing back to UI |
OneTime |
Source → Target (once) | Static values; no change-tracking overhead |
<!-- ✅ 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}" />
BindingContext and Property Paths
Every BindableObject inherits BindingContext from its parent unless
explicitly set. Property paths support dot notation and indexers:
<Label Text="{Binding Address.City}" />
<Label Text="{Binding Items[0].Name}" />
Set BindingContext in XAML:
<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):
public MainPage(MainViewModel vm)
{
InitializeComponent();
BindingContext = vm;
}
INotifyPropertyChanged and ObservableObject
Manual implementation
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)
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.
Value Converters — IValueConverter
Implement Convert (source → target) and ConvertBack (target → source):
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:
<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:
<Label Text="{Binding Score, Converter={StaticResource ThresholdConverter},
ConverterParameter=50}" />
Multi-Binding
Combine multiple source values with 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();
}
Relative Bindings
| Source | Syntax | Use 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 |
<!-- Square box: Height = Width -->
<BoxView WidthRequest="100"
HeightRequest="{Binding Source={RelativeSource Self}, Path=WidthRequest}" />
StringFormat
Use Binding.StringFormat for simple display formatting without a converter:
<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 Fallbacks
- FallbackValue — used when the binding path cannot be resolved or the converter throws.
- TargetNullValue — used when the bound value is
null.
<Label Text="{Binding MiddleName, TargetNullValue='(none)',
FallbackValue='unavailable'}" />
<Image Source="{Binding AvatarUrl, TargetNullValue='default_avatar.png'}" />
.NET 9+ Code Bindings (AOT-safe)
Fully AOT-safe, no reflection:
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:
// ✅ Safe — PropertyChanged is auto-marshalled
await Task.Run(() => Title = "Loaded");
// ⚠️ ObservableCollection.Add — dispatch to UI thread
MainThread.BeginInvokeOnMainThread(() => Items.Add(newItem));
Common Pitfalls
| Mistake | Fix |
|---|---|
Missing x:DataType — bindings silently fall back to reflection |
Add x:DataType at page root and every DataTemplate; promote XC0022 (see Enforce binding warnings as errors) |
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 paths | Pre-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 properties | Binding targets must be public properties (fields are ignored) |
References
Files (skills)
-
references
-
data-binding-api.md 4 KB
# Data Binding API Reference ## Binding Modes | Mode | Direction | Use case | |------|-----------|----------| | `OneWay` | Source → Target | Display-only (default for most properties) | | `TwoWay` | Source ↔ Target | Editable controls (`Entry.Text`, `Switch.IsToggled`) | | `OneWayToSource` | Target → Source | Read user input without pushing back to UI | | `OneTime` | Source → Target (once) | Static values; no change tracking overhead | ## BindingContext and Property Paths - 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 or code-behind: ```xml <ContentPage xmlns:vm="clr-namespace:MyApp.ViewModels" x:DataType="vm:MainViewModel"> <ContentPage.BindingContext> <vm:MainViewModel /> </ContentPage.BindingContext> </ContentPage> ``` ## IValueConverter Implement `IValueConverter` with `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; } ``` ### Declaring converters in XAML resources ```xml <ContentPage.Resources> <local:IntToBoolConverter x:Key="IntToBool" /> </ContentPage.Resources> <Switch IsToggled="{Binding Count, Converter={StaticResource IntToBool}}" /> ``` ### ConverterParameter `ConverterParameter` is always passed as a **string**. Parse it inside `Convert`: ```xml <Label Text="{Binding Score, Converter={StaticResource ThresholdConverter}, ConverterParameter=50}" /> ``` ```csharp int threshold = int.Parse((string)parameter); ``` ## 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}'}" /> ``` > **Note:** Wrap the format string in single quotes when it contains commas or braces. ## 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(); } ``` ## Relative Bindings | Source | Syntax | Use 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 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'}" /> ```
-
-
SKILL.md 14.5 KB
--- name: maui-data-binding description: >- Guidance for .NET MAUI XAML and C# data bindings — compiled bindings, INotifyPropertyChanged / ObservableObject, value converters, binding modes, multi-binding, relative bindings, fallbacks, and MVVM best practices. USE FOR: setting up compiled bindings with x:DataType, implementing INotifyPropertyChanged or CommunityToolkit ObservableObject, creating IValueConverter / IMultiValueConverter, choosing binding modes, configuring BindingContext, relative bindings, binding fallbacks, StringFormat, code-behind SetBinding with lambdas, and enforcing XC0022/XC0025 warnings. DO NOT USE FOR: CollectionView item templates and layouts (use maui-collectionview), Shell navigation data passing (use maui-shell-navigation), dependency injection (use maui-dependency-injection), or animations triggered by property changes (use .NET MAUI animation APIs). license: MIT --- # .NET MAUI Data Binding 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. ## 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+) ## 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](https://learn.microsoft.com/dotnet/maui/user-interface/animation/basic) ## 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) ## Rules That Change the Answer Apply these to every binding answer — they are the differences between "it compiles" and "it actually updates the UI". | Situation | Do this | Not this | |---|---|---| | Deciding where `x:DataType` goes | Put it wherever a binding scope starts — the page/view root, and **each** `DataTemplate` | Scattering it on arbitrary children that share the parent's `BindingContext` | | A binding falls back to reflection (XC0022 / XC0023) | Add the right `x:DataType` for that binding scope; for XC0023 remove the explicit `x:DataType="{x:Null}"` | `x:DataType="x:Object"` to silence it — this disables compile-time checking | | A `DataTemplate` inherits `x:DataType` from an outer scope (XC0024) | Give the `DataTemplate` its **own** `x:DataType` | Leaving it to resolve against the wrong type | | ViewModel change notification | `ObservableObject` + `[ObservableProperty]`, or implement `INotifyPropertyChanged` | A plain POCO base class — bindings will never update | | Bindings show blank | Check `BindingContext` is actually set | Assuming the binding path is wrong | | Enforcing compiled bindings | Set `MauiEnableXamlCBindingWithSourceCompilation` to `true`, **then** `<WarningsAsErrors>XC0022;XC0025</WarningsAsErrors>` | Promoting `XC0025` without the switch if the project uses `Source=` / `RelativeSource` bindings | **Do not** restructure a ViewModel or add a converter that the user did not ask for and that fixes no real defect. Adding `x:DataType` is different: when you are already editing a page's bindings, recommending compiled bindings is in scope. --- ## Compiled Bindings — x:DataType Placement Compiled bindings are **8–20× faster** than reflection-based bindings and are required for NativeAOT / trimming. Enable them with `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> ``` ### DataTemplate always needs its own x:DataType ```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 used **without `x:DataType` in scope** — not compiled, falls back to reflection | | **XC0023** | Binding not compiled because `x:DataType` is **explicitly `null`** | | **XC0024** | `x:DataType` came from an **outer scope** — annotate the `DataTemplate` with its own `x:DataType` | | **XC0025** | Binding not compiled because it has an explicit **`Source`** — enable `<MauiEnableXamlCBindingWithSourceCompilation>` | > These four codes are **verified against .NET 10 / .NET 11 MAUI** > (`Build.Tasks/BuildException.cs`, `ErrorMessages.resx`). Diagnostic numbering is > SDK-band-sensitive — re-check against `BuildException.cs` before relying on it on a > newer SDK. Add to the `.csproj`: ```xml <!-- Compile bindings that use Source= as well; otherwise XC0025 fires on every Source= / RelativeSource binding. As of .NET 10/11 this is on by default only for AOT / full-trim builds. --> <MauiEnableXamlCBindingWithSourceCompilation>true</MauiEnableXamlCBindingWithSourceCompilation> <WarningsAsErrors>XC0022;XC0025</WarningsAsErrors> ``` If you promote `XC0025` without enabling that switch, make sure the project has no `Source=` / `RelativeSource` bindings — otherwise they will be reported. --- ## Binding Modes Set `Mode` explicitly **only** when overriding the default. Most properties already have the correct default: | Mode | Direction | Use case | |------|-----------|----------| | `OneWay` | Source → Target | Display-only (default for most properties) | | `TwoWay` | Source ↔ Target | Editable controls (`Entry.Text`, `Switch.IsToggled`) | | `OneWayToSource` | Target → Source | Read 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}" /> ``` --- ## BindingContext and Property Paths 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; } ``` --- ## INotifyPropertyChanged and 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))); } } } } ``` ### CommunityToolkit.Mvvm (recommended) ```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. --- ## Value Converters — 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}" /> ``` --- ## 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(); } ``` --- ## Relative Bindings | Source | Syntax | Use 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}" /> ``` --- ## 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 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'}" /> ``` --- ## .NET 9+ Code Bindings (AOT-safe) 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()); ``` --- ## 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)); ``` --- ## Common Pitfalls | Mistake | Fix | |---------|-----| | Missing `x:DataType` — bindings silently fall back to reflection | Add `x:DataType` at page root and every `DataTemplate`; promote `XC0022` (see [Enforce binding warnings as errors](#enforce-binding-warnings-as-errors)) | | 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 paths | Pre-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 properties | Binding targets must be `public` properties (fields are ignored) | --- ## References - [Data binding overview](https://learn.microsoft.com/dotnet/maui/fundamentals/data-binding/) - [Compiled bindings](https://learn.microsoft.com/dotnet/maui/fundamentals/data-binding/compiled-bindings) - [Value converters](https://learn.microsoft.com/dotnet/maui/fundamentals/data-binding/converters) - [Relative bindings](https://learn.microsoft.com/dotnet/maui/fundamentals/data-binding/relative-bindings) - [Multi-bindings](https://learn.microsoft.com/dotnet/maui/fundamentals/data-binding/multibindings) - [CommunityToolkit.Mvvm](https://learn.microsoft.com/dotnet/communitytoolkit/mvvm/)
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.