Understanding XAML in Avalonia: Your First Steps into Cross-Platform Desktop Development

Getting started with Avalonia UI on Linux doesn’t have to be intimidating. Here’s everything you need to know about XAML, layouts, and data binding.

If you’re a beginner programmer looking to build beautiful desktop applications that run on Linux, Windows, and macOS, you’ve probably heard about Avalonia. But when you first encounter XAML (Extensible Application Markup Language), it might feel like learning a completely new language. Don’t worry – by the end of this guide, you’ll understand how XAML works in Avalonia and be ready to build your first cross-platform desktop app.

What Makes Avalonia Special for Linux Developers?

Unlike many desktop frameworks that treat Linux as an afterthought, Avalonia was built from the ground up to be truly cross-platform. As someone running EndeavourOS, you get the same development experience as developers on Windows or macOS. No compromises, no “it works better on Windows” disclaimers.

Why This Guide Uses JetBrains Rider

While you can develop Avalonia apps with any text editor, JetBrains Rider provides excellent XAML IntelliSense, debugging tools, and preview features that make learning much easier. Plus, it runs natively on Linux and integrates beautifully with the .NET ecosystem.

Setting Up Your Development Environment on EndeavourOS (Arch base)

Before we dive into XAML, let’s get your environment ready. Here’s what you’ll need:

Installing the .NET SDK

# Update your system
sudo pacman -Syu

# Install .NET SDK
sudo pacman -S dotnet-sdk

# Verify installation
dotnet --version

Creating Your First Avalonia Project

# Install Avalonia templates
dotnet new install Avalonia.ProjectTemplates

# Create a new MVVM project with Community Toolkit
dotnet new avalonia.mvvm -o MyFirstAvaloniaApp -f net9.0

# Navigate to your project
cd MyFirstAvaloniaApp

The MVVM template automatically includes the Community Toolkit, which we’ll use extensively for data binding.

XAML Basics: Your First Look Under the Hood

XAML in Avalonia is essentially XML that describes your user interface. Think of it as HTML for desktop applications, but with much more power and flexibility.

Understanding the Structure

Every Avalonia window starts with something like this:

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        x:Class="MyFirstAvaloniaApp.Views.MainWindow"
        Title="My First Avalonia App">
    
    <!-- Your UI content goes here -->
    
</Window>

Let’s break this down:

  • xmlns declarations tell XAML where to find control definitions
  • x:Class connects this XAML to your C# code-behind file
  • Title sets the window title (just like HTML’s <title> tag)

Your First Control

Replace the window content with:

<StackPanel Margin="20">
    <TextBlock Text="Hello, EndeavourOS!" 
               FontSize="24" 
               HorizontalAlignment="Center"/>
    <Button Content="Click Me!" 
            Margin="0,10,0,0"
            Click="OnButtonClick"/>
</StackPanel>

Mastering Avalonia Layouts: The Foundation of Great UIs

Layout controls in Avalonia are like invisible containers that organize your UI elements. Understanding them is crucial for creating responsive, professional-looking applications.

StackPanel: Your Go-To for Simple Layouts

StackPanel arranges children in a single row or column:

<StackPanel Orientation="Vertical" Spacing="10">
    <TextBlock Text="Item 1"/>
    <TextBlock Text="Item 2"/>
    <TextBlock Text="Item 3"/>
</StackPanel>

Pro tip: Use Spacing instead of margins on individual items – it’s cleaner and more maintainable.

Grid: The Swiss Army Knife of Layouts

Grid is incredibly powerful for complex layouts:

<Grid RowDefinitions="Auto,*,Auto" ColumnDefinitions="200,*">
    <!-- Header spanning both columns -->
    <TextBlock Grid.Row="0" Grid.ColumnSpan="2" 
               Text="Header" 
               Background="LightBlue"/>
    
    <!-- Sidebar -->
    <StackPanel Grid.Row="1" Grid.Column="0" 
                Background="LightGray">
        <Button Content="Menu Item 1"/>
        <Button Content="Menu Item 2"/>
    </StackPanel>
    
    <!-- Main content area -->
    <ScrollViewer Grid.Row="1" Grid.Column="1">
        <TextBlock Text="Main content goes here..."
                   TextWrapping="Wrap"/>
    </ScrollViewer>
    
    <!-- Footer -->
    <StackPanel Grid.Row="2" Grid.ColumnSpan="2"    Background="DeepSkyBlue">
        <TextBlock Text="Ready" Padding="3"/>
    </StackPanel>
