| name | avalonia-mvvm |
| description | Use when implementing the MVVM pattern in Avalonia, including ViewModels, INotifyPropertyChanged, ICommand, ReactiveUI, CommunityToolkit.Mvvm, or ViewModel-first navigation. |
Avalonia MVVM
Overview
MVVM: Model (data/business), View (.axaml), ViewModel (presentation logic, bindable state). ViewModel knows nothing about View. Views bind to ViewModel properties and commands.
INotifyPropertyChanged (Manual)
public class PersonViewModel : INotifyPropertyChanged
{
private string _name = "";
public string Name
{
get => _name;
set { _name = value; PropertyChanged?.Invoke(this, new(nameof(Name))); }
}
public event PropertyChangedEventHandler? PropertyChanged;
}
CommunityToolkit.Mvvm (Recommended)
Source-generator based, no reflection, clean code.
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
public partial class MainViewModel : ObservableObject
{
[ObservableProperty]
private string _name = "";
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
private bool _isDirty;
[RelayCommand(CanExecute = nameof(CanSave))]
private async Task SaveAsync()
{
}
private bool CanSave() => IsDirty && !string.IsNullOrEmpty(Name);
}
NuGet: CommunityToolkit.Mvvm
Generated: Name property with change notification, NameChanged partial method hook, SaveCommand as AsyncRelayCommand.
ReactiveUI (Alternative)
using ReactiveUI;
public class MainViewModel : ReactiveObject
{
private string _name = "";
public string Name
{
get => _name;
set => this.RaiseAndSetIfChanged(ref _name, value);
}
public ReactiveCommand<Unit, Unit> SaveCommand { get; }
public MainViewModel()
{
var canSave = this.WhenAnyValue(x => x.Name, n => !string.IsNullOrEmpty(n));
SaveCommand = ReactiveCommand.CreateFromTask(SaveAsync, canSave);
}
private async Task SaveAsync() { }
}
NuGet: Avalonia.ReactiveUI
App setup: .UseReactiveUI() in Program.cs
ICommand (Manual)
public class RelayCommand : ICommand
{
private readonly Action _execute;
private readonly Func<bool>? _canExecute;
public RelayCommand(Action execute, Func<bool>? canExecute = null)
{ _execute = execute; _canExecute = canExecute; }
public event EventHandler? CanExecuteChanged;
public bool CanExecute(object? p) => _canExecute?.Invoke() ?? true;
public void Execute(object? p) => _execute();
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
ViewLocator Pattern
Auto-maps ViewModels → Views by naming convention (FooViewModel → FooView):
public class ViewLocator : IDataTemplate
{
public Control? Build(object? data)
{
if (data is null) return null;
var name = data.GetType().FullName!.Replace("ViewModel", "View");
var type = Type.GetType(name);
return type != null
? (Control)Activator.CreateInstance(type)!
: new TextBlock { Text = $"Not Found: {name}" };
}
public bool Match(object? data) => data is ViewModelBase;
}
Register in App.axaml:
<Application.DataTemplates>
<local:ViewLocator/>
</Application.DataTemplates>
ViewModel-First Navigation
Pattern: Router/NavigationViewModel holds current page ViewModel, ContentControl displays it:
<ContentControl Content="{Binding CurrentPage}"/>
public partial class MainViewModel : ObservableObject
{
[ObservableProperty]
private ViewModelBase _currentPage = new HomeViewModel();
public void NavigateTo(ViewModelBase page) => CurrentPage = page;
}
Design-Time DataContext
<Window xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
d:DataContext="{d:DesignInstance vm:MainViewModel, IsDesignTimeCreatable=True}">
ObservableCollection for Lists
public ObservableCollection<PersonViewModel> People { get; } = new();
Use ObservableCollection<T> (not List<T>) so UI updates on add/remove.
Community Libraries
| Library | NuGet / Repo | Purpose |
|---|
| CommunityToolkit.Mvvm | CommunityToolkit.Mvvm | Source-generator MVVM (ObservableObject, RelayCommand) — recommended |
| ReactiveUI | Avalonia.ReactiveUI | Reactive MVVM with Rx.NET observables |
| Prism | Prism.Avalonia | Full MVVM framework with regions, modules, DI, dialogs |
| Epoxy | Epoxy.Avalonia | Lightweight multi-platform MVVM |
| FuncUI | Avalonia.FuncUI | F# MVU pattern with view DSL |
| ReactiveElmish.Avalonia | GitHub: JordanMarr/ReactiveElmish.Avalonia | F# MVU with XAML views |
| Stylet.Avalonia | Stylet.Avalonia | Lightweight ViewModel-first MVVM (Caliburn.Micro-inspired) |
| HanumanInstitute.MvvmDialogs | HanumanInstitute.MvvmDialogs.Avalonia | Open dialogs from ViewModel in pure MVVM |
| ReactiveValidation | ReactiveValidation | Fluent-style property validation with UI messages |
| Markup Declarative | Avalonia.Markup.Declarative | Declarative C# UI and MVU helpers |
| Lemon.ModuleNavigation | GitHub: NeverMorewd/Lemon.ModuleNavigation | .NET Generic Host + module navigation |
| AsyncNavigation | GitHub: NeverMorewd/AsyncNavigation | Async navigation via Microsoft.Extensions.DI |
Common Mistakes
- Using
List<T> instead of ObservableCollection<T> — list changes not reflected in UI
- Not calling
PropertyChanged with exact property name — use nameof()
- Doing UI work in ViewModel (creating controls, accessing
DataContext)
- Async command without
AsyncRelayCommand — exceptions swallowed, UI blocks
- Forgetting
partial keyword on CommunityToolkit ViewModel class — source gen fails silently