Goal
Why this matters
Binding, CompiledBindingFactory, IResourceHost, Style) remove stringly-typed errors and enable richer refactoring tools.Prerequisites
Avalonia's binding engine is expressed via Binding (external/Avalonia/src/Avalonia.Base/Data/Binding.cs). Construct bindings with property paths, modes, converters, and validation:
var binding = new Binding("Customer.Name")
{
Mode = BindingMode.TwoWay,
UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
ValidatesOnExceptions = true
};
nameTextBox.Bind(TextBox.TextProperty, binding);
Bind is an extension method on AvaloniaObject (see BindingExtensions). The same API supports command bindings:
saveButton.Bind(Button.CommandProperty, new Binding("SaveCommand"));
For one-time assignments, use BindingMode.OneTime. When you need relative bindings (RelativeSource in XAML), use RelativeSource objects:
var binding = new Binding
{
RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor)
{
AncestorType = typeof(Window)
},
Path = nameof(Window.Title)
};
header.Bind(TextBlock.TextProperty, binding);
Avalonia supports indexer paths (dictionary or list access) via the same Binding.Path syntax used in XAML.
var statusText = new TextBlock();
statusText.Bind(TextBlock.TextProperty, new Binding("Statuses[SelectedStatus]"));
Internally the binding engine uses IndexerNode (see ExpressionNodes). You still get change notifications when the indexer raises property change events (INotifyPropertyChanged + IndexerName). For dynamic dictionaries, call RaisePropertyChanged("Item[]") on changes.
CompiledBindingFactoryCompiled bindings avoid reflection at runtime. Create a factory and supply strongly-typed accessors, mirroring {CompiledBinding} usage.
var factory = new CompiledBindingFactory();
var compiled = factory.Create<DashboardViewModel, string>(
vmGetter: static vm => vm.Header,
vmSetter: static (vm, value) => vm.Header = value,
name: nameof(DashboardViewModel.Header),
mode: BindingMode.TwoWay);
headerText.Bind(TextBlock.TextProperty, compiled);
CompiledBindingFactory resides in Avalonia.Data.Core. Pass BindingPriority if you need to align with style triggers. Because compiled bindings capture delegates, they work well with source generators or analyzers.
Create extension methods to reduce boilerplate:
public static class BindingHelpers
{
public static T BindValue<T, TValue>(this T control, AvaloniaProperty<TValue> property, string path,
BindingMode mode = BindingMode.Default) where T : AvaloniaObject
{
control.Bind(property, new Binding(path) { Mode = mode });
return control;
}
}
Use them when composing views:
var searchBox = new TextBox()
.BindValue(TextBox.TextProperty, nameof(SearchViewModel.Query), BindingMode.TwoWay);
Avalonia surfaces validation errors via BindingNotification. In code you set validation options on binding instances:
var amountBinding = new Binding("Amount")
{
Mode = BindingMode.TwoWay,
ValidatesOnDataErrors = true,
ValidatesOnExceptions = true
};
amountTextBox.Bind(TextBox.TextProperty, amountBinding);
Listen for errors using BindingObserver or property change notifications on DataValidationErrors (see external/Avalonia/src/Avalonia.Controls/DataValidationErrors.cs). Example hooking into the attached property:
amountTextBox.GetObservable(DataValidationErrors.HasErrorsProperty)
.Subscribe(hasErrors => amountTextBox.Classes.Set(":invalid", hasErrors));
Instantiate converters directly and assign them to Binding.Converter:
var converter = new BooleanToVisibilityConverter();
var binding = new Binding("IsBusy")
{
Converter = converter
};
spinner.Bind(IsVisibleProperty, binding);
For inline converters, create lambda-based converter classes implementing IValueConverter. In code-first setups you can keep converter definitions close to usage.
MultiBinding lives in Avalonia.Base/Data/MultiBinding.cs. Configure binding collection and converters directly.
var multi = new MultiBinding
{
Bindings =
{
new Binding("FirstName"),
new Binding("LastName")
},
Converter = FullNameConverter.Instance
};
fullNameText.Bind(TextBlock.TextProperty, multi);
FullNameConverter implements IMultiValueConverter. When multi-binding in code, consider static singletons to avoid allocations.
Avalonia command support is just binding to ICommand. With code-first patterns, leverage ReactiveCommand or custom commands while still using Bind:
refreshButton.Bind(Button.CommandProperty, new Binding("RefreshCommand"));
To observe property changes for reactive flows, use GetObservable or PropertyChanged events. Combine with ReactiveUI by using WhenAnyValue inside view models—code-first views don’t change this interop.
ResourceDictionary is just a C# collection (see external/Avalonia/src/Avalonia.Base/Controls/ResourceDictionary.cs). Create dictionaries and merge them programmatically.
var typographyResources = new ResourceDictionary
{
["Heading.FontSize"] = 24.0,
["Body.FontSize"] = 14.0
};
Application.Current!.Resources.MergedDictionaries.Add(typographyResources);
For per-control resources:
var card = new Border
{
Resources =
{
["CardBackground"] = Brushes.White,
["CardShadow"] = new BoxShadow { Color = Colors.Black, Opacity = 0.1, Blur = 8 }
}
};
Resources property is itself a ResourceDictionary. Use strongly-typed wrapper classes to centralize resource keys:
public static class ResourceKeys
{
public const string AccentBrush = nameof(AccentBrush);
public const string AccentForeground = nameof(AccentForeground);
}
var accent = (IBrush)Application.Current!.Resources[ResourceKeys.AccentBrush];
Wrap lookups with helper methods to provide fallbacks:
public static TResource GetResource<TResource>(this IResourceHost host, string key, TResource fallback)
{
return host.TryFindResource(key, out var value) && value is TResource typed
? typed
: fallback;
}
IResourceHost/IResourceProvider interfaces are defined in Avalonia.Styling. Controls implement them, so you can call control.TryFindResource directly.
Style objects can be constructed with selectors and setters. The selector API mirrors XAML but uses lambda syntax.
var buttonStyle = new Style(x => x.OfType<Button>().Class("primary"))
{
Setters =
{
new Setter(Button.BackgroundProperty, Brushes.MediumPurple),
new Setter(Button.ForegroundProperty, Brushes.White),
new Setter(Button.PaddingProperty, new Thickness(20, 10))
},
Triggers =
{
new Trigger
{
Property = Button.IsPointerOverProperty,
Value = true,
Setters = { new Setter(Button.BackgroundProperty, Brushes.DarkMagenta) }
}
}
};
Add styles to Application.Current.Styles or to a specific control’s Styles collection. Remember to freeze brushes (call ToImmutable() or use static brushes) when reusing them widely.
You can still load existing .axaml resources via StyleInclude, or create purely code-based ones:
var theme = new Styles
{
new StyleInclude(new Uri("avares://App/Styles"))
{
Source = new Uri("avares://App/Styles/Buttons.axaml")
},
buttonStyle
};
Application.Current!.Styles.AddRange(theme);
In pure C#, Styles is just a list. If you don’t have AddRange, iterate:
foreach (var style in theme)
{
Application.Current!.Styles.Add(style);
}
Theme variants (ThemeVariant) can be set directly on styles:
buttonStyle.Resources[ThemeVariant.Light] = Brushes.Black;
buttonStyle.Resources[ThemeVariant.Dark] = Brushes.White;
Encapsulate binding creation in dedicated classes to avoid scattering strings:
public static class DashboardBindings
{
public static Binding TotalSales => new(nameof(DashboardViewModel.TotalSales)) { Mode = BindingMode.OneWay };
public static Binding RefreshCommand => new(nameof(DashboardViewModel.RefreshCommand));
}
salesText.Bind(TextBlock.TextProperty, DashboardBindings.TotalSales);
refreshButton.Bind(Button.CommandProperty, DashboardBindings.RefreshCommand);
Use expression trees to produce path strings while maintaining compile-time checks:
public static class BindingFactory
{
public static Binding Create<TViewModel, TValue>(Expression<Func<TViewModel, TValue>> expression,
BindingMode mode = BindingMode.Default)
{
var path = ExpressionHelper.GetMemberPath(expression); // custom helper
return new Binding(path) { Mode = mode };
}
}
ExpressionHelper can walk the expression tree to build Customer.Addresses[0].City style paths, ensuring refactors update bindings.
Provide factories for resource dictionaries similar to style factories:
public static class ResourceFactory
{
public static ResourceDictionary CreateColors() => new()
{
[ResourceKeys.AccentBrush] = new SolidColorBrush(Color.Parse("#4F8EF7")),
[ResourceKeys.AccentForeground] = Brushes.White
};
}
Merge them in App.Initialize() or feature modules when needed.
Metrics["TotalRevenue"] from a dictionary-backed view-model. Raise change notifications on dictionary updates and verify the UI refreshes.:invalid pseudo-class template to controls with validation errors. Trigger validation via a headless test.Styles collections (light/dark) in code, swap them at runtime, and ensure all bindings to theme resources update automatically. Validate behaviour with a headless pixel test (Chapter 40).With bindings, resources, and styles expressed in code, your Avalonia app gains powerful refactorability and testability. Embrace the fluent APIs and helper patterns to keep code-first UI as expressive as any XAML counterpart.
What's next