Avalonia is designed for the Model-View-ViewModel (MVVM) pattern, providing excellent support for separation of concerns, testability, and maintainability in your applications.
What is MVVM?
MVVM is an architectural pattern that separates application logic from UI:
┌──────────────────────────────────────────┐
│ View │
│ (XAML + Code-behind) │
│ - User Interface │
│ - No business logic │
└──────────────────────────────────────────┘
│
│ Data Binding
│
┌──────────────────────────────────────────┐
│ ViewModel │
│ - Presentation logic │
│ - Commands │
│ - Observable properties │
│ - No UI references │
└──────────────────────────────────────────┘
│
│ Calls
│
┌──────────────────────────────────────────┐
│ Model │
│ - Business logic │
│ - Data access │
│ - Domain entities │
└──────────────────────────────────────────┘
Benefits of MVVM
Separation of Concerns UI, presentation logic, and business logic are clearly separated.
Testability ViewModels can be unit tested without UI dependencies.
Designer-Developer Workflow Designers work on XAML while developers work on logic.
Reusability ViewModels can be reused across different views or platforms.
ReactiveUI Integration
Avalonia templates include ReactiveUI, a powerful MVVM framework built on reactive extensions.
Installation
dotnet add package ReactiveUI
dotnet add package ReactiveUI.Fody # Optional: property change weaving
ViewModelBase
All ViewModels inherit from ViewModelBase:
ViewModelBase.cs (Template)
using ReactiveUI ;
public class ViewModelBase : ReactiveObject
{
// Base class for all ViewModels
// Implements INotifyPropertyChanged via ReactiveObject
}
Creating ViewModels
Basic ViewModel
MainWindowViewModel.cs
MainWindow.axaml
MainWindow.axaml.cs
using ReactiveUI ;
using System . Collections . ObjectModel ;
public class MainWindowViewModel : ViewModelBase
{
private string _greeting = "Welcome to Avalonia!" ;
private string _userName = string . Empty ;
private bool _isLoading ;
// Reactive property with automatic change notification
public string Greeting
{
get => _greeting ;
set => this . RaiseAndSetIfChanged ( ref _greeting , value );
}
public string UserName
{
get => _userName ;
set => this . RaiseAndSetIfChanged ( ref _userName , value );
}
public bool IsLoading
{
get => _isLoading ;
set => this . RaiseAndSetIfChanged ( ref _isLoading , value );
}
// Observable collection for list bindings
public ObservableCollection < TodoItem > TodoItems { get ; }
public MainWindowViewModel ()
{
TodoItems = new ObservableCollection < TodoItem >();
}
}
Using ReactiveUI Fody
Simplify property declarations with Fody weaving:
using ReactiveUI . Fody . Helpers ;
public class MainWindowViewModel : ViewModelBase
{
// Fody automatically generates property change notifications
[ Reactive ]
public string Greeting { get ; set ; } = "Welcome!" ;
[ Reactive ]
public string UserName { get ; set ; } = string . Empty ;
[ Reactive ]
public bool IsLoading { get ; set ; }
}
ReactiveUI.Fody reduces boilerplate by auto-implementing INotifyPropertyChanged at compile time.
Commands
Commands encapsulate actions triggered by the UI:
ReactiveCommand
ViewModel with Commands
Binding Commands
using ReactiveUI ;
using System ;
using System . Reactive ;
using System . Reactive . Linq ;
using System . Threading . Tasks ;
public class MainWindowViewModel : ViewModelBase
{
private string _userName = string . Empty ;
public string UserName
{
get => _userName ;
set => this . RaiseAndSetIfChanged ( ref _userName , value );
}
// Simple command
public ReactiveCommand < Unit , Unit > GreetCommand { get ; }
// Command with parameter
public ReactiveCommand < string , Unit > SaveCommand { get ; }
// Async command
public ReactiveCommand < Unit , string > LoadDataCommand { get ; }
public MainWindowViewModel ()
{
// Can execute when UserName is not empty
var canExecute = this . WhenAnyValue (
x => x . UserName ,
userName => ! string . IsNullOrWhiteSpace ( userName ));
GreetCommand = ReactiveCommand . Create (
Greet ,
canExecute );
SaveCommand = ReactiveCommand . Create < string >(
name => Save ( name ));
LoadDataCommand = ReactiveCommand . CreateFromTask (
LoadDataAsync );
}
private void Greet ()
{
// Command logic
Console . WriteLine ( $"Hello, { UserName } !" );
}
private void Save ( string fileName )
{
// Save logic
Console . WriteLine ( $"Saving to { fileName } " );
}
private async Task < string > LoadDataAsync ()
{
// Async operation
await Task . Delay ( 1000 );
return "Data loaded" ;
}
}
Command Can Execute
var canDelete = this . WhenAnyValue (
x => x . SelectedItem ,
selectedItem => selectedItem != null );
DeleteCommand = ReactiveCommand . Create (
() => Delete ( SelectedItem ),
canDelete );
Buttons automatically disable when CanExecute returns false.
Reactive Properties
WhenAnyValue
Observe property changes reactively:
public MainWindowViewModel ()
{
// Observe single property
this . WhenAnyValue ( x => x . UserName )
. Subscribe ( name =>
Console . WriteLine ( $"Name changed to: { name } " ));
// Observe multiple properties
this . WhenAnyValue (
x => x . FirstName ,
x => x . LastName ,
( first , last ) => $" { first } { last } " )
. Subscribe ( fullName =>
FullName = fullName );
// With filtering
this . WhenAnyValue ( x => x . SearchQuery )
. Throttle ( TimeSpan . FromMilliseconds ( 300 ))
. Where ( query => ! string . IsNullOrWhiteSpace ( query ))
. Subscribe ( query =>
PerformSearch ( query ));
}
Derived Properties
private string _firstName = string . Empty ;
private string _lastName = string . Empty ;
private readonly ObservableAsPropertyHelper < string > _fullName ;
public string FirstName
{
get => _firstName ;
set => this . RaiseAndSetIfChanged ( ref _firstName , value );
}
public string LastName
{
get => _lastName ;
set => this . RaiseAndSetIfChanged ( ref _lastName , value );
}
// Read-only derived property
public string FullName => _fullName . Value ;
public MainWindowViewModel ()
{
_fullName = this . WhenAnyValue (
x => x . FirstName ,
x => x . LastName ,
( first , last ) => $" { first } { last } " )
. ToProperty ( this , x => x . FullName );
}
Interaction Patterns
Dialogs and Interactions
using ReactiveUI ;
public class MainWindowViewModel : ViewModelBase
{
public Interaction < Unit , bool > ConfirmDelete { get ; }
public ReactiveCommand < Unit , Unit > DeleteCommand { get ; }
public MainWindowViewModel ()
{
ConfirmDelete = new Interaction < Unit , bool >();
DeleteCommand = ReactiveCommand . CreateFromTask ( async () =>
{
var confirmed = await ConfirmDelete . Handle ( Unit . Default );
if ( confirmed )
{
// Perform deletion
}
});
}
}
public partial class MainWindow : Window
{
public MainWindow ()
{
InitializeComponent ();
var viewModel = new MainWindowViewModel ();
DataContext = viewModel ;
// Handle interaction in view
viewModel . ConfirmDelete . RegisterHandler ( async interaction =>
{
var result = await ShowConfirmationDialog ();
interaction . SetOutput ( result );
});
}
private async Task < bool > ShowConfirmationDialog ()
{
// Show dialog and return result
return true ; // or false
}
}
Navigation
View Model Navigation
public class MainWindowViewModel : ViewModelBase
{
private ViewModelBase _currentPage ;
public ViewModelBase CurrentPage
{
get => _currentPage ;
set => this . RaiseAndSetIfChanged ( ref _currentPage , value );
}
public ReactiveCommand < Unit , Unit > ShowHomeCommand { get ; }
public ReactiveCommand < Unit , Unit > ShowSettingsCommand { get ; }
public MainWindowViewModel ()
{
_currentPage = new HomeViewModel ();
ShowHomeCommand = ReactiveCommand . Create (
() => CurrentPage = new HomeViewModel ());
ShowSettingsCommand = ReactiveCommand . Create (
() => CurrentPage = new SettingsViewModel ());
}
}
< Window x:DataType = "vm:MainWindowViewModel" >
< DockPanel >
<!-- Navigation -->
< StackPanel DockPanel.Dock = "Top" Orientation = "Horizontal" >
< Button Content = "Home" Command = "{Binding ShowHomeCommand}" />
< Button Content = "Settings" Command = "{Binding ShowSettingsCommand}" />
</ StackPanel >
<!-- Content area -->
< ContentControl Content = "{Binding CurrentPage}" />
</ DockPanel >
</ Window >
Dependency Injection
Combine MVVM with DI for better testability:
using Microsoft . Extensions . DependencyInjection ;
public static class ServiceConfiguration
{
public static void ConfigureServices ( IServiceCollection services )
{
// Register services
services . AddSingleton < IDataService , DataService >();
// Register ViewModels
services . AddTransient < MainWindowViewModel >();
services . AddTransient < SettingsViewModel >();
}
}
public class MainWindowViewModel : ViewModelBase
{
private readonly IDataService _dataService ;
public MainWindowViewModel ( IDataService dataService )
{
_dataService = dataService ;
}
}
Best Practices
Keep Views thin
Minimize code-behind. Use it only for view-specific logic (animations, focus management).
ViewModels should be UI-agnostic
Never reference Avalonia types in ViewModels. Keep them testable.
Use ReactiveCommand for actions
Encapsulate all user actions in commands, not event handlers.
Leverage reactive extensions
Use WhenAnyValue and reactive operators for derived properties and validation.
Handle async properly
Use ReactiveCommand.CreateFromTask for async operations with automatic error handling.