Your First Cross-Platform Desktop App: Getting Started with Avalonia UI on Linux

Ready to build desktop apps that run everywhere? Let’s dive into Avalonia UI – the framework that lets you write once and deploy to Windows, macOS, and Linux.

Building desktop applications used to mean choosing between platforms. Want to reach Windows users? Learn WPF. macOS? SwiftUI. Linux? Good luck finding something modern. But what if I told you there’s a way to write one app that runs beautifully on all three platforms?

Enter Avalonia UI – a cross-platform .NET framework that’s been quietly revolutionizing desktop development. In this guide, we’ll get you up and running with your first Avalonia application using JetBrains Rider on EndeavourOS (Arch Linux). Don’t worry if you’re new to programming – I’ll walk you through every step.

Why Avalonia UI Will Change How You Think About Desktop Development

Before we jump into code, let’s talk about why Avalonia UI is worth your time. Unlike other cross-platform solutions that feel like web apps masquerading as desktop software, Avalonia creates truly native-feeling applications.

What makes Avalonia special:

  • True cross-platform: Write once, run on Windows, macOS, Linux, iOS, Android, and even in web browsers
  • XAML-based: If you know WPF or UWP, you’ll feel right at home
  • Performance: Native rendering means your apps feel fast and responsive
  • Modern .NET: Built on .NET 6+ with all the latest language features
  • Active community: Growing ecosystem with excellent documentation

The best part? You don’t need years of experience to start building impressive applications.

Setting Up Your Linux Development Environment (The Right Way)

What You’ll Need Before We Start

Let’s make sure you have everything ready. On your EndeavourOS machine, you’ll need:

  • .NET 9 SDK (the latest version as of this writing)
  • JetBrains Rider (the best IDE for .NET development on Linux)
  • Git (for version control)

Installing .NET 9 SDK on EndeavourOS

EndeavourOS makes this surprisingly easy. Open your terminal and run:

sudo pacman -S dotnet-sdk

That’s it! The Arch repositories keep the .NET SDK up to date. To verify your installation:

dotnet --version

You should see something like 9.0.xxx. If you see version 6 or 7, that’s okay too – Avalonia works with .NET 6 and newer.

Getting JetBrains Rider Ready

If you haven’t installed Rider yet, you can download it from the JetBrains website or use the AUR:

yay -S jetbrains-rider

Rider comes with excellent Avalonia support out of the box, including XAML IntelliSense, live preview, and debugging tools that make development a breeze.

Creating Your First Avalonia Project (It’s Easier Than You Think)

The Template That Does the Heavy Lifting

Avalonia provides project templates that set up everything you need. In your terminal, install the templates:

You can you the GUI using Rider to create a project or using Terminal

dotnet new install Avalonia.ProjectTemplates

Now let’s create your first project:

dotnet new avalonia.mvvm -n MyFirstAvaloniaApp
cd MyFirstAvaloniaApp

The avalonia.mvvm template gives you a solid foundation with:

  • MVVM (Model-View-ViewModel) pattern setup
  • Sample views and view models
  • Proper project structure

Opening Your Project in Rider

Launch Rider and open your project folder. You’ll see a structure that looks like this:

MyFirstAvaloniaApp/
├── App.axaml
├── App.axaml.cs
├── ViewLocator.cs
├── Program.cs
├── Views/
│   ├── MainWindow.axaml
│   └── MainWindow.axaml.cs
├── ViewModels/
│   ├── MainWindowViewModel.cs
│   └── ViewModelBase.cs
└── MyFirstAvaloniaApp.csproj

Don’t let this intimidate you – each file has a specific, simple purpose that we’ll explore.

Understanding the Avalonia Project Structure (Your New Best Friends)

The App.axaml File: Your Application’s Front Door

Think of App.axaml as your app’s configuration center. It defines global styles, resources, and how your application starts up. Here’s what a basic one looks like:

<Application xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             x:Class="MyFirstAvaloniaApp.App">
    <Application.Styles>
        <FluentTheme />
    </Application.Styles>
</Application>

The FluentTheme gives your app a modern, Microsoft Fluent Design look that adapts to each platform.

MainWindow.axaml: Where the Magic Happens

This is your main window definition. Open it in Rider and you’ll see something like:

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="using:MyFirstAvaloniaApp.ViewModels"
        x:Class="MyFirstAvaloniaApp.Views.MainWindow"
        Title="My First Avalonia App">
    
    <Design.DataContext>
        <vm:MainWindowViewModel/>
    </Design.DataContext>

    <TextBlock Text="{Binding Greeting}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Window>

The ViewModel: Your Data’s Best Friend

The MainWindowViewModel.cs file contains the logic and data for your window. With Community Toolkit MVVM, it looks like this:

using CommunityToolkit.Mvvm.ComponentModel;

namespace MyFirstAvaloniaApp.ViewModels;

public partial class MainWindowViewModel : ObservableObject
{
    public string Greeting => "Welcome to Avalonia!";
}

Notice the partial keyword and inheriting from ObservableObject – this is the Community Toolkit MVVM pattern in action. The toolkit’s source generators will automatically create the necessary boilerplate code for us.

Running Your First Avalonia App (The Moment of Truth)

Time for the exciting part! In Rider, you can run your app in several ways:

  1. Click the green play button in the toolbar
  2. Press Ctrl+F5 for run without debugging
  3. Use the terminal: dotnet run

