Avalonia’s layout system is a two-pass process that determines the size and position of every element in the visual tree. Understanding this system is crucial for creating responsive, well-behaved UIs.
Layout Process Overview
The layout system executes in two phases:
┌────────────────────────────────┐
│ 1. MEASURE PASS │
│ (Top-down: size constraints) │
│ (Bottom-up: desired sizes) │
└────────────────────────────────┘
│
↓
┌────────────────────────────────┐
│ 2. ARRANGE PASS │
│ (Top-down: final positions) │
│ (Children positioned) │
└────────────────────────────────┘
Measure Pass
Determines how much space each element needs:
Parent passes available size to children
Children calculate their desired size
Results stored in DesiredSize property
Arrange Pass
Positions elements:
Parent allocates final space to children
Children position themselves within allocated space
Final bounds stored in Bounds property
Layoutable Class
The Layoutable base class implements layout functionality:
Layoutable Properties (from source)
using Avalonia . Layout ;
public class Layoutable : Visual
{
// Size constraints
public double Width { get ; set ; } // Explicit width
public double Height { get ; set ; } // Explicit height
public double MinWidth { get ; set ; } // Minimum width
public double MaxWidth { get ; set ; } // Maximum width
public double MinHeight { get ; set ; } // Minimum height
public double MaxHeight { get ; set ; } // Maximum height
// Alignment
public HorizontalAlignment HorizontalAlignment { get ; set ; }
public VerticalAlignment VerticalAlignment { get ; set ; }
// Spacing
public Thickness Margin { get ; set ; } // External spacing
// Layout result
public Size DesiredSize { get ; } // Calculated desired size
// Layout rounding
public bool UseLayoutRounding { get ; set ; } // Pixel-perfect rendering
}
Size Properties
Width & Height
Min/Max
Size Precedence
<!-- Explicit size -->
< Button Width = "100" Height = "40" Content = "Fixed Size" />
<!-- NaN means auto-size -->
< Button Width = "NaN" Height = "NaN" Content = "Auto Size" />
<!-- In code -->
button . Width = 100 ;
button . Height = double . NaN ; // Auto height
<!-- Constrained sizing -->
< TextBox MinWidth = "100" MaxWidth = "300" />
<!-- Responsive button -->
< Button MinWidth = "80"
MinHeight = "32"
MaxWidth = "200"
Content = "Flexible" />
Final Size Calculation:
1. Start with DesiredSize
2. Apply MinWidth/MinHeight
3. Apply MaxWidth/MaxHeight
4. Apply Width/Height (if set)
Alignment
Controls how elements position within their allocated space:
HorizontalAlignment
VerticalAlignment
< StackPanel >
< Button HorizontalAlignment = "Left"
Content = "Left" />
< Button HorizontalAlignment = "Center"
Content = "Center" />
< Button HorizontalAlignment = "Right"
Content = "Right" />
< Button HorizontalAlignment = "Stretch"
Content = "Stretch" />
</ StackPanel >
Alignment Enums (from source)
public enum HorizontalAlignment
{
Stretch , // Fill available width (default)
Left , // Align to left edge
Center , // Center horizontally
Right // Align to right edge
}
public enum VerticalAlignment
{
Stretch , // Fill available height (default)
Top , // Align to top edge
Center , // Center vertically
Bottom // Align to bottom edge
}
Margin
Creates space around an element:
<!-- Uniform margin -->
< Button Margin = "10" Content = "All sides" />
<!-- Horizontal, Vertical -->
< Button Margin = "20,10" Content = "H: 20, V: 10" />
<!-- Left, Top, Right, Bottom -->
< Button Margin = "10,5,10,5" Content = "Specific sides" />
<!-- Named properties -->
< Button Content = "Individual" >
< Button.Margin >
< Thickness Left = "10" Top = "5" Right = "10" Bottom = "15" />
</ Button.Margin >
</ Button >
Margin creates space outside the element, while Padding (on controls like Border) creates space inside .
Layout Panels
Panels are containers that implement specific layout strategies:
Panel Base Class
Panel.cs (Simplified from source)
using Avalonia . Controls ;
public class Panel : Control
{
// Children collection
public Controls Children { get ; }
// Background brush
public IBrush ? Background { get ; set ; }
// Override to implement custom layout
protected override Size MeasureOverride ( Size availableSize )
{
// Measure children
// Return desired size
}
protected override Size ArrangeOverride ( Size finalSize )
{
// Position children
// Return used size
}
}
StackPanel
Arranges children in a vertical or horizontal stack:
Vertical Stack (Default)
Horizontal Stack
StackPanel Properties (from source)
< StackPanel Spacing = "10" >
< TextBlock Text = "Item 1" />
< TextBlock Text = "Item 2" />
< TextBlock Text = "Item 3" />
</ StackPanel >
Characteristics:
Children sized to content in stack direction
Children stretched in perpendicular direction (unless alignment set)
No wrapping
Grid
Arranges children in rows and columns:
< Grid RowDefinitions = "Auto,*,Auto"
ColumnDefinitions = "200,*" >
<!-- Header spans both columns -->
< TextBlock Grid.Row = "0"
Grid.Column = "0"
Grid.ColumnSpan = "2"
Text = "Header" />
<!-- Sidebar -->
< Border Grid.Row = "1" Grid.Column = "0"
Background = "LightGray" >
< TextBlock Text = "Sidebar" />
</ Border >
<!-- Main content -->
< ScrollViewer Grid.Row = "1" Grid.Column = "1" >
< TextBlock Text = "Content area" />
</ ScrollViewer >
<!-- Footer spans both columns -->
< TextBlock Grid.Row = "2"
Grid.Column = "0"
Grid.ColumnSpan = "2"
Text = "Footer" />
</ Grid >
Grid Sizing:
Sizes to content < Grid RowDefinitions = "Auto,Auto" >
< TextBlock Grid.Row = "0" Text = "Sized to content" />
< Button Grid.Row = "1" Content = "Also sized to content" />
</ Grid >
Proportional sizing < Grid ColumnDefinitions = "*,2*,*" >
<!-- Ratio: 1:2:1 -->
< Border Grid.Column = "0" /> <!-- 25% -->
< Border Grid.Column = "1" /> <!-- 50% -->
< Border Grid.Column = "2" /> <!-- 25% -->
</ Grid >
Fixed pixel size < Grid ColumnDefinitions = "200,*" >
< Border Grid.Column = "0" /> <!-- 200px -->
< Border Grid.Column = "1" /> <!-- Remaining space -->
</ Grid >
DockPanel
Docks children to edges:
< DockPanel LastChildFill = "True" >
<!-- Dock to edges -->
< Menu DockPanel.Dock = "Top" />
< StatusBar DockPanel.Dock = "Bottom" />
< TreeView DockPanel.Dock = "Left" Width = "200" />
<!-- Last child fills remaining space -->
< ContentControl Content = "{Binding MainContent}" />
</ DockPanel >
Dock Values: Left, Top, Right, Bottom
Canvas
Absolute positioning:
< Canvas Width = "400" Height = "300" >
< Rectangle Canvas.Left = "50"
Canvas.Top = "50"
Width = "100"
Height = "100"
Fill = "Blue" />
< Ellipse Canvas.Left = "200"
Canvas.Top = "100"
Width = "80"
Height = "80"
Fill = "Red" />
</ Canvas >
Canvas doesn’t adapt to different screen sizes. Use sparingly for custom graphics or fixed layouts.
WrapPanel
Wraps children to next line when space runs out:
< WrapPanel Orientation = "Horizontal" >
< Button Content = "Button 1" />
< Button Content = "Button 2" />
< Button Content = "Button 3" />
< Button Content = "Button 4" />
< Button Content = "Button 5" />
<!-- Wraps to next line when needed -->
</ WrapPanel >
Equal-sized cells:
< UniformGrid Rows = "2" Columns = "3" >
< Button Content = "1" />
< Button Content = "2" />
< Button Content = "3" />
< Button Content = "4" />
< Button Content = "5" />
< Button Content = "6" />
</ UniformGrid >
Layout Invalidation
Avalonia automatically re-layouts when properties change:
// These properties trigger layout updates:
button . Width = 200 ; // Invalidates measure
button . Margin = new Thickness ( 10 ); // Invalidates measure
stackPanel . Children . Add ( newChild ); // Invalidates measure
// Manual invalidation (rarely needed)
control . InvalidateMeasure ();
control . InvalidateArrange ();
Minimize layout passes
Batch property changes to avoid multiple layout updates.
Use virtualization
For long lists, use VirtualizingStackPanel or VirtualizingPanel.
Avoid nested measure-dependent layouts
Deep nesting of Auto-sized elements can be slow.
Use layout rounding sparingly
UseLayoutRounding="True" has a performance cost.
Custom Layout
Create custom panels by overriding measure and arrange:
using Avalonia ;
using Avalonia . Controls ;
using Avalonia . Layout ;
public class CircularPanel : Panel
{
protected override Size MeasureOverride ( Size availableSize )
{
// Measure all children
foreach ( var child in Children )
{
child . Measure ( availableSize );
}
// Return desired size
return new Size ( 200 , 200 );
}
protected override Size ArrangeOverride ( Size finalSize )
{
var count = Children . Count ;
if ( count == 0 ) return finalSize ;
var radius = Math . Min ( finalSize . Width , finalSize . Height ) / 2 - 20 ;
var center = new Point ( finalSize . Width / 2 , finalSize . Height / 2 );
var angleStep = 2 * Math . PI / count ;
// Arrange children in circle
for ( int i = 0 ; i < count ; i ++ )
{
var child = Children [ i ];
var angle = i * angleStep ;
var x = center . X + radius * Math . Cos ( angle ) - child . DesiredSize . Width / 2 ;
var y = center . Y + radius * Math . Sin ( angle ) - child . DesiredSize . Height / 2 ;
child . Arrange ( new Rect ( new Point ( x , y ), child . DesiredSize ));
}
return finalSize ;
}
}
Layout Debugging
Enable visual layout debugging:
// In your Window constructor
public MainWindow ()
{
InitializeComponent ();
# if DEBUG
this . AttachDevTools ();
# endif
}
Press F12 to open DevTools and inspect layout bounds.
Best Practices
Use appropriate panels
Choose the right panel for your layout needs. Grid for complex layouts, StackPanel for simple stacks.
Avoid explicit sizes
Let controls size to content when possible. Use Min/Max constraints instead of fixed sizes.
Leverage alignment
Use HorizontalAlignment and VerticalAlignment instead of margins for positioning.
Use Grid star sizing
For responsive layouts, prefer star sizing (*) over absolute sizes.
Consider layout performance
Deep nesting and complex layouts can impact performance. Profile and optimize as needed.