Goal
ContentControl, UserControl, and NameScope help you compose UIs cleanly.ItemsControl with DataTemplate and a simple value converter to repeat UI for collections.xmlns:) and how to reference custom classes or Avalonia namespaces.Why this matters
Prerequisites
dotnet new, dotnet build, and dotnet run on your machine.# Create a new sample app for this chapter
dotnet new avalonia.mvvm -o SampleUiBasics
cd SampleUiBasics
# Restore packages and run once to ensure the template works
dotnet run
Open the project in your IDE before continuing.
The root <Window> tag declares namespaces so XAML can resolve types:
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="clr-namespace:SampleUiBasics.Views"
x:Class="SampleUiBasics.Views.MainWindow">
xmlns:x exposes XAML keywords like x:Name, x:Key, and x:DataType.xmlns:ui) point to CLR namespaces in your project or other assemblies so you can reference your own classes or controls (ui:OrderRow).[XmlnsDefinition] attribute (for example, xmlns:fluent="avares://Avalonia.Themes.Fluent").InitializeComponent() in MainWindow.axaml.cs invokes AvaloniaXamlLoader.Load, wiring the compiled XAML into the partial class defined by x:Class.x:Class values in sync with your namespace; mismatches result in XamlLoadException messages complaining about missing compiled XAML.Open Views/MainWindow.axaml and replace the <Window.Content> with:
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="clr-namespace:SampleUiBasics.Views"
x:Class="SampleUiBasics.Views.MainWindow"
Width="540" Height="420"
Title="Customer overview">
<DockPanel LastChildFill="True" Margin="16">
<TextBlock DockPanel.Dock="Top"
Classes="h1"
Text="Customer overview"
Margin="0,0,0,16"/>
<Grid ColumnDefinitions="2*,3*"
RowDefinitions="Auto,*"
ColumnSpacing="16"
RowSpacing="16">
<StackPanel Grid.Column="0" Spacing="8">
<TextBlock Classes="h2" Text="Details"/>
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto,Auto" RowSpacing="8" ColumnSpacing="12">
<TextBlock Text="Name:"/>
<TextBox Grid.Column="1" Width="200" Text="{Binding Customer.Name}"/>
<TextBlock Grid.Row="1" Text="Email:"/>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Customer.Email}"/>
<TextBlock Grid.Row="2" Text="Status:"/>
<ComboBox Grid.Row="2" Grid.Column="1" SelectedIndex="0">
<ComboBoxItem>Prospect</ComboBoxItem>
<ComboBoxItem>Active</ComboBoxItem>
<ComboBoxItem>Dormant</ComboBoxItem>
</ComboBox>
</Grid>
</StackPanel>
<StackPanel Grid.Column="1" Spacing="8">
<TextBlock Classes="h2" Text="Recent orders"/>
<ItemsControl Items="{Binding RecentOrders}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ui:OrderRow />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</DockPanel>
</Window>
What you just used:
DockPanel places a title bar on top and fills the rest.Grid split into two columns for the form (left) and list (right).ItemsControl repeats a data template for each item in RecentOrders.OrderRow)Add a new file Views/OrderRow.axaml:
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="SampleUiBasics.Views.OrderRow"
Padding="8"
Classes="card">
<Border Background="{DynamicResource ThemeBackgroundBrush}"
CornerRadius="6"
Padding="12">
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,Auto" ColumnSpacing="12">
<TextBlock Classes="h3" Text="{Binding Title}"/>
<TextBlock Grid.Column="1"
Foreground="{DynamicResource ThemeAccentBrush}"
Text="{Binding Total, Converter={StaticResource CurrencyConverter}}"/>
<TextBlock Grid.Row="1" Grid.ColumnSpan="2" Text="{Binding PlacedOn, StringFormat='Ordered on {0:d}'}"/>
</Grid>
</Border>
</UserControl>
UserControl encapsulates UI so you can reuse it via <ui:OrderRow />.Title, Total, PlacedOn) which come from the current item in the data template.Converters adapt data for display. Create Converters/CurrencyConverter.cs:
using System;
using System.Globalization;
using Avalonia.Data.Converters;
namespace SampleUiBasics.Converters;
public sealed class CurrencyConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is decimal amount)
return string.Format(culture, "{0:C}", amount);
return value;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => value;
}
Register the converter in App.axaml so XAML can reference it:
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:SampleUiBasics.Converters"
x:Class="SampleUiBasics.App">
<Application.Resources>
<converters:CurrencyConverter x:Key="CurrencyConverter"/>
</Application.Resources>
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>
Open ViewModels/MainWindowViewModel.cs and replace its contents with:
using System;
using System.Collections.ObjectModel;
namespace SampleUiBasics.ViewModels;
public sealed class MainWindowViewModel
{
public CustomerViewModel Customer { get; } = new("Avery Diaz", "avery@example.com");
public ObservableCollection<OrderViewModel> RecentOrders { get; } = new()
{
new OrderViewModel("Starter subscription", 49.00m, DateTime.Today.AddDays(-2)),
new OrderViewModel("Design add-on", 129.00m, DateTime.Today.AddDays(-12)),
new OrderViewModel("Consulting", 900.00m, DateTime.Today.AddDays(-20))
};
}
public sealed record CustomerViewModel(string Name, string Email);
public sealed record OrderViewModel(string Title, decimal Total, DateTime PlacedOn);
Now bindings like {Binding Customer.Name} and {Binding RecentOrders} have backing data.
ContentControl, UserControl, and NameScopeContentControl (see ContentControl.cs) holds a single content object. Windows, Buttons, and many controls inherit from it. Setting Content or placing child XAML elements populates that content.UserControl (see UserControl.cs) packages a reusable view with its own XAML and code-behind. Each UserControl creates its own NameScope so x:Name values remain local.NameScope (see NameScope.cs) governs how x:Name lookups work. Use this.FindControl<T>("OrdersList") or NameScope.GetNameScope(this) to resolve names inside the nearest scope.Example: add x:Name="OrdersList" to the ItemsControl in MainWindow.axaml and access it from code-behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var ordersList = this.FindControl<ItemsControl>("OrdersList");
// Inspect or manipulate generated visuals here if needed.
}
}
When you nest user controls, remember: a name defined in OrderRow is not visible in MainWindow because each UserControl has its own scope. This avoids name collisions in templated scenarios.
this.GetLogicalChildren() or DevTools -> Logical tree.ContentPresenter) exist in the visual tree but not in the logical tree. When FindControl fails, confirm whether the element is in the logical tree.ItemsControl.ItemTemplate applies a DataTemplate for each item. Inside a data template, the DataContext is the individual item (an OrderViewModel).<DataTemplate x:Key="OrderTemplate"> ... and then ItemTemplate="{StaticResource OrderTemplate}".FindResource)Window.Resources or Application.Resources.FindResource or TryFindResource:<Window.Resources>
<SolidColorBrush x:Key="HighlightBrush" Color="#FFE57F"/>
</Window.Resources>
private void OnHighlight(object? sender, RoutedEventArgs e)
{
if (FindResource("HighlightBrush") is IBrush brush)
{
Background = brush;
}
}
FindResource walks the logical tree first, then escalates to application resources, mirroring how the XAML parser resolves StaticResource.UserControl or DataTemplate are scoped; use this.Resources to override per-view resources without affecting the rest of the app.dotnet run
While the app runs:
OrderRow entries.OrderRow TextBlock and confirm the binding path (Total) resolves to the right data.OrderViewModel values in code and rerun to see updates.x:DataType="vm:OrderViewModel" in templates for compile-time checks (once you add namespaces for view models).App.axaml matches the converter's CLR namespace and the key matches StaticResource CurrencyConverter.xmlns:ui matches the CLR namespace of OrderRow and that the class is partial with matching x:Class.NameScope. If the element is inside a data template, use e.Source from events or bind through the ViewModel instead of searching.ui:AddressCard user control showing billing address details. Bind it to Customer using ContentControl.Content="{Binding Customer}" and define a data template for CustomerViewModel.ValueConverter that highlights orders above $500 by returning a different brush; apply it to the Border background via {Binding Total, Converter=...}.ItemsControl (x:Name="OrdersList") and call this.FindControl<ItemsControl>("OrdersList") in code-behind to verify name scoping.HighlightBrush in MainWindow.Resources and use FindResource to swap the window background at runtime (e.g., from a button click).ListBox instead of ItemsControl and observe how selection adds visual states in the visual tree.OrderRow. Toggle the Namescope overlay to see how scopes nest.NameScope implementation: src/Avalonia.Base/Styling/NameScope.csxmlns) relate to CLR namespaces and assemblies?ContentControl and UserControl differ and when would you choose each?DataTemplate, what object provides the DataContext?What's next