maui-data-binding
>-
Works with
Claude CodeCursorCodex CLIGitHub CopilotGemini CLI
---
name: maui-data-binding
description: >-
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/)More General & Other skills
find-skills
vercel-labs/skills
Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill.
1.5M
grill-me
mattpocock/skills
A relentless interview to sharpen a plan or design.
972.7k
grill-with-docs
mattpocock/skills
A relentless interview to sharpen a plan or design, which also creates docs (ADR's and glossary) as we go.
828.8k

