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. Phase 10 ships ILLink roots and contract tests. Hardware RSS and a production post-mortem stay documented as manual reports — they are not catalog CI.
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] / [RegisterView] / [RequiresAuth] via a ModuleInitializer. Convention scan of *Page / *ViewModel is not a supported 1.3 path — MVVME010 is a DEBUG error. Handwritten Map remains an escape hatch.
- 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.