agentleFS
linuxdevellinuxdevel/Avalonia-skills/skills/avalonia/avalonia-custom-controls/SKILL.md

avalonia-custom-controls

Skill50 starsChanged 8 days agoMITView on GitHub

Use when creating custom UserControls, TemplatedControls, basic drawing controls, custom panels, control libraries, attached properties, or custom flyouts in Avalonia.

The file

We split this skill into the rules it gives an agent

4 rules. About 0 are common, 1 is distinctive, and 3 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Default template in Themes/Generic.axaml:

    line 57
  2. Distinctive:

    Add interactivity without code-behind or subclassing:

    line 220
  3. Very specific:

    Not calling InitializeComponent() in UserControl constructor

    line 245
  4. Very specific:

    Not providing x:Key="{x:Type local:MyControl}" in Generic.axaml ControlTheme

    line 249

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Avalonia Custom Controls

Overview

Three control types: UserControl (XAML composition, app-specific), TemplatedControl (lookless, restyled per theme), Control (custom drawing with Render override). Choose by reusability needs.

Choosing a Control Type

TypeInherit fromUse when
UserControlUserControlApp-specific view/page, composed of existing controls
TemplatedControlTemplatedControlReusable, restyled control for libraries/themes
Basic controlControlCustom drawing (charts, gauges, games)
PanelPanelCustom layout algorithm

UserControl

<!-- MyCard.axaml -->
<UserControl xmlns="https://github.com/avaloniaui"
             x:Class="MyApp.Controls.MyCard">
    <Border CornerRadius="8" Padding="16">
        <ContentPresenter Content="{Binding}"/>
    </Border>
</UserControl>
// MyCard.axaml.cs
public partial class MyCard : UserControl
{
    public MyCard() => InitializeComponent();
}

TemplatedControl

public class RatingControl : TemplatedControl
{
    public static readonly StyledProperty<int> ValueProperty =
        AvaloniaProperty.Register<RatingControl, int>(nameof(Value), defaultValue: 0);

    public int Value
    {
        get => GetValue(ValueProperty);
        set => SetValue(ValueProperty, value);
    }
}

Default template in Themes/Generic.axaml:

<ControlTheme x:Key="{x:Type local:RatingControl}" TargetType="local:RatingControl">
    <Setter Property="Template">
        <ControlTemplate>
            <StackPanel Orientation="Horizontal">
                <!-- template content -->
            </StackPanel>
        </ControlTemplate>
    </Setter>
</ControlTheme>

StyledProperty vs DirectProperty

StyledPropertyDirectProperty
Styling supportYesNo
BindingAll prioritiesLocalValue only
Value inheritanceYesNo
Per-instance storagePrioritized listField
Use whenNeeds styling, most casesHigh-perf, won't be styled
// StyledProperty
public static readonly StyledProperty<IBrush?> FillProperty =
    AvaloniaProperty.Register<MyControl, IBrush?>(nameof(Fill));

// DirectProperty
public static readonly DirectProperty<MyControl, int> CountProperty =
    AvaloniaProperty.RegisterDirect<MyControl, int>(
        nameof(Count), o => o.Count, (o, v) => o.Count = v);
private int _count;
public int Count
{
    get => _count;
    set => SetAndRaise(CountProperty, ref _count, value);
}

AttachedProperty

public static readonly AttachedProperty<bool> IsHighlightedProperty =
    AvaloniaProperty.RegisterAttached<MyHelper, Control, bool>("IsHighlighted");

public static bool GetIsHighlighted(Control element) => element.GetValue(IsHighlightedProperty);
public static void SetIsHighlighted(Control element, bool value) => element.SetValue(IsHighlightedProperty, value);

Usage: <Button local:MyHelper.IsHighlighted="True"/>

Template parts: OnApplyTemplate(TemplateAppliedEventArgs e) + e.NameScope.Find<T>("PART_X") — unchanged in 11.3/12.x.

C# Template Part Registration (11.3 / 12.1)

In a FuncControlTemplate, setting Name alone does not register the part for e.NameScope lookup. Register it in the scope supplied to the factory:

using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;

var template = new FuncControlTemplate<TemplatedControl>((owner, scope) =>
    new Border { Name = "PART_Frame" }.RegisterInNameScope(scope));

RegisterInNameScope requires a non-null Name; scope.Register("PART_Frame", part) is the explicit alternative. The factory receives a fresh scope for each build, so the same part names can appear in separate template instances. Do not share one scope across those instances. See the registration API and 12.1.4 implementation.

Custom Drawing (Render Override)

public class CircleControl : Control
{
    public override void Render(DrawingContext context)
    {
        var brush = new SolidColorBrush(Colors.CornflowerBlue);
        var pen = new Pen(Brushes.Navy, 2);
        context.DrawEllipse(brush, pen,
            new Point(Bounds.Width / 2, Bounds.Height / 2),
            Bounds.Width / 2 - 2, Bounds.Height / 2 - 2);
    }

