Skip to main content

Defining properties for custom controls

When creating a custom control, you can give it the following types of properties. This page walks you through registering and using each type, so you can choose the right ones for your control.

  1. Styled property: Set by the Avalonia styling system.
  2. Direct property: Has a backing C# field, supports data binding.
  3. Attached property: Hosted in a separate container class, then configured in XAML.

Styled properties

A styled property stores its value inside the Avalonia property system, not a backing field. As a result, styled properties can participate in styling, animations and value precedence. Use a styled property when you want to allow users to style or animate the property.

info

For more information on using styles in Avalonia, see the Styles guide.

Naming conventions

The static field must follow the pattern [PropertyName]Property, e.g., BackgroundProperty, FontWeightProperty. Avalonia uses this convention to map XAML attributes to properties automatically.

Failure to follow this naming convention may result in "Unable to find suitable setter or adder for property" errors during compilation.

C#
public static readonly StyledProperty<double> CornerRadiusProperty = ...
XAML
<local:MyControl CornerRadius="8" />

Registering a styled property

To register a styled property:

  1. Add a static readonly field of type StyledProperty<T>.
  2. Use the method AvaloniaProperty.Register to register.
  3. Provide a CLR getter and setter that call GetValue and SetValue respectively.

The following example registers a CornerRadius styled property with a default value of 0.0:

public class MyControl : Control
{
public static readonly StyledProperty<double> CornerRadiusProperty =
AvaloniaProperty.Register<MyControl, double>(nameof(CornerRadius), defaultValue: 0.0);

public double CornerRadius
{
get => GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
}
warning

The CLR property getter/setter must only call GetValue and SetValue. Avoid adding other methods, because some property changes do not use the CLR property.

The Register method accepts these optional parameters:

ParameterDescription
nameThe property name. Must match the CLR property name.
defaultValueThe default value of the property.
inheritsWhether the value inherits down the visual tree.
defaultBindingModeThe default binding mode (OneWay, TwoWay, OneTime, or OneWayToSource).
validateA function that returns false for values that should be rejected.
coerceA function that adjusts the value before it is applied.

Reusing an existing styled property

If another control already defines a property you wish to use (e.g., Background on Border), you can use AddOwner instead of registering a new property. By doing so, the properties share a single property identity, meaning styles targeting the property work on all controls that share it.

public class MyCustomControl : Control
{
public static readonly StyledProperty<IBrush?> BackgroundProperty =
Border.BackgroundProperty.AddOwner<MyCustomControl>();

public IBrush? Background
{
get => GetValue(BackgroundProperty);
set => SetValue(BackgroundProperty, value);
}
}

Styling a custom property

Once a styled property is registered, users can target it in XAML to set its style. The following example sets the Background of a custom control through a style:

<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:cc="using:AvaloniaCCExample.CustomControls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="AvaloniaCCExample.MainWindow"
Title="Avalonia Custom Control">

<Window.Styles>
<Style Selector="cc|MyCustomControl">
<Setter Property="Background" Value="Yellow"/>
</Style>
</Window.Styles>

<cc:MyCustomControl Height="200" Width="300"/>

</Window>
Preview of a custom control with a defined property

Direct properties

A direct property is backed by a conventional C# field. It does not participate in styling or animation, but supports data binding and change notifications. Use a direct property when:

  • You need a read-only property. (Styled properties cannot be read-only.)
  • You want better performance. (Values of direct properties are read directly from the field.)
  • You want a property that cannot be styled.

Registering a direct property

Use AvaloniaProperty.RegisterDirect. Provide getter and setter delegates that point to your backing field:

public class MyControl : Control
{
public static readonly DirectProperty<MyControl, string?> StatusProperty =
AvaloniaProperty.RegisterDirect<MyControl, string?>(
nameof(Status),
o => o.Status,
(o, v) => o.Status = v);

private string? _status;

public string? Status
{
get => _status;
set => SetAndRaise(StatusProperty, ref _status, value);
}
}
warning

Always use SetAndRaise in the CLR setter instead of assigning the backing field directly. SetAndRaise updates the field and raises the property-changed notification in a single call. Calling SetValue on a direct property will throw an exception.

Read-only direct properties

To create a read-only property, omit the setter delegate from the registration call and keep the CLR setter private:

public class MyControl : Control
{
public static readonly DirectProperty<MyControl, bool> IsActiveProperty =
AvaloniaProperty.RegisterDirect<MyControl, bool>(
nameof(IsActive),
o => o.IsActive);

private bool _isActive;

public bool IsActive
{
get => _isActive;
private set => SetAndRaise(IsActiveProperty, ref _isActive, value);
}
}

Styled vs. direct properties

BehaviorStyled propertyDirect property
Participates in stylingYesNo
Participates in animationsYesNo
Supports value precedenceYesNo (single value)
Can inherit valuesYesNo
Supports coercionYesNo
PerformanceProperty store lookupDirect field access
Can be read-onlyNoYes

Responding to property changes

For styled and direct properties, you can react to property value changes by overriding OnPropertyChanged in your control.

This example demonstrates reacting to a background change by invalidating the visual and thereby updating to the new background.

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

if (change.Property == BackgroundProperty)
{
// Invalidate the visual so the control repaints with the new background.
InvalidateVisual();
}
}

Data validation support

Data validation lets your control display an error when a bound property is detected as invalid.

Starting from Avalonia v12, properties registered with enableDataValidation: true report validation errors automatically. (i.e., You no longer need the UpdateDataValidation override calling DataValidationErrors.SetError from earlier versions of Avalonia.)

To add data validation to a custom control:

  1. Register the property with enableDataValidation: true.
  2. Wrap the custom control in a DataValidationErrors control, so that errors can be displayed to users.
  3. Optionally, style the :error pseudoclass to change the control's appearance when data is invalid.

