Testing package
Plugin.Maui.MVVMExpress.Testing is net10.0 and depends on Core. Put ViewModels in a shared net10.0 project so they run without MAUI. FakeNavigator is InMemoryNavigator — assert Current, Stack, and CanGoBack. LeakProbe.Track returns a WeakReference; IsCollected takes that reference, not a Func. Drop the strong reference before the assert (the leak snippet uses the first-screen HomeViewModel). Also: FakeMainThread, FakeConnectivity, FakeMessageHub, AppearAsync / DisappearAsync, and ScopedNavigator for page-scope push/pop GC. Button + popped-page collection is a Core test scenario (weak CanExecuteChanged), not a LeakProbe Button API.
public sealed class ProductListViewModel : PageViewModel
{
public ProductListViewModel(INavigator navigator)
: base(navigator)
{
}
public Task OpenDetailsAsync(int id, CancellationToken cancellationToken) =>
Navigator!.NavigateToAsync<ProductDetailsViewModel, ProductDetailsArgs>(
new ProductDetailsArgs(id), cancellationToken);
}
[Fact]
public async Task OpenDetails_pushes_details()
{
var navigator = new FakeNavigator()
.Map<ProductDetailsViewModel>("details");
var vm = new ProductListViewModel(navigator);
await vm.AppearAsync();
await vm.OpenDetailsAsync(42);
Assert.Equal(typeof(ProductDetailsViewModel), navigator.Current);
Assert.True(navigator.CanGoBack);
}[Fact]
public void Dispose_makes_the_viewmodel_collectable()
{
Assert.True(LeakProbe.IsCollected(CreateAndDispose()));
}
static WeakReference CreateAndDispose()
{
var vm = new HomeViewModel();
var leak = LeakProbe.Track(vm);
vm.Dispose();
return leak;
}Sample map
There is no separate MVVMExpress.SampleApp repository. Samples ship in the product repo. The 15-minute path is Playground (command, navigation, dialog, form, auth, list). First-run login is AuthApp: sign in → home, plus register and forgot password (demo@mvvmexpress.dev / secret). AuthApp uses UseMvvmExpress(o => o.UseShell().UseDialogs().UseAuth<AuthLoginViewModel>()), ResetAsync replace-root, [RequiresAuth], and FormViewModel dirty confirm. The flyout catalog still lives in Plugin.Maui.MVVMExpress.Sample.
Clone and run: samples/Playground
In-repo sample map: samples/
Standalone reference app: WhatsApp clone using MVVMExpress Framework
| Sample | ViewModels | What it integrates |
|---|---|---|
| Basic | CounterViewModel | ViewModel, SetProperty, NotifyDependsOn, ModelCommand |
| CRUD | ProductList / ProductEdit | FormViewModel dirty / undo / redo, AsyncState, IValidator |
| Navigation | Home / ProductDetails / ScopedCatalog | PageViewModel, INavigator, IAcceptNavArgs<T>, IAcceptNavQuery, INotifier toast, IViewModelScopeFactory |
| Page stack | PageStack / PageStackItem | IPageNavigator, URI query, Stack / CanGoBack / PopToRoot / Replace / Reset |
| Auth (flyout) | Login / SecureHome | IAuthState, GuardedNavigator — push secure; adapt SecureSession |
| Playground | Home / Details / Edit / Login | 15-minute path: UseAuth, command, nav, dialog, form, list |
| AuthApp | AuthLogin / AuthHome / Register / Forgot | UseAuth<AuthLoginViewModel>, ResetAsync replace-root, MustMatch, dirty confirm |
| Offline | OfflineCatalogViewModel | ICachedFetcher + FetchPolicy — adapt ApiCache / OfflineSync |
| Pagination | PagedProductViewModel | DelegatePagedCollection load-more + refresh (not a live inbox) |
| Chat host | ChatHost / ChatInbox | SectionHostViewModel, SnapshotCollection, SearchQuery.CommittedText, CoalescingDispatcher |
| WhatsApp clone | WhatsAppUIClone | Standalone reference app: SectionHost, SnapshotCollection, NavigationPage |
| Reactive | SearchViewModel | SearchQuery debounce + PropertyObservable.CombineLatest |
| Enterprise | EnterpriseShell / CatalogStatus | Child composition, IFeatureSwitch, hub, busy, probe, auth gate |
| Generated | GeneratedCatalogViewModel | [Notify], [ModelCommand], [PersistState], [RegisterViewModel], [Route], [RequiresAuth] |