Skip to content
Nuvyntra Labs

Documentation

Memory and scale

Leak rules, Small / Mid / Large list guarantees, and host-process measurements.

Memory, leaks, and scale

ScaleList sizeGuarantee
Small200Cheap notify; per-item Add is acceptable
Mid5,000AddRange → one Reset
Large50,000Same 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 -- --quick

Threading, 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.

Discussion

Comment on Plugin.Maui.MVVMExpress. The thread lives on this component's GitHub repository (nuvyntralabs/Plugin.Maui.MVVMExpress). Sign in with GitHub — Giscus uses Discussions, Utterances uses Issues.