Goal
Binding, CompiledBinding, MultiBinding, PriorityBinding, ElementName, RelativeSource) and imperative helpers via BindingOperations.ItemsControl/ListBox with data templates, SelectionModel, and compiled binding expressions.INotifyDataErrorInfo), asynchronous bindings, and reactive bridges (AvaloniaPropertyObservable).BindingDiagnostics logging.Why this matters
Prerequisites
Avalonia's binding engine lives under src/Avalonia.Base/Data. Key pieces:
DataContext: inherited down the logical tree. Most bindings resolve relative to the current element's DataContext.Binding: describes a path, mode, converter, fallback, etc.BindingBase: base for compiled bindings, multi bindings, priority bindings.BindingExpression: runtime evaluation created for each binding target.BindingOperations: static helpers to install, remove, or inspect bindings imperatively.ExpressionObserver: low-level observable pipeline underpinning async, compiled, and reactive bindings.Bindings resolve in this order:
Customer.Name).BindingOperations.SetBinding mirrors WPF/WinUI and is useful when you need to create bindings from code (for dynamic property names or custom controls). BindingOperations.ClearBinding removes them safely, keeping reference tracking intact.
Binding sources are resolved differently depending on the binding type:
DataContext inheritance – StyledElement.DataContext flows through the logical tree. Setting DataContext on a container automatically scopes child bindings.{Binding ElementName=Root, Path=Value} uses NameScope lookup to find another control.{Binding RelativeSource={RelativeSource AncestorType=ListBox}} walks the logical tree to find an ancestor of the specified type.{Binding Path=Bounds, RelativeSource={RelativeSource Self}} is handy when exposing properties of the control itself.{Binding Path=(local:ThemeOptions.AccentBrush)} reads attached or static properties registered as Avalonia properties.Avalonia also supports multi-level ancestor search and templated parent references:
<TextBlock Text="{Binding DataContext.Title, RelativeSource={RelativeSource AncestorType=Window}}"/>
<ContentControl ContentTemplate="{StaticResource CardTemplate}" />
<DataTemplate x:Key="CardTemplate" x:DataType="vm:Card">
<Border Background="{Binding Source={RelativeSource TemplatedParent}, Path=Background}"/>
</DataTemplate>
When creating controls dynamically, use BindingOperations.SetBinding so the engine tracks lifetimes and updates DataContext inheritance correctly:
var binding = new Binding
{
Path = "Person.FullName",
Mode = BindingMode.OneWay
};
BindingOperations.SetBinding(nameTextBlock, TextBlock.TextProperty, binding);
BindingOperations.ClearBinding(nameTextBlock, TextBlock.TextProperty) detaches it. To observe AvaloniaProperty values reactively, wrap them with AvaloniaPropertyObservable.Observe:
using System;
using System.Reactive.Linq;
using Avalonia.Reactive;
var textStream = AvaloniaPropertyObservable.Observe(this, TextBox.TextProperty)
.Select(value => value as string ?? string.Empty);
var subscription = textStream.Subscribe(text => ViewModel.TextLength = text.Length);
AvaloniaPropertyObservable lives in AvaloniaPropertyObservable.cs and bridges the binding system with IObservable<T> pipelines.
Dispose the subscription in OnDetachedFromVisualTree (or your view's Dispose pattern) to avoid leaks.
dotnet new avalonia.mvvm -o BindingPlayground
cd BindingPlayground
We'll expand MainWindow.axaml and MainWindowViewModel.cs.
View model implementing INotifyPropertyChanged:
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace BindingPlayground.ViewModels;
public class PersonViewModel : INotifyPropertyChanged
{
private string _firstName = "Ada";
private string _lastName = "Lovelace";
private int _age = 36;
public string FirstName
{
get => _firstName;
set { if (_firstName != value) { _firstName = value; OnPropertyChanged(); OnPropertyChanged(nameof(FullName)); } }
}
public string LastName
{
get => _lastName;
set { if (_lastName != value) { _lastName = value; OnPropertyChanged(); OnPropertyChanged(nameof(FullName)); } }
}
public int Age
{
get => _age;
set { if (_age != value) { _age = value; OnPropertyChanged(); } }
}
public string FullName => ($"{FirstName} {LastName}").Trim();
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
In MainWindow.axaml set the DataContext:
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:BindingPlayground.ViewModels"
x:Class="BindingPlayground.Views.MainWindow">
<Window.DataContext>
<vm:MainWindowViewModel />
</Window.DataContext>
<Design.DataContext>
<vm:MainWindowViewModel />
</Design.DataContext>
</Window>
Design.DataContext provides design-time data in the previewer.
<Grid ColumnDefinitions="*,*" RowDefinitions="Auto,*" Padding="16" RowSpacing="16" ColumnSpacing="24">
<TextBlock Grid.ColumnSpan="2" Classes="h1" Text="Binding basics"/>
<StackPanel Grid.Row="1" Spacing="8">
<TextBox Watermark="First name" Text="{Binding Person.FirstName, Mode=TwoWay}"/>
<TextBox Watermark="Last name" Text="{Binding Person.LastName, Mode=TwoWay}"/>
<NumericUpDown Minimum="0" Maximum="120" Value="{Binding Person.Age, Mode=TwoWay}"/>
</StackPanel>
<StackPanel Grid.Column="1" Grid.Row="1" Spacing="8">
<TextBlock Text="Live view" FontWeight="SemiBold"/>
<TextBlock Text="{Binding Person.FullName, Mode=OneWay}" FontSize="20"/>
<TextBlock Text="{Binding Person.Age, Mode=OneWay}"/>
<TextBlock Text="{Binding CreatedAt, Mode=OneTime, StringFormat='Created on {0:d}'}"/>
</StackPanel>
</Grid>
MainWindowViewModel holds Person and other state:
using System;
using System.Collections.ObjectModel;
namespace BindingPlayground.ViewModels;
public class MainWindowViewModel : INotifyPropertyChanged
{
public PersonViewModel Person { get; } = new();
public DateTime CreatedAt { get; } = DateTime.Now;
// Additional samples below
}
<StackPanel Margin="0,24,0,0" Spacing="6">
<Slider x:Name="VolumeSlider" Minimum="0" Maximum="100" Value="50"/>
<ProgressBar Minimum="0" Maximum="100" Value="{Binding #VolumeSlider.Value}"/>
</StackPanel>
#VolumeSlider targets the element with x:Name="VolumeSlider".
Use RelativeSource to bind to ancestors:
<TextBlock Text="{Binding DataContext.Person.FullName, RelativeSource={RelativeSource AncestorType=Window}}"/>
This binds to the window's DataContext even if the local control has its own DataContext.
Relative source syntax also supports Self (RelativeSource={RelativeSource Self}) and TemplatedParent for control templates.
Avalonia registers attached properties (e.g., ScrollViewer.HorizontalScrollBarVisibilityProperty) as AvaloniaProperty. Bind to them by wrapping the property name in parentheses:
<ListBox ItemsSource="{Binding Items}">
<ListBox.Styles>
<Style Selector="ListBox">
<Setter Property="(ScrollViewer.HorizontalScrollBarVisibility)" Value="Disabled"/>
<Setter Property="(ScrollViewer.VerticalScrollBarVisibility)" Value="Auto"/>
</Style>
</ListBox.Styles>
</ListBox>
<Border Background="{Binding (local:ThemeOptions.AccentBrush)}"/>
Attached property syntax also works inside Binding or MultiBinding. When setting them from code, use the generated static accessor (e.g., ScrollViewer.SetHorizontalScrollBarVisibility(listBox, ScrollBarVisibility.Disabled);).
Compiled bindings (CompiledBinding) produce strongly-typed accessors with better performance. Require x:DataType or CompiledBindings namespace:
xmlns:vm="clr-namespace:BindingPlayground.ViewModels"
x:DataType on a scope:<StackPanel DataContext="{Binding Person}" x:DataType="vm:PersonViewModel">
<TextBlock Text="{CompiledBinding FullName}"/>
<TextBox Text="{CompiledBinding FirstName}"/>
</StackPanel>
If x:DataType is set, CompiledBinding uses compile-time checking and generates binding code. Source: CompiledBindingExtension.cs.
Combine multiple values into one target:
public sealed class NameAgeFormatter : IMultiValueConverter
{
public object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
{
var name = values[0] as string ?? "";
var age = values[1] as int? ?? 0;
return $"{name} ({age})";
}
public object? ConvertBack(IList<object?> values, Type targetType, object? parameter, CultureInfo culture) => throw new NotSupportedException();
}
Register in resources:
<Window.Resources>
<conv:NameAgeFormatter x:Key="NameAgeFormatter"/>
</Window.Resources>
Use it:
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource NameAgeFormatter}">
<Binding Path="Person.FullName"/>
<Binding Path="Person.Age"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
Priority bindings try sources in order and use the first that yields a value:
<TextBlock>
<TextBlock.Text>
<PriorityBinding>
<Binding Path="OverrideTitle"/>
<Binding Path="Person.FullName"/>
<Binding Path="Person.FirstName"/>
<Binding Path="'Unknown user'"/>
</PriorityBinding>
</TextBlock.Text>
</TextBlock>
Source: PriorityBinding.cs.
MainWindowViewModel exposes collections:
public ObservableCollection<PersonViewModel> People { get; } = new()
{
new PersonViewModel { FirstName = "Ada", LastName = "Lovelace", Age = 36 },
new PersonViewModel { FirstName = "Grace", LastName = "Hopper", Age = 45 },
new PersonViewModel { FirstName = "Linus", LastName = "Torvalds", Age = 32 }
};
private PersonViewModel? _selectedPerson;
public PersonViewModel? SelectedPerson
{
get => _selectedPerson;
set { if (_selectedPerson != value) { _selectedPerson = value; OnPropertyChanged(); } }
}
Template the list:
<ListBox Items="{Binding People}"
SelectedItem="{Binding SelectedPerson, Mode=TwoWay}"
Height="180">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:PersonViewModel">
<StackPanel Orientation="Horizontal" Spacing="12">
<TextBlock Text="{CompiledBinding FullName}" FontWeight="SemiBold"/>
<TextBlock Text="{CompiledBinding Age}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Inside the details pane, bind to SelectedPerson safely using null-conditional binding (C#) or triggers. XAML automatically handles null (shows blank). Use x:DataType for compile-time checks.
SelectionModelFor advanced selection (multi-select, range), use SelectionModel<T> from SelectionModel.cs. Example:
public SelectionModel<PersonViewModel> PeopleSelection { get; } = new() { SelectionMode = SelectionMode.Multiple };
Bind it:
<ListBox Items="{Binding People}" Selection="{Binding PeopleSelection}"/>
INotifyDataErrorInfoImplement INotifyDataErrorInfo for asynchronous validation.
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
public class ValidatingPersonViewModel : PersonViewModel, INotifyDataErrorInfo
{
private readonly Dictionary<string, List<string>> _errors = new();
public bool HasErrors => _errors.Count > 0;
public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
public IEnumerable GetErrors(string? propertyName)
=> propertyName is not null && _errors.TryGetValue(propertyName, out var errors) ? errors : Array.Empty<string>();
protected override void OnPropertyChanged(string? propertyName)
{
base.OnPropertyChanged(propertyName);
Validate(propertyName);
}
private void Validate(string? propertyName)
{
if (propertyName is nameof(Age))
{
if (Age < 0 || Age > 120)
AddError(propertyName, "Age must be between 0 and 120");
else
ClearErrors(propertyName);
}
}
private void AddError(string propertyName, string error)
{
if (!_errors.TryGetValue(propertyName, out var list))
_errors[propertyName] = list = new List<string>();
if (!list.Contains(error))
{
list.Add(error);
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
}
}
private void ClearErrors(string propertyName)
{
if (_errors.Remove(propertyName))
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
}
}
Bind the validation feedback automatically:
<TextBox Text="{Binding ValidatingPerson.FirstName, Mode=TwoWay}"/>
<TextBox Text="{Binding ValidatingPerson.Age, Mode=TwoWay}"/>
<TextBlock Foreground="#B91C1C" Text="{Binding (Validation.Errors)[0].ErrorContent, RelativeSource={RelativeSource Self}}"/>
Avalonia surfaces validation errors via attached properties. For a full pattern see Validation.
Use Task-returning properties with Binding and BindingPriority.AsyncLocalValue. Example view model property:
private string? _weather;
public string? Weather
{
get => _weather;
private set { if (_weather != value) { _weather = value; OnPropertyChanged(); } }
}
public async Task LoadWeatherAsync()
{
Weather = "Loading...";
var result = await _weatherService.GetForecastAsync();
Weather = result;
}
Bind with fallback until the value arrives:
<TextBlock Text="{Binding Weather, FallbackValue='Fetching forecast...'}"/>
You can also bind directly to Task results using TaskObservableCollection or reactive extensions (Chapter 17 covers background work).
BindingDiagnostics.using Avalonia.Diagnostics;
public override void OnFrameworkInitializationCompleted()
{
BindingDiagnostics.Enable(
log => Console.WriteLine(log.Message),
new BindingDiagnosticOptions
{
Level = BindingDiagnosticLogLevel.Warning
});
base.OnFrameworkInitializationCompleted();
}
Source: BindingDiagnostics.cs.
Use TraceBindingFailures extension to log failures for specific bindings.
x:DataType to each data template and replace Binding with CompiledBinding where possible. Observe compile-time errors when property names are mistyped.FirstName, LastName, and Age into a sentence like "Ada Lovelace is 36 years old." Add a converter parameter for custom formats.FullName, falling back to initials if names are empty.INotifyDataErrorInfo and highlight inputs (Style Selector="TextBox:invalid").TextBlock for each person in a collection, use BindingOperations.SetBinding to wire TextBlock.Text, then ClearBinding when removing the item.TextBox.TextProperty through AvaloniaPropertyObservable.Observe and surface the text length in the UI.BindingDiagnostics to find it. Fix the binding and confirm logs clear.Binding.cs, BindingExpression.csBindingOperations.cs, ExpressionObserver.csCompiledBindingExtension.csMultiBinding.cs, PriorityBinding.csAvaloniaPropertyObservable.csSelectionModel.csValidation.csBindingDiagnostics.csCompiledBinding over Binding, and what prerequisites does it have?ElementName, RelativeSource, and attached property syntax change the binding source?MultiBinding, PriorityBinding, or programmatic calls to BindingOperations.SetBinding?AvaloniaPropertyObservable.Observe integrate with the binding engine, and when would you prefer it over classic bindings?What's next