Templated controls
Templated controls contain no rendering code in the control class. Instead, their appearance is defined by a ControlTemplate. This separates the control's visual structure from its behavior, allowing developers and designers to restyle the control without modifying its logic.
If you are familiar with WPF, these are sometimes called "lookless" controls.
Many of Avalonia's built-in controls are templated controls (e.g., Button, TextBox, and ListBox). You can follow the same pattern to build your own.
Creating a templated control
To create a templated control, add a new control class that inherits from TemplatedControl and register your custom properties using StyledProperty.
The following example is a templated control with a LabelText property, but no visual representation.
public class ToggleLabel : TemplatedControl
{
public static readonly StyledProperty<string> LabelTextProperty =
AvaloniaProperty.Register<ToggleLabel, string>(nameof(LabelText), "Default");
public string LabelText
{
get => GetValue(LabelTextProperty);
set => SetValue(LabelTextProperty, value);
}
}
Never assign DataContext = this in a custom control's constructor. This overrides the DataContext that your users expect to inherit from the parent visual tree. Bindings set on your control, such as <MyControl Items="{Binding SelectedItems}" />, will resolve against the control type instead of the parent's ViewModel, causing silent binding failures.
Templated controls do not need a self-referencing DataContext. Use TemplateBinding inside your control template to access your control's properties, and let the DataContext flow from the parent.
Defining the control theme
Every templated control must have a default ControlTheme that contains its ControlTemplate. This control theme is typically specified in a theme file, which can included in your application resources in App.axaml using ResourceInclude.
<Application.Resources>
<ControlTheme x:Key="{x:Type local:ToggleLabel}" TargetType="local:ToggleLabel">
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}" Padding="8">
<TextBlock Text="{TemplateBinding LabelText}" />
</Border>
</ControlTemplate>
</Setter>
</ControlTheme>
</Application.Resources>
Notes:
x:Key="{x:Type local:ToggleLabel}"instructs Avalonia to apply the default theme to all instances ofToggleLabel.- To add an alternative look that you can apply to selected instances of the control, add a string key, e.g.,
x:Key="CompactToggleLabel". Individual instances ofToggleLabelcan then use the alternative by settingTheme="{StaticResource CompactToggleLabel}". See control theme lookup for more information. TargetTypescopes the theme, so that property setters and template bindings resolve against the correct type.- Inside the
ControlTemplate, useTemplateBindingto bind to properties on the templated control.
Template parts
Sometimes, a templated control needs to interact with specific elements in its template from code. You can specify these elements by defining them as TemplatePart. By convention, template parts are named with the PART_ prefix.
Declaring parts
Apply the TemplatePart attribute to the control class. Create one TemplatePart for each element that should be recognized as an individual part.
[TemplatePart("PART_Button", typeof(Button), IsRequired = true)]
[TemplatePart("PART_Label", typeof(TextBlock))]
public class ToggleLabel : TemplatedControl
{
// Templated control behavior and logic
}
Each declaration carries three values:
| Value | Meaning |
|---|---|
Name | x:Name that the element uses. Must begin with the PART_ prefix. |
Type | Control type of the element, e.g., Button, Panel. |
IsRequired | Whether the part is mandatory for the template. Defaults to false. |
Declarations are inherited, so a control deriving from ToggleLabel would inherit its parts.
Retrieving parts in code
Override OnApplyTemplate to locate parts after the template has been applied:
Get throws a NotFound exception if the target part is missing. Find returns null if the target part is missing. Both Get and Find throw an InvalidOperation exception if the target part is present but is incompatible with the declared control Type.
private Button? _button;
private TextBlock? _label;
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
if (_button is not null)
{
_button.Click -= OnButtonClick;
}
// Required part: Get throws if the template does not provide it.
_button = e.NameScope.Get<Button>("PART_Button");
_button.Click += OnButtonClick;
// Optional part: Find returns null if the template does not provide it.
_label = e.NameScope.Find<TextBlock>("PART_Label");
}
See the ToggleSwitch source code for an example of both Get<T> and Find<T> in use.
TemplateBinding
Use TemplateBinding if you are creating a control template and you want to bind to the templated parent.
<TextBlock Name="tb" Text="{TemplateBinding Caption}"/>
<!-- Which is the same as -->
<TextBlock Name="tb" Text="{Binding Caption, RelativeSource={RelativeSource TemplatedParent}}"/>
Although the two syntaxes are equivalent in most cases, there are four differences:
-
TemplateBindingaccepts only a single property, rather than a property path. If you want to bind using a property path, you must use the longer syntax:<!-- This WON'T work -->
<TextBlock Name="tb" Text="{TemplateBinding Caption.Length}"/>
<!-- Instead, use this syntax for the property path -->
<TextBlock Name="tb" Text="{Binding Caption.Length, RelativeSource={RelativeSource TemplatedParent}}"/> -
TemplateBindingsupportsOneWayandTwoWaymodes.OneTimeandOneWayToSourcemodes are not supported. The default isOneWay. RequestTwoWayexplicitly if you need the value written back to the templated parent. (Note: This differs from WPF, whereTemplateBindingisOneWayonly.)<!-- Writes the slider's value back to the templated parent -->
<Slider Value="{TemplateBinding Value, Mode=TwoWay}"/> -
TemplateBindingcan only be used on aStyledElement. (Warning: If used on a property that is not aStyledElement, the binding fails without logging an error, and the property keeps its default value.)<!-- This WON'T work because GeometryDrawing is not a StyledElement -->
<GeometryDrawing Brush="{TemplateBinding AccentBrush}"/>
<!-- Instead, use the longer syntax -->
<GeometryDrawing Brush="{Binding AccentBrush, RelativeSource={RelativeSource TemplatedParent}}"/>
Pseudoclasses
Templated controls can expose visual states through pseudoclasses. This lets theme authors style the control differently based on its state, without needing code-behind access.
Declaring pseudoclasses
Apply PseudoClasses to the control class to declare which ones your control uses. Pseudoclass names must include the leading :.
[PseudoClasses(":active", ":dragging")]
public class ToggleLabel : TemplatedControl
{
// Templated control behavior and logic
}
Setting pseudoclasses in code
Set the pseudoclass state in your control logic.
PseudoClasses.Set(":active", isActive);
Then, target the pseudoclass in your control theme. Nest the Style inside the ControlTheme so that ^ resolves to the control.
<ControlTheme x:Key="{x:Type local:ToggleLabel}" TargetType="local:ToggleLabel">
<Setter Property="Template">
<!-- ControlTemplate as above -->
</Setter>
<Style Selector="^:active">
<Setter Property="Background" Value="Blue" />
</Style>
</ControlTheme>
See also
- Defining properties: Add styled, direct, and attached properties to a custom control.
- Defining events: Add routed events to a custom control.
- Control themes: How control themes define the appearance of a templated control.
- Control template walkthrough: A worked example of a control template.
- Pseudoclasses: How pseudoclasses expose control state to styles.
- Creating custom controls: Overview of the custom control types.
TemplatePartAttribute: API reference for declaring template parts.PseudoClassesAttribute: API reference for declaring pseudoclasses.