Goal
AvaloniaObject APIs (SetValue, SetCurrentValue, observers) replace attribute syntax when you skip XAML.Why this matters
Prerequisites
StackPanel, Grid, DockPanelAvalonia's panels live in external/Avalonia/src/Avalonia.Controls/. Construct them exactly as you would in XAML, but populate Children and set properties directly.
var layout = new StackPanel
{
Orientation = Orientation.Vertical,
Spacing = 12,
Margin = new Thickness(24),
Children =
{
new TextBlock { Text = "Customer" },
new TextBox { Watermark = "Name" },
new TextBox { Watermark = "Email" }
}
};
StackPanel's measure logic (see StackPanel.cs) respects Spacing and Orientation. Because you're in code, you can wrap control creation in helper methods to keep constructors clean:
private static TextBox CreateLabeledInput(string label, out TextBlock caption)
{
caption = new TextBlock { Text = label, FontWeight = FontWeight.SemiBold };
return new TextBox { Margin = new Thickness(0, 4, 0, 16) };
}
Grid exposes RowDefinitions/ColumnDefinitions collections of RowDefinition/ColumnDefinition. You add definitions and set attached properties programmatically.
var grid = new Grid
{
ColumnDefinitions =
{
new ColumnDefinition(GridLength.Auto),
new ColumnDefinition(GridLength.Star)
},
RowDefinitions =
{
new RowDefinition(GridLength.Auto),
new RowDefinition(GridLength.Auto),
new RowDefinition(GridLength.Star)
}
};
var title = new TextBlock { Text = "Orders", FontSize = 22 };
Grid.SetColumnSpan(title, 2);
grid.Children.Add(title);
var filterLabel = new TextBlock { Text = "Status" };
Grid.SetRow(filterLabel, 1);
Grid.SetColumn(filterLabel, 0);
grid.Children.Add(filterLabel);
var filterBox = new ComboBox { Items = Enum.GetValues<OrderStatus>() };
Grid.SetRow(filterBox, 1);
Grid.SetColumn(filterBox, 1);
grid.Children.Add(filterBox);
Attached property methods (Grid.SetRow, Grid.SetColumnSpan) are static for clarity. Because they ultimately call AvaloniaObject.SetValue, you can wrap them in fluent helpers if you prefer chaining (example later in section 3).
DockPanel (source: DockPanel.cs) uses the Dock attached property. From code you set it with DockPanel.SetDock(control, Dock.Left).
var dock = new DockPanel
{
LastChildFill = true,
Children =
{
CreateSidebar().DockLeft(),
CreateFooter().DockBottom(),
CreateMainRegion()
}
};
Implement DockLeft() as an extension to keep code terse:
public static class DockExtensions
{
public static T DockLeft<T>(this T control) where T : Control
{
DockPanel.SetDock(control, Dock.Left);
return control;
}
public static T DockBottom<T>(this T control) where T : Control
{
DockPanel.SetDock(control, Dock.Bottom);
return control;
}
}
You own these helpers, so you can tailor them for your team's conventions (dock with margins, apply classes, etc.).
SetValue, SetCurrentValue, observersWithout XAML attribute syntax you interact with AvaloniaProperty APIs directly. Every control inherits from AvaloniaObject (AvaloniaObject.cs), which exposes:
SetValue(AvaloniaProperty property, object? value) – sets the property locally, raising change notifications and affecting bindings.SetCurrentValue(AvaloniaProperty property, object? value) – updates the effective value but preserves existing bindings/animations (great for programmatic defaults).GetObservable<T>(AvaloniaProperty<T>) – returns an IObservable<T?> when you need to react to changes.Example: highlight focused text boxes by toggling a pseudo-class while keeping bindings intact.
var box = new TextBox();
box.GotFocus += (_, _) => box.PseudoClasses.Set(":focused", true);
box.LostFocus += (_, _) => box.PseudoClasses.Set(":focused", false);
// Provide a default width but leave bindings alone
box.SetCurrentValue(TextBox.WidthProperty, 240);
To wire property observers, use GetObservable or GetPropertyChangedObservable (for any property change):
box.GetObservable(TextBox.TextProperty)
.Subscribe(text => _logger.Information("Text changed to {Text}", text));
GetObservable is defined in AvaloniaObject. Remember to dispose subscriptions when controls leave the tree—store IDisposable tokens and call Dispose in your control's DetachedFromVisualTree handler.
When repeating property patterns, encapsulate them:
public static class ControlHelpers
{
public static T WithMargin<T>(this T control, Thickness margin) where T : Control
{
control.Margin = margin;
return control;
}
public static T Bind<T, TValue>(this T control, AvaloniaProperty<TValue> property, IBinding binding)
where T : AvaloniaObject
{
control.Bind(property, binding);
return control;
}
}
These mirror markup extensions in code, making complex layouts more declarative.
Large code-first views benefit from factory methods that return configured controls. Compose factories from smaller functions to keep logic readable.
public static class DashboardViewFactory
{
public static Control Create(IDashboardViewModel vm)
{
return new Grid
{
ColumnDefinitions =
{
new ColumnDefinition(GridLength.Star),
new ColumnDefinition(GridLength.Star)
},
Children =
{
CreateSummary(vm).WithGridPosition(0, 0),
CreateChart(vm).WithGridPosition(0, 1)
}
};
}
private static Control CreateSummary(IDashboardViewModel vm)
=> new Border
{
Padding = new Thickness(24),
Child = new TextBlock().Bind(TextBlock.TextProperty, new Binding(nameof(vm.TotalSales)))
};
}
WithGridPosition is a fluent helper you define:
public static class GridExtensions
{
public static T WithGridPosition<T>(this T element, int row, int column) where T : Control
{
Grid.SetRow(element, row);
Grid.SetColumn(element, column);
return element;
}
}
This approach keeps UI declarations near data bindings, reducing mental overhead for reviewers.
Because you're in C#, generate children dynamically:
var cards = vm.Notifications.Select((item, index) =>
CreateNotificationCard(item).WithGridPosition(index / 3, index % 3));
var grid = new Grid
{
ColumnDefinitions = { new ColumnDefinition(GridLength.Star), new ColumnDefinition(GridLength.Star), new ColumnDefinition(GridLength.Star) }
};
foreach (var card in cards)
{
grid.Children.Add(card);
}
Grid measure logic handles dynamic counts; just ensure RowDefinitions fits the generated children (add rows as needed or rely on GridLength.Auto).
Factories can return both controls and supporting Styles:
public static Styles DashboardStyles { get; } = new Styles
{
new Style(x => x.OfType<TextBlock>().Class("section-title"))
{
Setters = { new Setter(TextBlock.FontSizeProperty, 18), new Setter(TextBlock.FontWeightProperty, FontWeight.SemiBold) }
}
};
Merge these into Application.Current.Styles in App.Initialize() or on demand when the feature loads.
NameScope, logical/visual trees, and lookupXAML automatically registers names in a NameScope. In code-first views you create and assign it manually when you need element lookup or ElementName-like references.
var scope = new NameScope();
var container = new Grid();
NameScope.SetNameScope(container, scope);
var detailPanel = new StackPanel { Orientation = Orientation.Vertical };
scope.Register("DetailPanel", detailPanel);
container.Children.Add(detailPanel);
Later you can resolve controls with FindControl<T>:
var detail = container.FindControl<StackPanel>("DetailPanel");
NameScope implementation lives in external/Avalonia/src/Avalonia.Base/LogicalTree/NameScope.cs. Remember that nested scopes behave like XAML: children inherit the nearest scope unless you assign a new one.
Avalonia's logical tree helpers (LogicalTreeExtensions.cs) are just as useful without XAML. Use them to inspect or traverse the tree:
Control? parent = myControl.GetLogicalParent();
IEnumerable<IControl> children = myControl.GetLogicalChildren().OfType<IControl>();
This is handy when you dynamically add/remove controls and need to ensure data contexts or resources flow correctly. To validate at runtime, enable DevTools (Avalonia.Diagnostics) even in code-only views—the visual tree is identical.
TabControl and dynamic pagesTabControl expects TabItem children. Compose them programmatically and bind headers/content.
var tabControl = new TabControl
{
Items = new[]
{
new TabItem
{
Header = "Overview",
Content = new OverviewView { DataContext = vm.Overview }
},
new TabItem
{
Header = "Details",
Content = CreateDetailsGrid(vm.Details)
}
}
};
If you prefer data-driven tabs, set Items to a collection of view-models and provide ItemTemplate using FuncDataTemplate (see Chapter 36 for full coverage). Even then, you create the template in code:
tabControl.ItemTemplate = new FuncDataTemplate<IDetailViewModel>((context, _) =>
new DetailView { DataContext = context },
supportsRecycling: true);
ItemsControl and ListBox take Items plus optional panel templates. Build the items panel in code to control layout.
var list = new ListBox
{
ItemsPanel = new FuncTemplate<Panel?>(() => new WrapPanel { ItemWidth = 160, ItemHeight = 200 }),
Items = vm.Products.Select(p => CreateProductCard(p))
};
Here FuncTemplate comes from Avalonia.Controls.Templates (source: FuncTemplate.cs). It mirrors <ItemsPanelTemplate>.
Controls like FlyoutBase or Popup are fully accessible in code. Example: attach a contextual menu.
var button = new Button { Content = "Options" };
button.Flyout = new MenuFlyout
{
Items =
{
new MenuItem { Header = "Refresh", Command = vm.RefreshCommand },
new MenuItem { Header = "Export", Command = vm.ExportCommand }
}
};
The object initializer syntax keeps the code close to the equivalent XAML while exposing full IntelliSense.
Because no XAML compilation step validates your layout, lean on:
Avalonia.Headless to instantiate controls and assert layout bounds.AttachDevTools() in debug builds ).Example headless test snippet:
[Fact]
public void Summary_panel_contains_totals()
{
using var app = AvaloniaApp();
var view = DashboardViewFactory.Create(new FakeDashboardVm());
var panel = view.GetLogicalDescendants().OfType<TextBlock>()
.First(t => t.Classes.Contains("total"));
panel.Text.Should().Be("$42,000");
}
GetLogicalDescendants is defined in LogicalTreeExtensions. Pair this with Chapter 38 for deeper testing patterns.
StackPanel form built in code. Refactor it to a Grid with columns and auto-sizing rows using only C# helpers. Confirm layout parity via DevTools.DashboardViewFactory that returns a Grid with cards arranged dynamically based on a view-model collection. Add fluent helpers for grid position, dock, and margin management.Grid.GetRow, DockPanel.GetDock) to prevent regressions.Func<Control>. Merge their Styles/ResourceDictionary contributions when modules activate and remove them when deactivated.RenderTimerDiagnostics from DevTools to monitor layout passes. Compare baseline vs. dynamic code generation to ensure your factories don't introduce unnecessary measure/arrange churn.Mastering these patterns means you can weave Avalonia's layout system into any C#-driven architecture—no XAML required, just the underlying property system and a toolbox of fluent helpers tailored to your project.
What's next