</Grid>

Grid definitions explained:

  • Auto: Size to content
  • *: Take remaining space
  • 200: Fixed pixel width
  • 2*: Take twice as much space as *

DockPanel: Perfect for Traditional App Layouts

<DockPanel>
    <Menu DockPanel.Dock="Top">
        <MenuItem Header="File"/>
        <MenuItem Header="Edit"/>
    </Menu>
    
    <StatusBar DockPanel.Dock="Bottom">
        <TextBlock Text="Status: Ready"/>
    </StatusBar>
    
    <StackPanel DockPanel.Dock="Left" Width="200">
        <!-- Sidebar content -->
    </StackPanel>
    
    <!-- This fills remaining space -->
    <ContentPresenter Content="{Binding CurrentView}"/>
</DockPanel>

Essential Avalonia Controls Every Beginner Should Know

Input Controls That Actually Work Well on Linux

<StackPanel Spacing="10">
    <!-- Text input -->
    <TextBox Watermark="Enter your name..."
             Text="{Binding UserName}"/>
    
    <!-- Password input -->
    <TextBox PasswordChar="*" 
             Watermark="Password"
             Text="{Binding Password}"/>
    
    <!-- Multiline text -->
    <TextBox AcceptsReturn="True"
             Height="100"
             TextWrapping="Wrap"
             Text="{Binding Comments}"/>
    
    <!-- Numeric input -->
    <NumericUpDown Value="{Binding Age}"
                   Minimum="0"
                   Maximum="120"/>
    
    <!-- Date selection -->
    <DatePicker SelectedDate="{Binding BirthDate}"/>
    
    <!-- Dropdown selection -->
    <ComboBox ItemsSource="{Binding Countries}"
              SelectedItem="{Binding SelectedCountry}"/>
    
    <!-- Checkboxes -->
    <CheckBox Content="Subscribe to newsletter"
              IsChecked="{Binding WantsNewsletter}"/>
    
    <!-- Radio buttons -->
    <StackPanel>
        <RadioButton Content="Option A" GroupName="Options"/>
        <RadioButton Content="Option B" GroupName="Options"/>
    </StackPanel>
</StackPanel>

Display Controls for Rich Content

<StackPanel Spacing="15">
    <!-- Rich text display -->
    <TextBlock TextWrapping="Wrap">
        <Run Text="Welcome to "/>
        <Run Text="Avalonia" FontWeight="Bold" Foreground="Blue"/>
        <Run Text=" development!"/>
    </TextBlock>
    
    <!-- Images -->
    <Image Source="/Assets/logo.png" 
           Width="100" 
           Height="100"/>
    
    <!-- Progress indication -->
    <ProgressBar Value="{Binding DownloadProgress}"
                 Maximum="100"
                 ShowProgressText="True"/>
    
    <!-- Lists -->
    <ListBox ItemsSource="{Binding Items}"
             SelectedItem="{Binding SelectedItem}">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding Name}"/>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</StackPanel>

Data Binding with Community Toolkit MVVM: Connect Your UI to Your Code

Data binding is where Avalonia really shines. Instead of manually updating UI elements in code, you bind them to properties in your ViewModel, and they update automatically.

Create a ViewModel:

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

namespace MyFirstAvaloniaApp.ViewModels;

public partial class MainViewModel : ObservableObject
{
    [ObservableProperty]
    private string _userName = string.Empty;
    
    [ObservableProperty] 
    private string _greeting = "Hello!";
    
    [ObservableProperty]
    private int _clickCount = 0;
    
    [RelayCommand]
    private void UpdateGreeting()
    {
        if (!string.IsNullOrWhiteSpace(UserName))
        {
            Greeting = $"Hello, {UserName}!";
            ClickCount++;
        }
    }
    
    [RelayCommand]
    private void Reset()
    {
        UserName = string.Empty;
        Greeting = "Hello!";
        ClickCount = 0;
    }
}

Connecting Your ViewModel to the View

In your MainWindow.axaml.cs:

using MyFirstAvaloniaApp.ViewModels;

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        DataContext = new MainViewModel();
    }
}

The Magic of Two-Way Data Binding

Now in your XAML:

