How to Build an Onboarding Flow in Avalonia – Step by Step


Creating a welcoming first‑run experience is a small investment that pays dividends in user satisfaction and retention. An onboarding flow is the sequence of screens that introduce an application and guide a new user through initial choices. This post demonstrates how to create an onboarding flow for an Avalonia desktop application, utilizing the multi-page architecture introduced in the previous article. If you haven’t set up multi‑page navigation and a shared layout yet, check out my earlier post. First, it lays the foundation for switching between pages and maintaining shared state across the app.
Why onboarding matters
From a user’s perspective, the first launch of an app is a critical moment. Good onboarding teaches the core value quickly, collects essential preferences, and makes the user feel in control. Without it, users can feel lost and may abandon the app. Avalonia’s MVVM pattern and XAML UI engine make it straightforward to build structured onboarding flows that look at home on Windows, macOS, and Linux.
1 – Create the Onboarding ViewModel and Steps
In the AvaloniaMultiPageStarter project, we added a new OnboardingViewModel
class. The view model manages the steps and exposes properties that drive the UI. It defines an array of step UserControl
views and keeps track of which one is currently displayed:
// Array of step views
private readonly UserControl[] _steps =
{
new WelcomeStepView(),
new PreferencesStepView(),
new CompletionStepView()
};
// Current step index and view
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CurrentStepView))]
[NotifyPropertyChangedFor(nameof(ShowPreviousButton))]
[NotifyPropertyChangedFor(nameof(PrimaryButtonText))]
[NotifyPropertyChangedFor(nameof(CanGoPrevious))]
[NotifyCanExecuteChangedFor(nameof(PreviousStepCommand))]
private int _currentStepIndex;
[ObservableProperty]
private UserControl _currentStepView;
The CommunityToolkit.Mvvm attributes automatically generate property‑changed notifications. When the _currentStepIndex
changes, CurrentStepView
, ShowPreviousButton
and PrimaryButtonText
update accordingly. The view model initializes CurrentStepView
to the first step in its constructor.
2 – Handle navigation commands
Navigation buttons need logic behind them. The OnboardingViewModel
defines two commands: PreviousStep
and PrimaryAction
. The [RelayCommand]
attribute generates ICommand
properties for us.
The PreviousStep
command checks whether the user can go back and moves to the previous view:
[RelayCommand(CanExecute = nameof(CanGoPrevious))]
private void PreviousStep()
{
if (CurrentStepIndex > 0)
{
CurrentStepIndex--;
CurrentStepView = _steps[CurrentStepIndex];
}
}
The PrimaryAction
command works as a Next/Finish
button. It increments the step index until the last step, then launches the main window:
[RelayCommand]
private void PrimaryAction()
{
if (CurrentStepIndex < _steps.Length - 1)
{
CurrentStepIndex++;
CurrentStepView = _steps[CurrentStepIndex];
}
else
{
Console.WriteLine("Launching main window");
// Mark onboarding complete (save to config/store)
App.LaunchMainWindow();
}
}
The PrimaryButtonText
property returns "Next"
for every step except the last one, where it returns "Finish"
. This makes the UI automatically switch labels without any extra code.
public string PrimaryButtonText => CurrentStepIndex == _steps.Length - 1 ? "Finish" : "Next";
3 – Build step‑specific views
Each step is a simple UserControl
defined in XAML. For example, the welcome step might show a greeting and some descriptive text:
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="AvaloniaMultiPageStarter.Views.Onboarding.Steps.WelcomeStepView">
<StackPanel Spacing="8">
<TextBlock Text="Welcome to FileTidy" FontSize="20" FontWeight="Bold" />
<TextBlock Text="We’ll help you clean and organize your cluttered folders automatically." TextWrapping="Wrap" />
</StackPanel>
</UserControl>
The preferences and completion steps follow the same pattern, giving you a clean separation of concerns and making it easy to add more screens later.
4 – Design the Onboarding window
The onboarding flow is presented in its own window (OnboardingWindow.axaml
). It contains a ContentControl
bound to the current step and a pair of navigation buttons:
<Window … x:Class="AvaloniaMultiPageStarter.Views.Onboarding.OnboardingWindow" x:DataType="vm:OnboardingViewModel"
Width="600" Height="400" CanResize="False" WindowStartupLocation="CenterScreen"
Title="Welcome to the Avalonia Multi Page Starter">
<Border Padding="20">
<StackPanel Spacing="16">
<!-- Dynamic content area -->
<ContentControl Content="{Binding CurrentStepView}" />
<!-- Navigation row -->
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Content="Back"
Command="{Binding PreviousStepCommand}"
IsVisible="{Binding ShowPreviousButton}" />
<Button Content="{Binding PrimaryButtonText}"
Command="{Binding PrimaryActionCommand}" />
</StackPanel>
</StackPanel>
</Border>
</Window>
The IsVisible
binding ensures the Back button disappears on the first step. The primary button’s Content
binding automatically switches between “Next” and “Finish” based on the view model state.
5 – Launch the main window after completion
When the user finishes the last step, we call App.LaunchMainWindow()
. This method creates the MainWindow
and replaces the current window. In the starter project it looks like this:
public static void LaunchMainWindow()
{
if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
var mainWindowViewModel = Services.GetRequiredService<MainWindowViewModel>();
var newMainWindow = new MainWindow { DataContext = mainWindowViewModel };
newMainWindow.Show();
(desktop.MainWindow as Window)?.Close();
desktop.MainWindow = newMainWindow;
}
}
Switching windows instead of just hiding the onboarding window helps free resources and gives the app a clean slate.
Bonus tip – persist onboarding state
In the FileTidy application, the onboarding flow is persisted so users don’t have to see it again. The App.axaml.cs
checks a configuration flag before deciding which window to show:
var config = Services.GetRequiredService<IAppConfigService>();
bool hasCompletedOnboarding = config.GetHasCompletedOnboardingAsync().GetAwaiter().GetResult();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = hasCompletedOnboarding
? new MainWindow { DataContext = Services.GetRequiredService<MainWindowViewModel>(), Title = "FileTidy" }
: new OnboardingWindow { DataContext = Services.GetRequiredService<OnboardingViewModel>() };
}
The IAppConfigService
simply reads and writes a flag in the app’s configuration store. Its GetHasCompletedOnboardingAsync
method reads a string value and parses it to a boolean. When the onboarding is finished, you can call SetHasCompletedOnboardingAsync(true)
to ensure the user goes directly to the main window on subsequent launches.
Persisting the onboarding state is a small addition that makes a big difference in user experience. It’s also a great place to save other onboarding preferences (e.g., theme or analytics opt‑in) so the app behaves appropriately after the first run.
Final Thoughts
Building an onboarding flow in Avalonia is surprisingly straightforward. By splitting each step into its own UserControl
, binding a simple view model, and using CommunityToolkit.Mvvm for commands and property notifications, you can create a guided first‑run experience without tangling your main code base. Don’t forget to persist the completion state so returning users jump straight into the app. With these patterns in place, your Avalonia apps will welcome new users gracefully and keep them engaged.
If you found this helpful, feel free to fork the demo repo, adapt it to your app, and let me know what you build.
Subscribe to my newsletter
Read articles from Freeman Madudili directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Freeman Madudili
Freeman Madudili
Hi, I’m Freeman, a Software Developer specializing in C#, .NET, and Azure. With a passion for creating seamless web and cloud solutions, I’ve built and deployed apps that solve real-world problems. Follow along as I share insights, projects, and tips from my journey in tech!