Goal
Render) versus build templated controls (pure XAML).DrawingContext, invalidation (AffectsRender, InvalidateVisual), and caching for performance.TemplatedControl, expose properties, and support theming/accessibility.Why this matters
Prerequisites
| Scenario | Draw (override Render) |
Template (ControlTemplate) |
|---|---|---|
| Pixel-perfect graphics, charts | [x] | |
| Animations driven by drawing primitives | [x] | |
| Standard widgets composed of existing controls | [x] | |
| Consumer needs to restyle via XAML | [x] | |
| Complex interaction per element (buttons in control) | [x] |
Hybrid: templated control containing a custom-drawn child for performance-critical surface.
InvalidateVisual() schedules redraw.AffectsRender<TControl>(property1, ...) in static constructor to auto-invalidate on property change.InvalidateMeasure similarly (handled automatically for StyledPropertys registered with AffectsMeasure).DrawingContext primitives:
DrawGeometry(brush, pen, geometry)DrawRectangle/DrawEllipseDrawImage(image, sourceRect, destRect)DrawText(formattedText, origin)PushClip, PushOpacity, PushOpacityMask, PushTransform -- use in using blocks to auto-pop state.Example pattern:
public override void Render(DrawingContext ctx)
{
base.Render(ctx);
using (ctx.PushClip(new Rect(Bounds.Size)))
{
ctx.DrawRectangle(Brushes.Black, null, Bounds);
ctx.DrawText(_formattedText, new Point(10, 10));
}
}
TemplatedControl raises TemplateApplied when the ControlTemplate is inflated. Override OnApplyTemplate(TemplateAppliedEventArgs e) to wire named parts via e.NameScope.TemplateResult<Control> behind the scenes (ControlTemplate.Build). It carries a NameScope so you can fetch presenters (e.NameScope.Find<ContentPresenter>("PART_Content")).ContentPresenter, ItemsPresenter, ScrollContentPresenter, and ToggleSwitchPresenter. They bridge templated surfaces with logical children (content, items, scrollable regions).TemplateApplied to subscribe to events on named parts, but always detach previous handlers before attaching new ones to prevent leaks.Example:
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_toggleRoot?.PointerPressed -= OnToggle;
_toggleRoot = e.NameScope.Find<Border>("PART_ToggleRoot");
_toggleRoot?.PointerPressed += OnToggle;
}
For library-ready controls publish a ControlTheme default template so consumers can restyle without copying large XAML fragments.
public sealed class Sparkline : Control
{
public static readonly StyledProperty<IReadOnlyList<double>?> ValuesProperty =
AvaloniaProperty.Register<Sparkline, IReadOnlyList<double>?>(nameof(Values));
public static readonly StyledProperty<IBrush> StrokeProperty =
AvaloniaProperty.Register<Sparkline, IBrush>(nameof(Stroke), Brushes.DeepSkyBlue);
public static readonly StyledProperty<double> StrokeThicknessProperty =
AvaloniaProperty.Register<Sparkline, double>(nameof(StrokeThickness), 2.0);
static Sparkline()
{
AffectsRender<Sparkline>(ValuesProperty, StrokeProperty, StrokeThicknessProperty);
}
public IReadOnlyList<double>? Values
{
get => GetValue(ValuesProperty);
set => SetValue(ValuesProperty, value);
}
public IBrush Stroke
{
get => GetValue(StrokeProperty);
set => SetValue(StrokeProperty, value);
}
public double StrokeThickness
{
get => GetValue(StrokeThicknessProperty);
set => SetValue(StrokeThicknessProperty, value);
}
public override void Render(DrawingContext ctx)
{
base.Render(ctx);
var values = Values;
var bounds = Bounds;
if (values is null || values.Count < 2 || bounds.Width <= 0 || bounds.Height <= 0)
return;
double min = values.Min();
double max = values.Max();
double range = Math.Max(1e-9, max - min);
using var geometry = new StreamGeometry();
using (var gctx = geometry.Open())
{
for (int i = 0; i < values.Count; i++)
{
double t = i / (double)(values.Count - 1);
double x = bounds.X + t * bounds.Width;
double yNorm = (values[i] - min) / range;
double y = bounds.Y + (1 - yNorm) * bounds.Height;
if (i == 0)
gctx.BeginFigure(new Point(x, y), isFilled: false);
else
gctx.LineTo(new Point(x, y));
}
gctx.EndFigure(false);
}
var pen = new Pen(Stroke, StrokeThickness);
ctx.DrawGeometry(null, pen, geometry);
}
}
Usage:
<local:Sparkline Width="160" Height="36" Values="3,7,4,8,12" StrokeThickness="2"/>
Render. Cache Pen, FormattedText when possible.StreamGeometry and reuse if values rarely change (rebuild when invalidated).Create Badge : TemplatedControl with properties (Content, Background, Foreground, CornerRadius, MaxWidth, etc.). Default style in Styles.axaml:
<ControlTheme TargetType="local:Badge">
<Setter Property="Template">
<ControlTemplate TargetType="local:Badge">
<Border x:Name="PART_Border"
Background="{TemplateBinding Background}"
CornerRadius="{TemplateBinding CornerRadius}"
Padding="6,0"
MinHeight="16" MinWidth="20"
HorizontalAlignment="Left"
VerticalAlignment="Top">
<ContentPresenter x:Name="PART_Content"
Content="{TemplateBinding Content}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{TemplateBinding Foreground}"/>
</Border>
</ControlTemplate>
</Setter>
<Setter Property="Background" Value="#E53935"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
</ControlTheme>
In code, capture named parts once the template applies:
public sealed class Badge : TemplatedControl
{
public static readonly StyledProperty<object?> ContentProperty =
AvaloniaProperty.Register<Badge, object?>(nameof(Content));
Border? _border;
public object? Content
{
get => GetValue(ContentProperty);
set => SetValue(ContentProperty, value);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_border = e.NameScope.Find<Border>("PART_Border");
}
}
Expose additional state through StyledPropertys so themes and animations can target them.
PseudoClasses (e.g., PseudoClasses.Set(":badge-has-content", true)) to signal template states that styles can observe.PseudoClasses with Transitions or Animations to create hover/pressed effects without rewriting templates.ControlTheme resources referencing the same TemplatedControl type.Visual subclasses (e.g., BadgeGlyph) and expose them as named template parts.Focusable as appropriate; override OnPointerPressed/OnKeyDown for interaction or to update pseudo classes.AutomationProperties.Name, HelpText, or custom AutomationPeer for drawn controls.OnCreateAutomationPeer when your control represents a unique semantic (BadgeAutomationPeer describing count, severity).Custom controls should override MeasureOverride/ArrangeOverride when size depends on content/drawing.
protected override Size MeasureOverride(Size availableSize)
{
var values = Values;
if (values is null || values.Count == 0)
return Size.Empty;
return new Size(Math.Min(availableSize.Width, 120), Math.Min(availableSize.Height, 36));
}
TemplatedControl handles measurement via its template (border + content). For custom-drawn controls, define desired size heuristics.
Use RenderTargetBitmap for saving custom visuals:
var rtb = new RenderTargetBitmap(new PixelSize(200, 100), new Vector(96, 96));
await rtb.RenderAsync(sparkline);
await using var stream = File.OpenWrite("spark.png");
await rtb.SaveAsync(stream);
Use RenderOptions to adjust interpolation for exported graphics if needed.
Example: ChartControl template contains toolbar (Buttons, ComboBox) and a custom ChartCanvas child that handles drawing/selection.
Bounds using PushClip when necessary.RenderOptions.SetEdgeMode and align lines to device pixels (e.g., Math.Round(x) + 0.5 for 1px strokes at 1.0 scale).StreamGeometry/FormattedText.TemplateBinding; use DevTools -> Style Inspector to check which template applies.PseudoClasses within OnApplyTemplate.RenderTargetBitmap or direct drawing) and expose it as a named part in the template.OnCreateAutomationPeer so assistive tech can report badge count and severity; verify with the accessibility tree in DevTools.Logical Tree to confirm your presenter hierarchy (content vs drawn part) matches expectations and retains bindings when themes change.Visual.csDrawingContext.csStreamGeometryContextImplControlTemplate.csTemplateAppliedEventArgs.csNameScope.csTemplatedControl.csControlTheme.csStyledElement.csControlAutomationPeer.csRender versus ControlTemplate?AffectsRender simplify invalidation?Render?What's next