Visual, OnRender, DrawingVisual) to Avalonia Render PipelineOnRender Migration ExampleICustomDrawOperationPrimary WPF APIs:
UIElement.OnRender(DrawingContext)DrawingVisual, VisualCollection, RenderOpen()Adorner, AdornerLayer, AdornerDecoratorInvalidateVisual()CompositionTarget.Rendering (frame-driven callback patterns)Primary Avalonia APIs:
Control.Render(DrawingContext)Visual.InvalidateVisual(), AffectsRender<T>(...) in derived controlsAdornerLayer, AdornerLayer.SetAdorner, AdornerLayer.GetAdornerLayerDrawingContext.Custom(ICustomDrawOperation) for low-level drawing interopDispatcherTimer for frame-like updates when requiredWPF render flow (high level):
OnRender emits draw instructions into the retained scene.DrawingVisual trees can be used for low-level retained drawing structures.CompositionTarget.Rendering is commonly used for per-frame updates.Migration implications:
CompositionTarget.Rendering loops can become declarative animations,DrawingVisual heavy architectures usually migrate better to custom controls or scene operations,Avalonia render flow:
InvalidateVisual() marks visual nodes dirty.Render(DrawingContext) emits draw commands for dirty visuals.Practical migration notes:
Render,| WPF | Avalonia |
|---|---|
OnRender(DrawingContext) |
Render(DrawingContext) |
InvalidateVisual() |
InvalidateVisual() |
DrawingVisual graph for custom retained drawing |
custom Control rendering, compositor visuals, or ICustomDrawOperation for low-level paths |
AdornerLayer |
AdornerLayer with attached adorner APIs |
CompositionTarget.Rendering loop |
animations/transitions; DispatcherTimer + InvalidateVisual for explicit loops |
Render logic deterministic and allocation-light.AdornerLayer attachment.ICustomDrawOperation only for integration scenarios that truly need it.OnRender Migration ExampleWPF C#:
public sealed class SignalMeter : Control
{
public double Level { get; set; }
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
var bounds = new Rect(0, 0, ActualWidth, ActualHeight);
drawingContext.DrawRectangle(Brushes.Black, null, bounds);
drawingContext.DrawRectangle(Brushes.LimeGreen, null, new Rect(4, 4, Math.Max(0, Level) * (ActualWidth - 8), ActualHeight - 8));
}
}
Avalonia XAML:
<local:SignalMeter xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:MyApp.Controls"
Width="220"
Height="24"
Level="{Binding ProgressRatio}" />
Avalonia C#:
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
public sealed class SignalMeter : Control
{
static SignalMeter()
{
AffectsRender<SignalMeter>(LevelProperty);
}
public static readonly StyledProperty<double> LevelProperty =
AvaloniaProperty.Register<SignalMeter, double>(nameof(Level), 0.0);
public double Level
{
get => GetValue(LevelProperty);
set => SetValue(LevelProperty, value);
}
public override void Render(DrawingContext context)
{
base.Render(context);
var clamped = Math.Clamp(Level, 0.0, 1.0);
var inner = new Rect(4, 4, Math.Max(0, Bounds.Width - 8), Math.Max(0, Bounds.Height - 8));
context.FillRectangle(Brushes.Black, Bounds);
context.FillRectangle(Brushes.LimeGreen, new Rect(inner.X, inner.Y, inner.Width * clamped, inner.Height));
context.DrawRectangle(null, new Pen(Brushes.Gray, 1), inner);
}
}
WPF XAML:
<TextBox Text="{Binding Email, UpdateSourceTrigger=PropertyChanged}" />
WPF C# (conceptual):
var layer = AdornerLayer.GetAdornerLayer(emailTextBox);
layer?.Add(new ValidationAdorner(emailTextBox));
Avalonia XAML:
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<TextBox x:Name="EmailBox"
Width="260"
Text="{Binding Email}">
<AdornerLayer.Adorner>
<Border IsVisible="{Binding HasEmailError}"
Background="#A0FF3B30"
IsHitTestVisible="False">
<TextBlock Margin="6,2" Text="{Binding EmailError}" />
</Border>
</AdornerLayer.Adorner>
</TextBox>
</UserControl>
Avalonia C#:
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Media;
var emailBox = new TextBox { Width = 260 };
var errorOverlay = new Border
{
Background = new SolidColorBrush(Color.Parse("#A0FF3B30")),
IsHitTestVisible = false,
Child = new TextBlock { Text = "Invalid email", Margin = new Avalonia.Thickness(6, 2) }
};
AdornerLayer.SetAdorner(emailBox, errorOverlay);
ICustomDrawOperationFor custom native rendering integration:
ICustomDrawOperation (Bounds, HitTest, Render, Dispose),Render using context.Custom(...),Use this for advanced scenarios, not for normal control styling.
OnRender to Render with AffectsRender on relevant properties.IsHitTestVisible=False).AffectsRender or explicitly call InvalidateVisual.IsHitTestVisible="False" unless interactive behavior is required.