3. Your first UI: layouts, controls, and XAML basics

Goal

Why this matters

Prerequisites

1. Scaffold the sample project

# 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.

2. Quick primer on XAML namespaces

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">

3. How Avalonia loads this XAML

4. Build the main layout (StackPanel + Grid)

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:

5. Create a reusable user control (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>

6. Add a value converter

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>

7. Populate the ViewModel with nested data

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.

8. Understand ContentControl, UserControl, and NameScope

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.

9. Logical tree vs visual tree (why it matters)

10. Data templates explained

11. Work with resources (FindResource)

<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;
    }
}

12. Run, inspect, and iterate

dotnet run

While the app runs:

Troubleshooting

Practice and validation

  1. Add a ui:AddressCard user control showing billing address details. Bind it to Customer using ContentControl.Content="{Binding Customer}" and define a data template for CustomerViewModel.
  2. Add a ValueConverter that highlights orders above $500 by returning a different brush; apply it to the Border background via {Binding Total, Converter=...}.
  3. Name the ItemsControl (x:Name="OrdersList") and call this.FindControl<ItemsControl>("OrdersList") in code-behind to verify name scoping.
  4. Override HighlightBrush in MainWindow.Resources and use FindResource to swap the window background at runtime (e.g., from a button click).
  5. Add a ListBox instead of ItemsControl and observe how selection adds visual states in the visual tree.
  6. Use DevTools to inspect both logical and visual trees for OrderRow. Toggle the Namescope overlay to see how scopes nest.

Look under the hood (source bookmarks)

Check yourself

What's next