<StackPanel Margin="20" Spacing="15">
    <TextBox Watermark="Enter your name"
             Text="{Binding UserName}"/>
    
    <TextBlock Text="{Binding Greeting}"
               FontSize="18"
               HorizontalAlignment="Center"/>
    
    <Button Content="Update Greeting"
            Command="{Binding UpdateGreetingCommand}"/>
    
    <TextBlock Text="{Binding ClickCount, StringFormat='Button clicked {0} times'}"
               HorizontalAlignment="Center"/>
    
    <Button Content="Reset"
            Command="{Binding ResetCommand}"/>
</StackPanel>

What’s happening here:

  • {Binding UserName} creates a two-way connection between the TextBox and your ViewModel property
  • When you type, the ViewModel property updates automatically
  • When the property changes in code, the UI updates automatically
  • Commands handle button clicks without any code-behind

Advanced Binding Scenarios That Solve Real Problems

Formatting Data for Display

<!-- Format numbers -->
<TextBlock Text="{Binding Price, StringFormat=C}"/>

<!-- Format dates -->
<TextBlock Text="{Binding LastUpdated, StringFormat='Last updated: {0:yyyy-MM-dd}'}"/>

<!-- Conditional formatting -->
<TextBlock Text="{Binding Status}"
           Foreground="{Binding IsOnline, 
                       Converter={StaticResource BoolToColorConverter}}"/>

Binding to Collections

<ListBox ItemsSource="{Binding Users}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal" Spacing="10">
                <Image Source="{Binding AvatarUrl}" 
                       Width="32" Height="32"/>
                <StackPanel>
                    <TextBlock Text="{Binding Name}" 
                               FontWeight="Bold"/>
                    <TextBlock Text="{Binding Email}" 
                               Foreground="Gray"/>
                </StackPanel>
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Enabling/Disabling Controls Based on State

<StackPanel>
    <TextBox Text="{Binding SearchText}"/>
    
    <Button Content="Search"
            Command="{Binding SearchCommand}"
            IsEnabled="{Binding !IsSearching}"/>
    
    <ProgressBar IsVisible="{Binding IsSearching}"
                 IsIndeterminate="True"/>
</StackPanel>

Styling Your App: Making It Look Professional

Avalonia’s styling system is similar to CSS but more powerful:

<Window.Styles>
    <Style Selector="Button">
        <Setter Property="Margin" Value="5"/>
        <Setter Property="Padding" Value="10,5"/>
        <Setter Property="CornerRadius" Value="4"/>
    </Style>
    
    <Style Selector="Button:pointerover /template/ ContentPresenter">
        <Setter Property="Background" Value="LightBlue"/>
    </Style>
    
    <Style Selector="TextBlock.header">
        <Setter Property="FontSize" Value="18"/>
        <Setter Property="FontWeight" Value="Bold"/>
        <Setter Property="Margin" Value="0,0,0,10"/>
    </Style>
</Window.Styles>

Use the styles:

<StackPanel>
    <TextBlock Classes="header" Text="Welcome"/>
    <Button Content="Styled Button"/>
</StackPanel>

Debugging XAML Issues in JetBrains Rider

Rider provides excellent tools for debugging XAML problems:

Using the XAML Preview

  1. Right-click on your .axaml file
  2. Select “Show in XAML Preview”
  3. See your changes in real-time as you edit

Debugging Data Binding Issues

  1. Set breakpoints in your ViewModel properties
  2. Use the debugger to see if binding is working
  3. Check the output window for binding errors

Common XAML Mistakes and How to Fix Them

Problem: UI doesn’t update when property changes Solution: Make sure your ViewModel inherits from ObservableObject and properties use [ObservableProperty]

Problem: Button command doesn’t work Solution: Check that you’re using [RelayCommand] and binding to CommandName + "Command" (e.g., UpdateGreetingCommand)

Problem: Layout looks wrong on different screen sizes Solution: Use relative sizing (*) instead of fixed pixel values in Grid definitions

Building and Running Your App on EndeavourOS

# Build your app
dotnet build

# Run in development
dotnet run

# Publish for Linux
dotnet publish -r linux-x64 --self-contained

# The output will be in bin/Debug/net8.0/linux-x64/publish/

thank you, see other Avalonia posts available

Mohammed Chami
Mohammed Chami
Articles: 44

Leave a Reply

Your email address will not be published. Required fields are marked *