Within seconds, you should see a window appear with “Welcome to Avalonia!” displayed in the center. Congratulations – you’ve just run your first cross-platform desktop application!

Essential Avalonia Concepts Every Beginner Should Master

Data Binding and Observable Properties: The Secret Sauce of Modern UIs

Data binding is what makes Avalonia (and modern UI frameworks) so powerful. With Community Toolkit MVVM, creating observable properties is incredibly simple using the [ObservableProperty] attribute:

[ObservableProperty]
private string _userName;

The source generator automatically creates a public UserName property with proper change notification. Your XAML can then bind to it:

<TextBox Text="{Binding UserName}" />

This TextBox will always display whatever value is in the UserName property, and when the user types, the property updates automatically.

XAML: Your New UI Language

XAML (eXtensible Application Markup Language) is how you define your user interface. It might look intimidating at first, but it’s actually quite intuitive:

<StackPanel Orientation="Vertical" Spacing="10">
    <TextBlock Text="Enter your name:" />
    <TextBox Text="{Binding UserName}" />
    <Button Content="Say Hello" Command="{Binding SayHelloCommand}" />
</StackPanel>

This creates a vertical stack of controls: a label, a text input, and a button.

Controls: The Building Blocks of Your Interface

Avalonia comes with all the controls you’d expect:

  • TextBlock: Display text
  • TextBox: Text input
  • Button: Clickable buttons
  • StackPanel: Arrange controls in a line
  • Grid: Create complex layouts
  • ListBox: Display lists of items

Building Something Real: A Simple Counter App

Let’s extend the default template to create something more interesting – a counter app. This will teach you about observable properties, commands, and user interaction using the modern Community Toolkit MVVM approach.

Updating the ViewModel

Replace the content of MainWindowViewModel.cs:

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

namespace MyFirstAvaloniaApp.ViewModels;

public partial class MainWindowViewModel : ViewModelBase
{
    [ObservableProperty]
    private int _counter;

    [RelayCommand]
    private void Increment()
    {
        Counter++;
    }

    [RelayCommand(CanExecute = nameof(CanDecrement))]
    private void Decrement()
    {
        Counter--;
    }

    partial void OnCounterChanged(int value)
    {
        DecrementCommand.NotifyCanExecuteChanged();
    }
}

Look how clean this is! The [ObservableProperty] attribute automatically generates:

  • A public Counter property
  • Proper INotifyPropertyChanged implementation
  • Change notifications when the value updates

After we increment or decrement, our CanDecrement method isn’t being re-evaluated automatically when Counter changes, so the button stays disabled once it hits 0, and won’t re-enable unless you tell it to.

In that partial method, we call NotifyCanExecuteChanged() to tell the command system to re-check CanDecrement() instantly when Counter changes.

The [RelayCommand] attributes automatically generate IncrementCommand and DecrementCommand properties that your UI can bind to.


### Updating the View

Now update `MainWindow.axaml`:

```xml
<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="using:MyFirstAvaloniaApp.ViewModels"
        x:Class="MyFirstAvaloniaApp.Views.MainWindow"
        Title="Counter App"
        Width="300" Height="200">
    
    <Design.DataContext>
        <vm:MainWindowViewModel/>
    </Design.DataContext>

    <StackPanel Orientation="Vertical" 
                HorizontalAlignment="Center" 
                VerticalAlignment="Center" 
                Spacing="20">
        
        <TextBlock Text="Simple Counter" 
                   FontSize="24" 
                   HorizontalAlignment="Center"/>
        
        <TextBlock Text="{Binding Counter}" 
                   FontSize="48" 
                   FontWeight="Bold"
                   HorizontalAlignment="Center"/>
        
        <StackPanel Orientation="Horizontal" Spacing="10">
            <Button Content="+" 
                    Command="{Binding IncrementCommand}"
                    Width="50" Height="40"
                    FontSize="20"/>
            <Button Content="-" 
                    Command="{Binding DecrementCommand}"
                    Width="50" Height="40"
                    FontSize="20"/>
        </StackPanel>
    </StackPanel>
</Window>

Run the app again, and you now have a functional counter! Click the buttons and watch the number change in real-time.

Common Beginner Mistakes (And How to Avoid Them)

Forgetting the Partial Keyword

When using Community Toolkit MVVM, your ViewModels must be marked as partial classes. The source generators need this to add their generated code to your class.

// ✅ Correct
public partial class MainWindowViewModel : ViewModelBase

// ❌ Wrong - missing partial
public class MainWindowViewModel : ViewModelBase

Accessing Private Fields Instead of Properties

Remember that [ObservableProperty] generates a public property from your private field. Always use the property (capitalized) in your code:

[ObservableProperty]
private int _counter;

// ✅ Correct - use the generated property
public void Reset() => Counter = 0;

// ❌ Wrong - accessing the private field directly
public void Reset() => _counter = 0;

Putting Logic in Code-Behind

Resist the temptation to put business logic in your MainWindow.axaml.cs file. Keep it in the ViewModel – your future self will thank you.

Not Understanding the Design Context

The <Design.DataContext> in your XAML allows Rider’s designer to show you how your UI will look. It’s incredibly helpful for development but doesn’t affect your running app.

thank you see other posts to learn other Avalonia UI and Community Toolkit MVVM.

Mohammed Chami
Mohammed Chami
Articles: 45

Leave a Reply

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