For more information on data validation in Avalonia in general, see Validation in data binding.

Enabling data validation on a property

enableDataValidation works with both styled properties and direct properties. It is set the same way whether you are registering a new property with Register or reusing an existing property with AddOWner.

warning

You must set BindingMode.TwoWay on the property. Data validation works by returning the value to the binding source.

public static readonly StyledProperty<int> ValueProperty =
AvaloniaProperty.Register<QuantityStepper, int>(nameof(Value),
defaultValue: 1,
defaultBindingMode: BindingMode.TwoWay,
enableDataValidation: true);

Displaying errors with DataValidationErrors

To display data validation errors to the user, enclose your custom control in a DataValidationErrors control. DataValidationErrors is a ContentControl that provides attached properties to handle error states.

Use DataValidationErrors within <UserControl> for user controls, or within <ControlTemplate> for templated controls.

<ControlTemplate> / <UserControl>
<DataValidationErrors>
<!-- your control's visuals -->
</DataValidationErrors>
</ControlTemplate> / </UserControl>

While a control has errors, DataValidationErrors sets the :error pseudoclass. Target the pseudoclass with a Style to customize how the control should look when in an error state.

<UserControl.Styles>
<Style Selector="local|MyCustomControl:error Border#Frame">
<Setter Property="BorderBrush" Value="Red" />
</Style>
</UserControl.Styles>

Data validation example

The following example creates QuantitySelector, a control that sets a numeric value using + and - buttons. Data validation is enabled on Value, a styled property representing the number on the selector. It is bound to a view model that rejects quantities outside the range of 1–10. If an invalid value is set, the error state triggers, which turns the border red and displays an error message.

A numeric selector showing the number 11. The selector is highlighted in yellow and a text error message is shown underneath.

<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:ValidationDemo"
x:Class="ValidationDemo.QuantitySelector"
x:Name="root">

<UserControl.Styles>
<!-- Set the default color in Styles, not in the control itself.
Colors set directly in the control override the :error style. -->
<Style Selector="Border#Frame">
<Setter Property="BorderBrush" Value="Gray" />
</Style>
<!-- Set the :error pseudoclass on the QuantitySelector. -->
<Style Selector="local|QuantitySelector:error Border#Frame">
<Setter Property="BorderBrush" Value="Red" />
</Style>
</UserControl.Styles>

<DataValidationErrors Owner="{Binding #root}">
<!-- Custom control layout goes inside DataValidationErrors. -->
<Border x:Name="Frame" BorderThickness="1"
CornerRadius="4" Padding="4">
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Content="-" Click="DecreaseButtonClick" Width="32"/>
<TextBlock Text="{Binding #root.Value}"
MinWidth="24" TextAlignment="Center"
VerticalAlignment="Center"/>
<Button Content="+" Click="IncreaseButtonClick" Width="32"/>
</StackPanel>
</Border>
</DataValidationErrors>

</UserControl>

Attached properties

An attached property lives in its own container class and is configured on compatible controls in XAML. This allows you to have additional properties that are not part of your custom control's own control class. For example, you may wish to use an attached property to allow child elements to specify their own layout positions within the parent custom control. (See Custom Panel for a practical example.)

Naming conventions

  • Like styled properties, the static field for the attached property follows the pattern [PropertyName]Property.
  • The name parameter is [PropertyName] alone (without the Property suffix).

Registering an attached property

  1. Add a new container class inheriting from AvaloniaObject.
  2. Use the AvaloniaProperty.RegisterAttached method to register the attached property.
  3. Provide a CLR getter and setter that call GetValue and SetValue respectively.
  4. Further define the behavior of the property, as necessary.

The following example creates an attached property called IsDimmed in a standalone file DimExtensions.cs. It is a Boolean property that renders a control at 50% opacity when True.

DimExtensions.cs
using Avalonia;
using Avalonia.Controls;

namespace MyApp;

// Attached properties live in a container class that inherits from AvaloniaObject.
public class DimExtensions : AvaloniaObject
{
// Register the attached property. The type arguments are:
// <owner class, type it can be set on, value type>.
public static readonly AttachedProperty<bool> IsDimmedProperty =
AvaloniaProperty.RegisterAttached<DimExtensions, Control, bool>("IsDimmed");

// Provide static getter and setter. The XAML system finds these by name.
public static void SetIsDimmed(Control element, bool value) =>
element.SetValue(IsDimmedProperty, value);

public static bool GetIsDimmed(Control element) =>
element.GetValue(IsDimmedProperty);

// React when the value changes.
static DimExtensions()
{
IsDimmedProperty.Changed.AddClassHandler<Control>((control, _) =>
control.Opacity = GetIsDimmed(control) ? 0.5 : 1.0);
}
}

Using the attached property in XAML

Declare the namespace in XAML. Then, set the attached property using dot notation.

The following example shows the IsDimmed attached property from the previous section applied to two buttons. The second button renders at half opacity because it is given IsDimmed=True.

MainWindow.axaml
<StackPanel>
<Button Content="Normal" />
<Button Content="Dimmed" local:DimExtensions.IsDimmed="True" />
</StackPanel>

Common pitfalls

  • Mismatched names. The name argument you pass to Register must match the CLR property name exactly. A mismatch causes errors at run-time.
  • Using SetValue with a direct property. Direct properties require SetAndRaise. Calling SetValue throws an InvalidOperationException.
  • Adding a backing field for a styled property. Styled properties store values inside the Avalonia property system. If you read from a local field, you will get stale data. Always use GetValue and SetValue.
  • Forgetting to call base.OnPropertyChanged. If you override OnPropertyChanged, always call the base implementation first so the framework can process the change.

See also