    protected override Size MeasureOverride(Size availableSize)
        => new Size(100, 100); // desired size
}

Invalidate drawing: InvalidateVisual()

Rendering API changes in 12.0 (mostly custom backends / low-level bitmap code)

11.x12.0+
DrawingContext.PushPreTransform / PushPostTransform / PushTransformContainerPushTransform(matrix)
BindingPriority.TemplatedParentBindingPriority.Template
ILockedFramebuffer without alpha infoILockedFramebuffer.AlphaFormat added; new LockedFramebuffer(...) ctor requires an AlphaFormat argument
Bitmap.CopyPixels(..., AlphaFormat)Bitmap.CopyPixels(...) — alpha format comes from the ILockedFramebuffer
IReadableBitmapWithAlphaImplIReadableBitmapImpl.AlphaFormat
IPlatformRenderInterfaceContext.CreateOffscreenRenderTarget(PixelSize, double)(PixelSize, Vector, bool)
Layoutable.GetMaxSizeFromConstraint (private)protected, overridable

DrawEllipse/DrawRectangle/DrawGeometry/DrawText usage in Render() is unchanged. Standard RenderTargetBitmap code is unaffected.

Data validation in custom properties (12.0+)

Binding plugins (BindingPlugins, IDataValidationPlugin, ...) were removed; data-annotations validation is off by default. Properties registered with enableDataValidation: true now report errors automatically, so remove UpdateDataValidation overrides that only call DataValidationErrors.SetError. Override it only to suppress the default for a given property (skip the base call).

Custom Panel

public class CircularPanel : Panel
{
    protected override Size MeasureOverride(Size availableSize)
    {
        foreach (var child in Children)
            child.Measure(availableSize);
        return availableSize;
    }

    protected override Size ArrangeOverride(Size finalSize)
    {
        double angle = 0, step = 2 * Math.PI / Children.Count;
        double cx = finalSize.Width / 2, cy = finalSize.Height / 2;
        double r = Math.Min(cx, cy) * 0.8;
        foreach (var child in Children)
        {
            var x = cx + r * Math.Cos(angle) - child.DesiredSize.Width / 2;
            var y = cy + r * Math.Sin(angle) - child.DesiredSize.Height / 2;
            child.Arrange(new Rect(x, y, child.DesiredSize.Width, child.DesiredSize.Height));
            angle += step;
        }
        return finalSize;
    }
}

StyleKeyOverride

// Make MyButton style as base Button (picks up Button's ControlTheme)
protected override Type StyleKeyOverride => typeof(Button);

IStyleable (Type IStyleable.StyleKey) was removed in 12 — always use StyleKeyOverride (works in 11.3 too); constrain generics on StyledElement, not IStyleable.

Control Library Project

  • Separate class library project
  • Reference Avalonia NuGet (not Avalonia.Desktop)
  • Include Generic.axaml as embedded resource: avares://MyLib/Themes/Generic.axaml
  • Register in consuming app: <StyleInclude Source="avares://MyLib/Themes/Generic.axaml"/>

Community Tools for Control Development

Tool / LibraryNuGet / RepoPurpose
HotAvaloniaHotAvaloniaHot reload for AXAML during control development
Avant GardeGitHub: kuiperzone/AvantGardeStandalone XAML previewer for control libraries
ShowMeTheXaml.AvaloniaShowMeTheXaml.AvaloniaShow rendered XAML at runtime (useful for demos)
Avalonia.Xaml.BehaviorsAvalonia.Xaml.BehaviorsAttach behaviors to controls without subclassing
Sortable.Avaloniasortable-avaloniaDrag-drop sort behavior for ItemsControls

Behaviors (Avalonia.Xaml.Behaviors)

Add interactivity without code-behind or subclassing:

<!-- NuGet: Avalonia.Xaml.Behaviors -->
<Button>
    <Interaction.Behaviors>
        <EventTriggerBehavior EventName="Click">
            <InvokeCommandAction Command="{Binding MyCommand}"/>
        </EventTriggerBehavior>
    </Interaction.Behaviors>
</Button>

<!-- DataTrigger equivalent -->
<Border>
    <Interaction.Behaviors>
        <DataTriggerBehavior Binding="{Binding IsLoading}" ComparisonCondition="Equal" Value="True">
            <ChangePropertyAction TargetObject="{Binding #spinner}" PropertyName="IsVisible" Value="True"/>
        </DataTriggerBehavior>
    </Interaction.Behaviors>
</Border>

This is the Avalonia replacement for WPF DataTrigger and EventTrigger.

Common Mistakes

  • Inheriting from Control (WPF habit) instead of TemplatedControl for lookless controls
  • Not calling InitializeComponent() in UserControl constructor
  • Forgetting partial keyword on UserControl class
  • Using StyledProperty for high-frequency properties that don't need styling (perf issue)
  • Implementing IStyleable or using BindingPriority.TemplatedParent / PushPreTransform in 12.x — removed (see above)
  • Not providing x:Key="{x:Type local:MyControl}" in Generic.axaml ControlTheme

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.