| name | avalonia-property-system |
| description | Use when working with Avalonia's property system: StyledProperty, DirectProperty, AttachedProperty, property inheritance, metadata override, coercion, value priority, or observing property changes via GetObservable or OnPropertyChanged. |
Avalonia Property System
Overview
AvaloniaProperty is the foundation of Avalonia's data binding, styling, and animations. Three kinds:
- StyledProperty — full-featured; supports styling, animation, inheritance
- DirectProperty — lightweight, CLR-field-backed; no styling/animation support
- AttachedProperty — set on foreign controls (like
Grid.Row)
StyledProperty
public static readonly StyledProperty<IBrush?> BackgroundProperty =
AvaloniaProperty.Register<MyControl, IBrush?>(
name: nameof(Background),
defaultValue: null,
inherits: false,
defaultBindingMode: BindingMode.OneWay,
validate: null,
coerce: CoerceBackground);
public IBrush? Background
{
get => GetValue(BackgroundProperty);
set => SetValue(BackgroundProperty, value);
}
private static IBrush? CoerceBackground(AvaloniaObject instance, IBrush? value)
=> value ?? Brushes.Transparent;
Inherit/reuse from another class:
public static readonly StyledProperty<IBrush?> BackgroundProperty =
Border.BackgroundProperty.AddOwner<MyPanel>();
DirectProperty
Use for properties that back observable collections, streams, or are performance-sensitive:
public static readonly DirectProperty<MyControl, string> TextProperty =
AvaloniaProperty.RegisterDirect<MyControl, string>(
name: nameof(Text),
getter: o => o.Text,
setter: (o, v) => o.Text = v,
defaultBindingMode: BindingMode.TwoWay,
enableDataValidation: true);
private string _text = "";
public string Text
{
get => _text;
set => SetAndRaise(TextProperty, ref _text, value);
}
SetAndRaise performs equality check, updates field, raises PropertyChanged.
AttachedProperty
public static class Grid
{
public static readonly AttachedProperty<int> RowProperty =
AvaloniaProperty.RegisterAttached<Grid, Control, int>(
name: "Row",
defaultValue: 0,
inherits: false);
public static int GetRow(Control element) => element.GetValue(RowProperty);
public static void SetRow(Control element, int value) => element.SetValue(RowProperty, value);
}
XAML usage:
<TextBlock Grid.Row="1" Grid.Column="2"/>
Property Value Priority (highest → lowest)
| Priority | Source |
|---|
| 1 | Animation |
| 2 | Local value (SetValue / binding at LocalValue) |
| 3 | Style trigger (active pseudoclass setter) |
| 4 | Template binding |
| 5 | Style setter (non-triggered) |
| 6 | Inherited value |
| 7 | Default value |
SetCurrentValue — sets without overriding bindings/styles (priority: LocalValue but clears on style change):
myControl.SetCurrentValue(IsCheckedProperty, true);
Observing Property Changes
On a specific instance (reactive):
myTextBox
.GetObservable(TextBox.TextProperty)
.Subscribe(text => Console.WriteLine($"Text: {text}"));
myTextBox
.GetObservable(TextBox.TextProperty)
.Throttle(TimeSpan.FromMilliseconds(300))
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(HandleTextChanged);
In a control, override OnPropertyChanged:
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == ValueProperty)
{
var newValue = change.GetNewValue<int>();
var oldValue = change.GetOldValue<int>();
UpdateVisuals(newValue);
}
}
Static class handler (fires for all instances):
static MyControl()
{
ValueProperty.Changed.AddClassHandler<MyControl>((control, e) =>
control.OnValueChanged(e));
AffectsRender<MyControl>(ForegroundProperty, BackgroundProperty, PaddingProperty);
AffectsMeasure<MyControl>(WidthProperty, HeightProperty);
AffectsArrange<MyControl>(AlignmentProperty);
}
AffectsRender / AffectsMeasure / AffectsArrange — declare layout/render dependencies efficiently.
Inherited Properties
public static readonly StyledProperty<FlowDirection> FlowDirectionProperty =
AvaloniaProperty.Register<Control, FlowDirection>(
nameof(FlowDirection),
defaultValue: FlowDirection.LeftToRight,
inherits: true);
Child controls inherit the value from their nearest ancestor that sets it explicitly.
Metadata Override
Override default value or coerce function for a subclass:
static MyControl()
{
BackgroundProperty.OverrideMetadata<MyControl>(
new StyledPropertyMetadata<IBrush?>(
defaultValue: Brushes.White,
defaultBindingMode: BindingMode.OneWay,
coerce: MyCoerce));
}
Data Validation
Enable on DirectProperty with enableDataValidation: true, then throw in setter or implement INotifyDataErrorInfo:
private int _age;
public int Age
{
get => _age;
set
{
if (value < 0 || value > 150)
throw new ArgumentOutOfRangeException(nameof(Age), "Age must be 0–150");
SetAndRaise(AgeProperty, ref _age, value);
}
}
Common Mistakes
- Field must end with
Property — convention: FooProperty for public property Foo; breaks tooling otherwise
DirectProperty for rarely-changed values — incurs no overhead, but can't be used in styles or animations; use StyledProperty if styling needed
- Forgetting
SetAndRaise in DirectProperty setter — plain field assignment won't notify bindings
- Setting value in constructor via property setter — creates a local value that blocks style/theme overrides; use
SetCurrentValue or set field directly in constructor
AddOwner vs. Register — AddOwner shares metadata and identity with source property (same AvaloniaProperty instance); bindings between types sharing a property work correctly
change.GetNewValue<T>() — must match registered type exactly; mismatched cast throws InvalidCastException