Memory, leaks, and scale
| Scale | List size | Guarantee |
|---|---|---|
| Small | 200 | Cheap notify; per-item Add is acceptable |
| Mid | 5,000 | AddRange → one Reset |
| Large | 50,000 | Same batching; UI must virtualize |
App data is not framework overhead. A 50,000-row product list occupies whatever the Product objects occupy. The framework is responsible for PropertyChanged allocations, CollectionChanged fan-out, whether popped ViewModels are collectable, and whether messengers and commands pin graphs.
- ViewModel never holds Page. Lifecycle is a host behavior that unsubscribes on Unloaded.
- MessageHub default subscribe is weak. The handler must use the recipient argument.
- Commands are instance fields of the ViewModel and die with it. CanExecuteChanged is a weak event so a popped Button does not stay pinned.
- Dispose cancels in-flight AsyncModelCommand work.
- SetProperty equality-exits without allocating a new PropertyChangedEventArgs. PropertyChanged hops to IMainThread when a dispatcher is present.
Host-process measurements (2026-08-31, .NET 10 on macOS, not device RSS): unchanged SetProperty ≈ 5 ns; changed SetProperty ≈ 19 ns with cached EventArgs; AddRange of 50,000 items raises one CollectionChanged and about 525 KB for the int list backing store, not 50,000 EventArgs. Device RSS budgets are not measured yet.
dotnet test tests/Plugin.Maui.MVVMExpress.Core.Tests
dotnet run --project benchmarks/Plugin.Maui.MVVMExpress.Benchmarks -c Release -- --quickThreading, AOT, and security
- ObservableModel.SetProperty is not thread-safe. Property, command, and navigation notifications hop to IMainThread when MarshalNotifications is true (default).
- Do not ConfigureAwait(false) then new Page() or Shell.GoToAsync. Navigators hop first; NavigationThread.EnsurePageFactoryOnMainThread throws if they do not.
- Bind Button.Command to AsyncModelCommand only on 0.6.0-preview+. 0.5.0 raised CanExecuteChanged off the UI thread after ConfigureAwait(false).
- ICommand.Execute (async void) never throws. Failures go to IErrorSink / IDialogs. ExecuteAsync still rethrows.
- AsyncState updates are compare-exchange; PropertyChanged is marshalled when a dispatcher is present.
- INavigator is serialized per window. Concurrent navigations queue or fail as Outcome.
- Library pipeline code uses ConfigureAwait(false) except where the next step must run on the UI context.
- Typed NavigateToAsync<TViewModel>() is the AOT path. UseMvvmExpress applies generated [Route] / [RequiresAuth] via a ModuleInitializer. Convention scan of *Page / *ViewModel is opt-in and annotated.
- Call InitializeComponent() on App before resolving AppShell / pages (App(IServiceProvider) + CreateWindow).
- ViewModels do not store secrets. Tokens belong in Plugin.Maui.SecureSession / SecureStoragePlus.
- Authorization attributes only consult IAuthState supplied by the app.