Mohammed Chami
.NET Developer | Content Creator
Mohammed Chami
.NET Developer | Content Creator

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.
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.
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.
Before we dive into XAML, let’s get your environment ready. Here’s what you’ll need:
# Update your system
sudo pacman -Syu
# Install .NET SDK
sudo pacman -S dotnet-sdk
# Verify installation
dotnet --version
# 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 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.
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 definitionsx:Class connects this XAML to your C# code-behind fileTitle sets the window title (just like HTML’s <title> tag)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>
Layout controls in Avalonia are like invisible containers that organize your UI elements. Understanding them is crucial for creating responsive, professional-looking applications.
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 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 space200: Fixed pixel width2*: Take twice as much space as *<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>
<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>
<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 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;
}
}
In your MainWindow.axaml.cs:
using MyFirstAvaloniaApp.ViewModels;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel();
}
}
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<!-- 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}}"/>
<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>
<StackPanel>
<TextBox Text="{Binding SearchText}"/>
<Button Content="Search"
Command="{Binding SearchCommand}"
IsEnabled="{Binding !IsSearching}"/>
<ProgressBar IsVisible="{Binding IsSearching}"
IsIndeterminate="True"/>
</StackPanel>
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>
Rider provides excellent tools for debugging XAML problems:
.axaml fileProblem: 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
# 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