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

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.
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:
The best part? You don’t need years of experience to start building impressive applications.
Let’s make sure you have everything ready. On your EndeavourOS machine, you’ll need:
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.
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.
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:
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.
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.
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 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.
Time for the exciting part! In Rider, you can run your app in several ways:
dotnet runWithin 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!
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 (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.
Avalonia comes with all the controls you’d expect:
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.
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:
Counter propertyINotifyPropertyChanged implementationAfter 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.
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
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;
Resist the temptation to put business logic in your MainWindow.axaml.cs file. Keep it in the ViewModel – your future self will thank you.
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.