LayoutEngine -> custom Panel)Primary WinForms APIs:
Control.PerformLayout(), SuspendLayout(), ResumeLayout(bool)Control.Dock, Control.Anchor, AutoSize, GetPreferredSize(Size)LayoutEngine, LayoutEventArgs, LayoutTransactionFlowLayoutPanel, TableLayoutPanel, SplitContainer, ScrollableControl.AutoScrollPrimary Avalonia APIs:
Layoutable.Measure(Size), Arrange(Rect), InvalidateMeasure(), InvalidateArrange(), UpdateLayout()Layoutable.MeasureOverride(Size), ArrangeOverride(Size), DesiredSizeLayoutable.UseLayoutRounding, Layoutable.LayoutUpdatedGrid, DockPanel, WrapPanel, StackPanel, ScrollViewer, GridSplitterWinForms layout is event/transaction-driven:
Dock, Anchor, Size, font, visibility) trigger PerformLayout.LayoutEngine on each container computes child bounds.Layout events can recurse if child sizes changed.SuspendLayout/ResumeLayout(true) are used to batch and avoid repeated passes.Implications for migration:
Bounds directly.AutoSize + GetPreferredSize behavior is container-specific.Avalonia layout is invalidation-queued and root-driven:
Layoutable nodes.LayoutManager executes measure pass then arrange pass for the visual root.MeasureOverride computes desired size; ArrangeOverride places children.Migration implications:
Auto, *, alignments, margins) over manual bounds math.InvalidateMeasure/InvalidateArrange when layout-affecting state changes.UpdateLayout() exists, but should stay a last resort for edge interoperability paths.| WinForms | Avalonia |
|---|---|
SuspendLayout/ResumeLayout |
Not usually required; layout manager batches invalidations automatically |
PerformLayout |
InvalidateMeasure/InvalidateArrange, optional UpdateLayout() |
DockStyle.Fill |
Grid star-sized cell or last DockPanel child |
AnchorStyles.Bottom | Right |
place in bottom/right-aligned Grid cell |
AutoScroll |
ScrollViewer |
TableLayoutPanel |
Grid with RowDefinitions/ColumnDefinitions and optional shared-size groups |
FlowLayoutPanel |
WrapPanel or custom Panel |
Layout event |
property-driven layout invalidation + optional LayoutUpdated observers |
Bounds writes from feature code first.Grid + DockPanel + GridSplitter.AutoScroll containers with explicit ScrollViewer boundaries.TableLayoutPanel forms to explicit rows/columns and shared-size groups where needed.LayoutEngine to a dedicated Avalonia Panel (MeasureOverride/ArrangeOverride).InvalidateMeasure vs InvalidateVisual).WinForms C# (typical designer/runtime mix):
SuspendLayout();
var header = new Panel { Dock = DockStyle.Top, Height = 48 };
var title = new Label { Text = "Orders", AutoSize = true, Left = 12, Top = 14 };
var save = new Button { Text = "Save", Width = 96, Anchor = AnchorStyles.Top | AnchorStyles.Right };
save.Left = ClientSize.Width - save.Width - 12;
save.Top = 10;
header.Controls.Add(title);
header.Controls.Add(save);
var split = new SplitContainer
{
Dock = DockStyle.Fill,
SplitterDistance = 280,
FixedPanel = FixedPanel.Panel1
};
split.Panel1.Controls.Add(new TreeView { Dock = DockStyle.Fill });
split.Panel2.Controls.Add(new ListView { Dock = DockStyle.Fill, View = View.Details });
Controls.Add(split);
Controls.Add(header);
ResumeLayout(performLayout: true);
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:OrdersShellViewModel">
<Grid RowDefinitions="Auto,*" ColumnDefinitions="280,6,*">
<Border Grid.Row="0" Grid.ColumnSpan="3" Background="{DynamicResource ThemeControlMidBrush}" Padding="12,8">
<Grid ColumnDefinitions="*,Auto">
<TextBlock VerticalAlignment="Center" Text="Orders" FontSize="16" />
<Button Grid.Column="1"
Width="96"
HorizontalAlignment="Right"
Command="{CompiledBinding SaveCommand}"
Content="Save" />
</Grid>
</Border>
<ScrollViewer Grid.Row="1" Grid.Column="0">
<TreeView ItemsSource="{CompiledBinding Groups}" />
</ScrollViewer>
<GridSplitter Grid.Row="1" Grid.Column="1" Width="6" ResizeDirection="Columns" />
<ScrollViewer Grid.Row="1" Grid.Column="2">
<ListBox ItemsSource="{CompiledBinding Orders}" />
</ScrollViewer>
</Grid>
</UserControl>
Avalonia C#:
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
var root = new Grid
{
RowDefinitions = RowDefinitions.Parse("Auto,*"),
ColumnDefinitions = ColumnDefinitions.Parse("280,6,*")
};
var header = new Border { Padding = new Thickness(12, 8) };
Grid.SetColumnSpan(header, 3);
var headerGrid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("*,Auto") };
headerGrid.Children.Add(new TextBlock { Text = "Orders", FontSize = 16, VerticalAlignment = VerticalAlignment.Center });
var saveButton = new Button
{
Width = 96,
Content = "Save",
HorizontalAlignment = HorizontalAlignment.Right,
Command = viewModel.SaveCommand
};
Grid.SetColumn(saveButton, 1);
headerGrid.Children.Add(saveButton);
header.Child = headerGrid;
root.Children.Add(header);
var left = new ScrollViewer { Content = new TreeView { ItemsSource = viewModel.Groups } };
Grid.SetRow(left, 1);
root.Children.Add(left);
var splitter = new GridSplitter { Width = 6, ResizeDirection = GridResizeDirection.Columns };
Grid.SetRow(splitter, 1);
Grid.SetColumn(splitter, 1);
root.Children.Add(splitter);
var right = new ScrollViewer { Content = new ListBox { ItemsSource = viewModel.Orders } };
Grid.SetRow(right, 1);
Grid.SetColumn(right, 2);
root.Children.Add(right);
LayoutEngine -> custom Panel)WinForms C# (LayoutEngine):
using System.Windows.Forms;
using System.Windows.Forms.Layout;
public sealed class TileHost : Panel
{
private readonly LayoutEngine _engine = new TileLayoutEngine();
public override LayoutEngine LayoutEngine => _engine;
}
file sealed class TileLayoutEngine : LayoutEngine
{
public override bool Layout(object container, LayoutEventArgs layoutEventArgs)
{
var parent = (TileHost)container;
const int spacing = 8;
const int itemWidth = 180;
const int itemHeight = 96;
var x = spacing;
var y = spacing;
foreach (Control child in parent.Controls)
{
if (x + itemWidth > parent.ClientSize.Width)
{
x = spacing;
y += itemHeight + spacing;
}
child.Bounds = new Rectangle(x, y, itemWidth, itemHeight);
x += itemWidth + spacing;
}
return false;
}
}
Avalonia XAML:
<local:TilePanel xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:MyApp.Controls"
ItemWidth="180"
ItemHeight="96"
Spacing="8">
<Button Content="Orders" />
<Button Content="Invoices" />
<Button Content="Shipments" />
<Button Content="Returns" />
</local:TilePanel>
Avalonia C# (Panel):
using Avalonia;
using Avalonia.Controls;
public sealed class TilePanel : Panel
{
static TilePanel()
{
AffectsMeasure<TilePanel>(ItemWidthProperty, ItemHeightProperty, SpacingProperty);
}
public static readonly StyledProperty<double> ItemWidthProperty =
AvaloniaProperty.Register<TilePanel, double>(nameof(ItemWidth), 180);
public static readonly StyledProperty<double> ItemHeightProperty =
AvaloniaProperty.Register<TilePanel, double>(nameof(ItemHeight), 96);
public static readonly StyledProperty<double> SpacingProperty =
AvaloniaProperty.Register<TilePanel, double>(nameof(Spacing), 8);
public double ItemWidth
{
get => GetValue(ItemWidthProperty);
set => SetValue(ItemWidthProperty, value);
}
public double ItemHeight
{
get => GetValue(ItemHeightProperty);
set => SetValue(ItemHeightProperty, value);
}
public double Spacing
{
get => GetValue(SpacingProperty);
set => SetValue(SpacingProperty, value);
}
protected override Size MeasureOverride(Size availableSize)
{
foreach (var child in Children)
{
child.Measure(new Size(ItemWidth, ItemHeight));
}
var width = double.IsInfinity(availableSize.Width) ? ItemWidth * Children.Count : availableSize.Width;
var x = Spacing;
var y = Spacing;
var maxY = y;
foreach (var _ in Children)
{
if (x + ItemWidth > width && x > Spacing)
{
x = Spacing;
y += ItemHeight + Spacing;
}
maxY = y + ItemHeight;
x += ItemWidth + Spacing;
}
return new Size(width, maxY + Spacing);
}
protected override Size ArrangeOverride(Size finalSize)
{
var x = Spacing;
var y = Spacing;
foreach (var child in Children)
{
if (x + ItemWidth > finalSize.Width && x > Spacing)
{
x = Spacing;
y += ItemHeight + Spacing;
}
child.Arrange(new Rect(x, y, ItemWidth, ItemHeight));
x += ItemWidth + Spacing;
}
return finalSize;
}
}
ScrollViewer intentionally at boundaries where WinForms used AutoScroll.Left/Top/Width/Height mutation loops directly.UpdateLayout() in normal command paths.LayoutUpdated handlers; remove or debounce.Anchor.
Grid row/column sizing and alignments (Auto, *, HorizontalAlignment, VerticalAlignment).finalSize in ArrangeOverride, not old cached bounds.