Nkraft.MvvmEssentials 1.2.0

dotnet add package Nkraft.MvvmEssentials --version 1.2.0
                    
NuGet\Install-Package Nkraft.MvvmEssentials -Version 1.2.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Nkraft.MvvmEssentials" Version="1.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Nkraft.MvvmEssentials" Version="1.2.0" />
                    
Directory.Packages.props
<PackageReference Include="Nkraft.MvvmEssentials" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Nkraft.MvvmEssentials --version 1.2.0
                    
#r "nuget: Nkraft.MvvmEssentials, 1.2.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Nkraft.MvvmEssentials@1.2.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Nkraft.MvvmEssentials&version=1.2.0
                    
Install as a Cake Addin
#tool nuget:?package=Nkraft.MvvmEssentials&version=1.2.0
                    
Install as a Cake Tool

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.

NuGet Version NuGet Pre-release GitHub Release NuGet Downloads .NET

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 your IAppStartup implementation if one exists, or generates a default one from the page marked isInitial: 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.


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 IAppStartup implementation is allowed. Defining two will produce a compile error (MVE002).
  • If no IAppStartup is found and no page is marked isInitial: true, a compiler warning (MVE001) is emitted.
  • When IAppStartup is defined, the isInitial: true flag is ignored.

Usage

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);
}

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

Contribution

Pull requests and issues are welcome. Thanks!

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.2.0 83 3/9/2026
1.1.3 82 2/28/2026
1.0.17 146 10/18/2025 1.0.17 is deprecated because it is no longer maintained.
Loading failed

Make ViewModel disposable and other improvements