LayoutManager, Measure/Arrange) to Avalonia Layout PipelinePanel Migration ExamplePrimary WPF APIs:
UIElement.Measure(Size), Arrange(Rect), DesiredSizeFrameworkElement.MeasureOverride(Size), ArrangeOverride(Size)InvalidateMeasure(), InvalidateArrange(), UpdateLayout()Grid, DockPanel, WrapPanel, StackPanel, GridSplitter, Grid.IsSharedSizeScopePrimary Avalonia APIs:
Layoutable.Measure(Size), Arrange(Rect), DesiredSizeLayoutable.MeasureOverride(Size), ArrangeOverride(Size)InvalidateMeasure(), InvalidateArrange(), UpdateLayout()Grid, DockPanel, WrapPanel, StackPanel, GridSplitter, Grid.IsSharedSizeScopeLayoutable.UseLayoutRounding, Layoutable.LayoutUpdatedWPF layout is dispatcher-integrated and queue-based:
LayoutManager executes measure then arrange passes.LayoutUpdated signals completion of a pass.Migration implications:
UpdateLayout() calls in old WPF code usually indicate architectural coupling,ActualWidth/ActualHeight timing assumptions often need cleanup during migration,MeasureOverride/ArrangeOverride ports conceptually 1:1.Avalonia uses the same high-level model:
InvalidateMeasure/InvalidateArrange.LayoutManager batches and executes passes.MeasureOverride computes desired size; ArrangeOverride positions children.LayoutUpdated can be observed but should not drive main behavior logic.Key differences to plan for:
| WPF | Avalonia |
|---|---|
MeasureOverride/ArrangeOverride |
same override model |
InvalidateMeasure/InvalidateArrange |
same methods and intent |
UpdateLayout |
available, but avoid in normal app flow |
Grid.IsSharedSizeScope + SharedSizeGroup |
same pattern |
GridSplitter |
GridSplitter |
UseLayoutRounding |
UseLayoutRounding on Layoutable |
LayoutUpdated event |
LayoutUpdated event on Layoutable |
LayoutUpdated handlers.Auto, *, alignment.ScrollViewer) instead of implicit container behavior.WPF XAML:
<Grid Grid.IsSharedSizeScope="True"
RowDefinitions="Auto,*"
ColumnDefinitions="220,5,*">
<Grid Grid.Row="0" Grid.ColumnSpan="3" ColumnDefinitions="Auto,*">
<TextBlock Grid.Column="0" Text="Customer" />
<TextBox Grid.Column="1" />
</Grid>
<TreeView Grid.Row="1" Grid.Column="0" />
<GridSplitter Grid.Row="1" Grid.Column="1" Width="5" HorizontalAlignment="Stretch" />
<ListView Grid.Row="1" Grid.Column="2" />
</Grid>
Avalonia XAML:
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:MyApp.ViewModels"
x:DataType="vm:CustomerShellViewModel">
<Grid Grid.IsSharedSizeScope="True"
RowDefinitions="Auto,*"
ColumnDefinitions="220,6,*"
RowSpacing="8"
ColumnSpacing="0">
<Grid Grid.Row="0" Grid.ColumnSpan="3" ColumnDefinitions="Auto,*" ColumnSpacing="8">
<TextBlock Grid.Column="0" VerticalAlignment="Center" Text="Customer" />
<TextBox Grid.Column="1" Text="{CompiledBinding Query}" />
</Grid>
<TreeView Grid.Row="1" Grid.Column="0" ItemsSource="{CompiledBinding Groups}" />
<GridSplitter Grid.Row="1" Grid.Column="1" Width="6" ResizeDirection="Columns" />
<ListBox Grid.Row="1" Grid.Column="2" ItemsSource="{CompiledBinding Items}" />
</Grid>
</UserControl>
Avalonia C#:
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
var root = new Grid
{
RowDefinitions = RowDefinitions.Parse("Auto,*"),
ColumnDefinitions = ColumnDefinitions.Parse("220,6,*"),
RowSpacing = 8
};
Grid.SetIsSharedSizeScope(root, true);
var header = new Grid
{
ColumnDefinitions = ColumnDefinitions.Parse("Auto,*"),
ColumnSpacing = 8
};
Grid.SetColumnSpan(header, 3);
header.Children.Add(new TextBlock { Text = "Customer", VerticalAlignment = VerticalAlignment.Center });
var queryBox = new TextBox { Text = viewModel.Query };
Grid.SetColumn(queryBox, 1);
header.Children.Add(queryBox);
root.Children.Add(header);
var groups = new TreeView { ItemsSource = viewModel.Groups };
Grid.SetRow(groups, 1);
root.Children.Add(groups);
var splitter = new GridSplitter { Width = 6, ResizeDirection = GridResizeDirection.Columns };
Grid.SetRow(splitter, 1);
Grid.SetColumn(splitter, 1);
root.Children.Add(splitter);
var items = new ListBox { ItemsSource = viewModel.Items };
Grid.SetRow(items, 1);
Grid.SetColumn(items, 2);
root.Children.Add(items);
Panel Migration ExampleWPF C#:
public sealed class TimelinePanel : Panel
{
protected override Size MeasureOverride(Size availableSize)
{
foreach (UIElement child in InternalChildren)
{
child.Measure(new Size(availableSize.Width, double.PositiveInfinity));
}
var height = InternalChildren.Cast<UIElement>().Sum(x => x.DesiredSize.Height);
return new Size(availableSize.Width, height);
}
protected override Size ArrangeOverride(Size finalSize)
{
double y = 0;
foreach (UIElement child in InternalChildren)
{
child.Arrange(new Rect(0, y, finalSize.Width, child.DesiredSize.Height));
y += child.DesiredSize.Height;
}
return finalSize;
}
}
Avalonia XAML:
<local:TimelinePanel xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:MyApp.Controls">
<Border Height="56" />
<Border Height="72" />
<Border Height="40" />
</local:TimelinePanel>
Avalonia C#:
using Avalonia;
using Avalonia.Controls;
public sealed class TimelinePanel : Panel
{
protected override Size MeasureOverride(Size availableSize)
{
foreach (var child in Children)
{
child.Measure(new Size(availableSize.Width, double.PositiveInfinity));
}
var totalHeight = 0.0;
foreach (var child in Children)
{
totalHeight += child.DesiredSize.Height;
}
return new Size(availableSize.Width, totalHeight);
}
protected override Size ArrangeOverride(Size finalSize)
{
var y = 0.0;
foreach (var child in Children)
{
var h = child.DesiredSize.Height;
child.Arrange(new Rect(0, y, finalSize.Width, h));
y += h;
}
return finalSize;
}
}
UpdateLayout() in command handlers unless absolutely required.Grid.IsSharedSizeScope="True" is set on a common ancestor and SharedSizeGroup names match.finalSize, not stale measured width.