Nkraft.MvvmEssentials
1.2.0
dotnet add package Nkraft.MvvmEssentials --version 1.2.0
NuGet\Install-Package Nkraft.MvvmEssentials -Version 1.2.0
<PackageReference Include="Nkraft.MvvmEssentials" Version="1.2.0" />
<PackageVersion Include="Nkraft.MvvmEssentials" Version="1.2.0" />
<PackageReference Include="Nkraft.MvvmEssentials" />
paket add Nkraft.MvvmEssentials --version 1.2.0
#r "nuget: Nkraft.MvvmEssentials, 1.2.0"
#:package Nkraft.MvvmEssentials@1.2.0
#addin nuget:?package=Nkraft.MvvmEssentials&version=1.2.0
#tool nuget:?package=Nkraft.MvvmEssentials&version=1.2.0
MvvmEssentials
Lightweight MVVM utility library for .NET MAUI. It simplifies navigation, tab handling, and popup management with opinionated conventions and minimal boilerplate. It also serves as an alternative to .NET MAUI Shell.
Setup
Quick test using this test project, or just follow the instructions below.
1. Configure in MauiProgram.cs
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureMvvmEssentials(Assembly.GetExecutingAssembly(), registry =>
{
// ViewModel and Page naming convention must strictly be followed:
// page_name + "Page", vm_name + "ViewModel", wherein page_name == vm_name
//
// Mark one page with isInitial: true. This is where the app starts.
// If you need conditional startup logic (e.g. auth checks),
// implement IAppStartup instead (see below).
registry.MapPage<LandingPage, LandingViewModel>(isInitial: true)
.MapPage<MainPage, MainViewModel>()
.MapPage<LoginPage, LoginViewModel>()
.MapPage<SettingsPage, SettingsViewModel>();
});
// Required: registers the auto-discovered or generated IAppStartup
builder.Services.AddDiscoveredAppStartup();
// ..
}
}
Note:
AddDiscoveredAppStartup()is source-generated at compile time. It automatically wires up yourIAppStartupimplementation if one exists, or generates a default one from the page markedisInitial: true.
2. Wire up the window in App.xaml.cs
public partial class App : Application
{
private readonly AppStartupWindowHook _hook;
public App(AppStartupWindowHook hook)
{
_hook = hook;
InitializeComponent();
}
protected override Window CreateWindow(IActivationState? activationState)
{
_hook.Attach();
return base.CreateWindow(activationState);
}
}
That's it. The hook fires the initial navigation automatically.
3. Delete any Shell related files. They are not used here.
Custom Startup Logic (Optional)
If you need to run async logic before deciding where to navigate (e.g. auth checks, feature flags),
implement IAppStartup anywhere in your project. The source generator will find it automatically,
no registration needed.
public class AppStartup : IAppStartup
{
private readonly INavigationService _navigationService;
private readonly IAuthService _authService;
public AppStartup(INavigationService navigationService, IAuthService authService)
{
_navigationService = navigationService;
_authService = authService;
}
public async Task OnInitializedAsync()
{
var isLoggedIn = await _authService.CheckAsync();
await _navigationService
.Absolute(withNavigation: false)
.Push(isLoggedIn ? typeof(HomeViewModel) : typeof(LoginViewModel))
.NavigateAsync();
}
}
Rules:
- Only one
IAppStartupimplementation is allowed. Defining two will produce a compile error (MVE002).- If no
IAppStartupis found and no page is markedisInitial: true, a compiler warning (MVE001) is emitted.- When
IAppStartupis defined, theisInitial: trueflag is ignored.
Usage
NavigationService
interface INavigationService
{
// Under the hood, detects which current page type is active and performs either
// a page replacement or pushes onto the stack if it's a NavigationPage.
// Prefer the extensions below over calling this directly.
Task<IResult> NavigateAsync(string path, INavigationParameters? parameters = null, bool animated = true);
// Wraps Navigation.PopAsync()
Task<IResult> NavigateBackAsync(bool animated = true);
// Wraps Navigation.PopToRootAsync()
Task<IResult> NavigateToRootAsync(INavigationParameters? parameters = null, bool animated = true);
}
NavigationExtension Examples
1. Absolute navigation (page replacement)
await _navigationService.Absolute(withNavigation: true)
.Push<FirstViewModel, object>(new { A = 1 }) // .Push can only handle "primitive" data types
.Push<SecondViewModel, object>(new { B = 2 })
.Push<ThirdViewModel, object>(new { C = 3 })
.NavigateAsync();
// Constructs "//NavigationPage/FirstPage?A=1/SecondPage?B=2/ThirdPage?C=3"
2. Navigation with parameters
// Pass parameters via object type
await _navigationService.NavigateAsync<LoginViewModel, object>(new { ErrorMessage = "Session expired", Test = 1 });
// Pass parameters via custom type
record LoginParameters(string ErrorMessage, int Test);
await _navigationService.NavigateAsync<LoginViewModel, LoginParameters>(new("Session expired", 1));
// Pass parameters via INavigationParameters
var parameters = new NavigationParameters
{
{ "ErrorMessage", "Session expired" },
{ "Test", 1 }
};
await _navigationService.NavigateAsync<LoginViewModel>(parameters);
// LoginViewModel.cs
class LoginViewModel : PageViewModel
{
// Automatically mapped from navigation parameters
public string? ErrorMessage { get; set; }
// Or handle manually
protected override void OnParametersSet(INavigationParameters parameters)
{
if (parameters.TryGetValue<int>("Test", out var testValue))
{
// ..
}
}
}
3. Contextual navigation
// Replaces the page if the active page is not a NavigationPage,
// or pushes onto the stack if it is.
await _navigationService.NavigateAsync<AccountViewModel>();
4. Select tab of TabbedPage
await _navigationService.Absolute(withNavigation: false)
.Push<MainViewModel, object>(new { SelectedTabIndex = 2 }) // switches to 3rd tab
.NavigateAsync();
ViewModel Lifecycle
<details> <summary>PageViewModel</summary>
| Method | When it is called |
|---|---|
OnParametersSet |
Called when navigation parameters are passed to this ViewModel |
OnInitialized |
Called once on the first page appearing |
OnInitializedAsync |
Async version of OnInitialized |
OnPageAppearing |
Called every time the page appears |
OnPageAppearingAsync |
Async version of OnPageAppearing |
OnPageDisappearing |
Called every time the page disappears |
OnPageDisappearingAsync |
Async version of OnPageDisappearing |
OnNavigatedTo |
Called when the page is navigated to |
OnNavigatedFrom |
Called when the page is navigated away from |
OnNavigatedToRoot |
Called when the navigation stack is popped back to this page as root |
OnNavigatedToRootAsync |
Async version of OnNavigatedToRoot |
OnPageUnloaded |
Called when the page is removed from the visual tree |
OnDispose |
Called when the DI scope is disposed |
</details>
<details> <summary>TabViewModel</summary>
| Method | When it is called |
|---|---|
OnInitialized |
Called once on the first tab selection |
OnInitializedAsync |
Async version of OnInitialized |
OnTabSelected |
Called every time the tab is selected |
OnTabSelectedAsync |
Async version of OnTabSelected |
OnTabUnselected |
Called every time the tab is unselected |
OnTabUnselectedAsync |
Async version of OnTabUnselected |
OnDispose |
Called when the parent host's DI scope is disposed |
</details>
<details> <summary>PopupViewModel</summary>
Inherits all lifecycle methods from PageViewModel.
</details>
Resource Cleanup (IDisposable)
All ViewModels (PageViewModel, TabViewModel, PopupViewModel) implement IDisposable. When a page is unloaded, the library automatically disposes its ViewModel via the DI scope.
To clean up resources, override OnDispose() in your ViewModel:
public class MyViewModel : PageViewModel
{
private readonly Timer _timer;
public MyViewModel()
{
_timer = new Timer(OnTick, null, 0, 1000);
}
protected override void OnDispose()
{
_timer.Dispose();
}
}
TabbedPage + NavigationPage
1. Register tabs in DI
// Tab ViewModels are not mapped to pages. They are bound via XAML.
// Use RegisterTab to ensure they are registered with the correct lifetime.
registry.MapPage<MainPage, MainViewModel>()
.RegisterTab<HomeViewModel>()
.RegisterTab<SettingsViewModel>();
2. Define tabs in XAML
<TabbedPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:behaviors="clr-namespace:Nkraft.MvvmEssentials.Behaviors;assembly=Nkraft.MvvmEssentials"
xmlns:local="clr-namespace:MauiApp1"
xmlns:android="clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration.AndroidSpecific;assembly=Microsoft.Maui.Controls"
android:TabbedPage.ToolbarPlacement="Bottom"
x:DataType="local:MainViewModel"
x:Class="MauiApp1.MainPage">
<TabbedPage.Behaviors>
<behaviors:TabSelectionBehavior />
</TabbedPage.Behaviors>
<NavigationPage Title="Home">
<x:Arguments>
<local:HomePage BindingContext="{Binding HomeViewModel}" />
</x:Arguments>
</NavigationPage>
<NavigationPage Title="Settings">
<x:Arguments>
<local:SettingsPage BindingContext="{Binding SettingsViewModel}" />
</x:Arguments>
</NavigationPage>
</TabbedPage>
3. Define the tab host ViewModel
public class MainViewModel(HomeViewModel homeViewModel, SettingsViewModel settingsViewModel) : TabHostViewModel
{
protected override IReadOnlyCollection<ITabComponent> Tabs => [HomeViewModel, SettingsViewModel];
public HomeViewModel HomeViewModel { get; } = homeViewModel;
public SettingsViewModel SettingsViewModel { get; } = settingsViewModel;
}
4. Define tab ViewModels
public partial class HomeViewModel(ISemanticScreenReader screenReader) : TabViewModel
{
private readonly ISemanticScreenReader _screenReader = screenReader;
protected override void OnTabSelected()
{
base.OnTabSelected();
Console.WriteLine("Home tab selected");
}
protected override void OnTabUnselected()
{
base.OnTabUnselected();
Console.WriteLine("Bye!");
}
[RelayCommand]
private void IncreaseCount()
{
Count++;
CountButtonText = Count == 1 ? $"Clicked {Count} time" : $"Clicked {Count} times";
_screenReader.Announce(CountButtonText);
}
public int Count { get; set; }
public string CountButtonText { get; set; } = "Click me";
}
FlyoutPage
// TODO
PopupPage
This feature is made possible by the awesome Mopups library.
Setup
builder
.UseMauiApp<App>()
.ConfigureMopups(); // Required for Mopups
// Register popup pages alongside regular pages
registry.MapPage<ConfirmPopup, ConfirmViewModel>();
Usage
1. Define the popup in XAML
<nkraft:PopupPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:nkraft="clr-namespace:Nkraft.MvvmEssentials.Pages;assembly=Nkraft.MvvmEssentials"
x:DataType="local:ConfirmViewModel"
x:Class="MauiApp1.ConfirmPopup"
Title="Confirm">
</nkraft:PopupPage>
2. Define the backing ViewModel
record ConfirmResult(bool Confirm);
internal partial class ConfirmViewModel(IPopupService popupService) : PopupViewModel<ConfirmResult>(popupService)
{
[RelayCommand]
private async Task Yes() => await Dismiss(new ConfirmResult(true));
[RelayCommand]
private async Task No() => await Dismiss(new ConfirmResult(false));
public string? ConfirmationMessage { get; set; }
}
3. Present the popup from another ViewModel
var navParams = new NavigationParameters
{
{ nameof(ConfirmViewModel.ConfirmationMessage), "Reset counter?" }
};
var result = await _popupService.PresentAsync<ConfirmViewModel, ConfirmResult>(navParams);
if (result.TryGetValue(out var confirmResult))
{
Console.WriteLine("User pressed: {0}", confirmResult.Confirm ? "Yes" : "No");
if (confirmResult.Confirm)
Count = 0;
}
else
{
// User tapped the background or pressed back
Console.WriteLine("User cancelled the popup");
}
Notes
- Inspired by Prism
Contribution
Pull requests and issues are welcome. Thanks!
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net9.0
- CommunityToolkit.Mvvm (>= 8.4.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.3)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.3)
- Microsoft.Extensions.Options (>= 10.0.3)
- Microsoft.Maui.Controls (>= 9.0.120)
- Mopups (>= 1.3.4)
- Nkraft.CrossUtility (>= 1.0.7)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Make ViewModel disposable and other improvements