Skip to main content

Metadata and callbacks

Every Avalonia property has associated metadata that controls its default value, binding behavior, and optional coercion logic. You can specify metadata when registering a property and override it for derived types.

Styled property metadata

The StyledPropertyMetadata<T> class controls the behavior of styled properties:

ParameterTypeDescription
defaultValueTThe default value for the property. Used when no other value source provides a value.
defaultBindingModeBindingModeThe binding mode used when a binding does not specify one explicitly.
coerceFunc<AvaloniaObject, T, T>?A callback that can adjust or constrain the property value before it is applied.
enableDataValidationboolWhether the property participates in data validation.

Default values

Specify a default value when registering a property:

public static readonly StyledProperty<double> OpacityProperty =
AvaloniaProperty.Register<MyControl, double>(nameof(Opacity), defaultValue: 1.0);

Overriding default values

A derived control can change the default value of an inherited property:

public class MySpecialButton : Button
{
static MySpecialButton()
{
// Change the default Background for MySpecialButton
BackgroundProperty.OverrideDefaultValue<MySpecialButton>(Brushes.LightBlue);
}
}

You can also supply full metadata when overriding:

static MySpecialButton()
{
BackgroundProperty.OverrideMetadata<MySpecialButton>(
new StyledPropertyMetadata<IBrush?>(Brushes.LightBlue));
}
caution

Metadata overrides must be registered in the static constructor of the type. Overriding metadata after any instance of the type has been created results in undefined behavior.

Value coercion

A coercion callback adjusts the property value before it is stored. This is useful for enforcing constraints, such as clamping a number to a valid range.

public static readonly StyledProperty<double> ProgressProperty =
AvaloniaProperty.Register<MyControl, double>(
nameof(Progress),
defaultValue: 0.0,
coerce: CoerceProgress);

private static double CoerceProgress(AvaloniaObject sender, double value)
{
// Clamp between 0 and 100
return Math.Clamp(value, 0.0, 100.0);
}

public double Progress
{
get => GetValue(ProgressProperty);
set => SetValue(ProgressProperty, value);
}

The coercion callback receives the AvaloniaObject instance and the proposed value, and returns the adjusted value. Coercion runs every time the effective value changes, regardless of the value source (local, style, animation, and similar).

Triggering re-coercion

If your coercion logic depends on another property, you can trigger re-coercion when that other property changes:

protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);

if (change.Property == MaximumProperty)
{
// Re-coerce Progress when Maximum changes
CoerceValue(ProgressProperty);
}
}

Value validation

A validation callback rejects values that are never valid for a property. Unlike coercion, validation does not adjust the value. It returns true to accept or false to reject. Invalid values throw an exception.

public static readonly StyledProperty<int> ColumnSpanProperty =
AvaloniaProperty.Register<MyControl, int>(
nameof(ColumnSpan),
defaultValue: 1,
validate: v => v > 0);

public int ColumnSpan
{
get => GetValue(ColumnSpanProperty);
set => SetValue(ColumnSpanProperty, value);
}

Setting ColumnSpan to 0 or a negative number will throw an exception.

info

Validation is set once at registration time and cannot be overridden per type. Use coercion when you need per-type or instance-dependent value adjustment.

Responding to property changes

Override OnPropertyChanged

The most common way to respond to property changes in a custom control:

protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);

if (change.Property == IsExpandedProperty)
{
var wasExpanded = change.GetOldValue<bool>();
var isExpanded = change.GetNewValue<bool>();
UpdateVisualState(isExpanded);
}
}

GetObservable

Subscribe to changes on a specific object from external code:

myControl.GetObservable(MyControl.IsExpandedProperty)
.Subscribe(isExpanded =>
{
Console.WriteLine($"IsExpanded is now {isExpanded}");
});

Class handlers

Register a handler that fires for all instances of a type. This is typically done in a static constructor:

static MyControl()
{
IsExpandedProperty.Changed.AddClassHandler<MyControl>((control, args) =>
{
control.OnIsExpandedChanged(args);
});
}

private void OnIsExpandedChanged(AvaloniaPropertyChangedEventArgs args)
{
// Handle the change
}

Direct property metadata

Direct properties use DirectPropertyMetadata<T>:

ParameterTypeDescription
unsetValueTThe value used when the property is cleared. This serves as the effective default for direct properties.
defaultBindingModeBindingModeThe default binding mode.
enableDataValidationboolWhether the property participates in data validation.

Direct properties do not support coercion or value validation through metadata. Implement these checks in the CLR property setter instead:

private int _retryCount;

public int RetryCount
{
get => _retryCount;
set
{
if (value < 0)
throw new ArgumentOutOfRangeException(nameof(value));
SetAndRaise(RetryCountProperty, ref _retryCount, value);
}
}

See also