SuperAPI AI Directory

avalonia-mvvm

Coding & development

View repository

Updated 11-03 16:15

About this skill

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()
    {
        // save logic
    }

    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() { /* ... */ }
}
Avalonia 12.x (12.0+) Avalonia 11.x (11.x)
NuGet ReactiveUI.Avalonia (12.1.x on NuGet requires Avalonia >= 12.1.3; later 14.x line exists — check NuGet) Avalonia.ReactiveUI (ends at 11.3.x)
Namespace using ReactiveUI.Avalonia; using Avalonia.ReactiveUI;

App setup (12.0+):

using ReactiveUI.Avalonia;

AppBuilder.Configure<App>()
    .UsePlatformDetect()
    .UseReactiveUI(rxui => { /* optional rxui.WithRegistration(...) */ })
    .RegisterReactiveUIViewsFromEntryAssembly();

(11.x) .UseReactiveUI() with no arguments. DI variants in 12: UseReactiveUIWithMicrosoftDependencyResolver, ...WithAutofac, ...WithDryIoc, ...WithNinject (separate ReactiveUI.Avalonia.* packages, namespace ReactiveUI.Avalonia.Splat). Base classes: ReactiveWindow<TVM>, ReactiveUserControl<TVM>; routing via <rxui:RoutedViewHost Router="{Binding Router}"/>.

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 ReactiveUI.Avalonia (12.0+) / Avalonia.ReactiveUI (11.x) 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)
  • Creating DispatcherTimer in a ViewModel off the UI thread in 12 — it uses the current thread's dispatcher, not always the UI thread
  • Async command without AsyncRelayCommand — exceptions swallowed, UI blocks
  • Using Avalonia.ReactiveUI in an Avalonia 12 project — package stops at 11.3.x; switch to ReactiveUI.Avalonia and using ReactiveUI.Avalonia;
  • Forgetting partial keyword on CommunityToolkit ViewModel class — source gen fails silently