skills/davidortinau/maui-skills/maui-data-binding

maui-data-binding

SKILL.md

.NET MAUI Data Binding

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

Set explicitly only when the property default doesn't match your intent. Do not specify Mode=OneWay on properties where OneWay is already the default (e.g. Label.Text, Image.Source) — it adds noise without changing behavior.

<!-- ✅ OneTime overrides the default — be explicit -->
<Label Text="{Binding Title, Mode=OneTime}" />
<!-- ✅ OneWayToSource overrides the default — be explicit -->
<Entry Text="{Binding SearchQuery, Mode=OneWayToSource}" />

<!-- ✅ Defaults — omit Mode -->
<Label Text="{Binding Score}" />           <!-- OneWay is the default for Label.Text -->
<Entry Text="{Binding UserName}" />        <!-- TwoWay is the default for Entry.Text -->
<Switch IsToggled="{Binding DarkMode}" />  <!-- TwoWay is the default for Switch.IsToggled -->

<!-- ❌ Redundant — these just restate the default -->
<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 or code-behind:
<ContentPage xmlns:vm="clr-namespace:MyApp.ViewModels"
             x:DataType="vm:MainViewModel">
    <ContentPage.BindingContext>
        <vm:MainViewModel />
    </ContentPage.BindingContext>
</ContentPage>

Compiled Bindings

Compiled bindings resolve at build time, delivering 8–20× faster binding resolution than reflection-based bindings.

Enabling compiled bindings

Declare x:DataType on the element or an ancestor:

<ContentPage x:DataType="vm:MainViewModel">
    <Label Text="{Binding UserName}" />
</ContentPage>

Where to place x:DataType

x:DataType should only be declared at levels where BindingContext is set:

  1. Page root – where you assign BindingContext (in XAML or code-behind).
  2. DataTemplate – which creates a new binding scope with a different type.

Do not scatter x:DataType on intermediate child elements. Children inherit the x:DataType from their ancestor, just as they inherit BindingContext. 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 only where BindingContext is set -->
<ContentPage x:DataType="vm:MainViewModel">
    <StackLayout>
        <Label Text="{Binding Title}" />
        <Slider Value="{Binding Progress}" />
        <GraphicsView />
    </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}" />
        <GraphicsView x:DataType="x:Object" />
    </StackLayout>
</ContentPage>

DataTemplate requires its own x:DataType

DataTemplate creates a new binding scope. Always redeclare:

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

Compiler warnings

Warning Meaning
XC0022 Binding path not found on the declared x:DataType
XC0023 Property is not bindable
XC0024 x:DataType type not found
XC0025 Binding used without x:DataType (non-compiled fallback)

Treat these as errors in CI: <WarningsAsErrors>XC0022;XC0025</WarningsAsErrors>.

.NET 9+ compiled code bindings (SetBinding with lambda)

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

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

IValueConverter

Implement IValueConverter with 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;
}

Declaring converters in XAML resources

<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:

<Label Text="{Binding Score, Converter={StaticResource ThresholdConverter},
              ConverterParameter=50}" />
int threshold = int.Parse((string)parameter);

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}'}" />

Note: Wrap the format string in single quotes when it contains commas or braces.

Multi-Binding

Combine multiple source values with IMultiValueConverter:

<Label>
    <Label.Text>
        <MultiBinding Converter="{StaticResource FullNameConverter}"
                      StringFormat="{}{0}">
            <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=Width} Bind to own properties
Ancestor {Binding Source={RelativeSource AncestorType={x:Type vm:ParentVM}}, Path=Title} 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}" />

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'}" />

Threading

MAUI automatically marshals property-change notifications to the UI thread. You can raise PropertyChanged from any thread; the binding engine dispatches the update to the main thread.

// Safe from a background thread
await Task.Run(() =>
{
    Items = LoadData();          // Raises PropertyChanged
    OnPropertyChanged(nameof(Items));
});

Caveat: Direct ObservableCollection mutations (Add/Remove) from background threads may still require MainThread.BeginInvokeOnMainThread.

Performance

  • Reflection overhead: Non-compiled bindings use reflection to resolve paths at runtime—measurably slower on large lists and startup.
  • Compiled bindings eliminate reflection; always prefer them.
  • NativeAOT / trimming: Reflection-based bindings may break under trimming. Compiled bindings (XAML x:DataType or code SetBinding with lambdas) are trimmer- and AOT-safe.
  • Avoid complex converter chains in hot paths; pre-compute values in the ViewModel instead.
  • Use OneTime mode for truly static data to skip change-tracking registration.
Weekly Installs
22
GitHub Stars
71
First Seen
Feb 17, 2026
Installed on
codex21
opencode20
gemini-cli19
github-copilot19
kimi-cli19
amp19