Skip to main content
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

Package 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

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:
With Fody
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

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

Dynamic CanExecute
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:
Reactive Observations
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

Computed 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

ViewModel 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
            }
        });
    }
}
View Handler
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
    }
}

View Model Navigation

Navigation ViewModel
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());
    }
}
View with Navigation
<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:
With DI Container
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

1

Keep Views thin

Minimize code-behind. Use it only for view-specific logic (animations, focus management).
2

ViewModels should be UI-agnostic

Never reference Avalonia types in ViewModels. Keep them testable.
3

Use ReactiveCommand for actions

Encapsulate all user actions in commands, not event handlers.
4

Leverage reactive extensions

Use WhenAnyValue and reactive operators for derived properties and validation.
5

Handle async properly

Use ReactiveCommand.CreateFromTask for async operations with automatic error handling.

Build docs developers (and LLMs) love