Light weight prism-like navigation for MAUI
Table of Contents
- • Core functionality set up
- • PageKeys
- • IPageRegistry + PageRegistry
- • INavigationAware + BaseViewModel
- • INavigationService + NavigationService
- • Register service in MauiProgram.cs
- • First page resolve
- • Example & Usage in view model
- • Conclusion and tips
Lightweight Prism-Style Navigation in .NET MAUI - Step by Step Code Snippets
A compact, practical tutorial you can drop straight into your blog. It shows how to build a lightweight, Prism-like navigation system for .NET MAUI that is:
This patter gives solid base for simple application to easy scale up into robust and complex application without affecting the performance especially on cold start, because it's avoid resolving huge libraries.
For this case you will need nuget package CommunityToolkit.Maui
Core functionality set up
These four components form the core of a lightweight, Prism-like navigation system in MAUI.
PageKeys
Central place for all navigation keys.
// Core/PageKeys.cs
public static class PageKeys
{
public const string MainPage = "MainPage";
public const string FoodPage = "FoodPage";
public const string FoodDetailsPage = "FoodDetailsPage";
public const string InfoPopup = "InfoPopup";
}IPageRegistry + PageRegistry
Registry maps page keys to (PageType, ViewModelType, isPopup).
// Core/IPageRegistry.cs
public interface IPageRegistry
{
void Register(string key, Type pageType, Type viewModelType, bool isPopup = false);
(Type pageType, Type viewModelType, bool isPopup) GetRegistration(string key);
}// Core/PageRegistry.cs
using System.Collections.Concurrent;
public class PageRegistry : IPageRegistry
{
private readonly ConcurrentDictionary<string, (Type page, Type vm, bool popup)> _map
= new();
public void Register(string key, Type pageType, Type viewModelType, bool isPopup = false)
{
_map[key] = (pageType, viewModelType, isPopup);
}
public (Type pageType, Type viewModelType, bool isPopup) GetRegistration(string key)
{
if (_map.TryGetValue(key, out var entry)) return entry;
throw new KeyNotFoundException($"Page key '{key}' not registered.");
}
}INavigationAware + BaseViewModel
These provide InitializeAsync, OnNavigatedTo, OnNavigatedFrom. Put InitializeAsync virtual in the base so all VMs can override. This helps to copy the prism-like navigation using initialize or pass object between pages.
Also BaseViewModel helps to have share code where all other ViewModels inherits and make easier to build more robust applications.
// Core/INavigationAware.cs
public interface INavigationAware
{
Task InitializeAsync(object? parameter = null);
void OnNavigatedTo(object? parameter);
void OnNavigatedFrom();
}// Core/BaseViewModel.cs
using System.ComponentModel;
using System.Runtime.CompilerServices;
public abstract class BaseViewModel : INotifyPropertyChanged, INavigationAware
{
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
public virtual Task InitializeAsync(object? parameter = null) => Task.CompletedTask;
public virtual void OnNavigatedTo(object? parameter) { }
public virtual void OnNavigatedFrom() { }
// Helper for properties
protected bool SetProperty<T>(ref T backing, T value, [CallerMemberName] string? propName = null)
{
if (EqualityComparer<T>.Default.Equals(backing, value)) return false;
backing = value;
OnPropertyChanged(propName);
return true;
}
}INavigationService + NavigationService
❤️ The Heart of the Navigation System: NavigationService
The NavigationService is the core of this lightweight Prism-like navigation system. It does three critical things:
Resolves Pages and ViewModels via DI
Handles Navigation Logic
Manages ViewModel Lifecycle
💡 Why It’s Important
Without a service like this, each page would need to know about other pages and how to create their viewmodels, which leads to:
The NavigationService centralizes everything, making your navigation declarative, lightweight, testable, and MVVM-friendly.
public interface INavigationService
{
public Task NavigateToAsync(string pageKey, object? parameter = null);
public Task GoBackAsync();
public Task PopupAsync(string popupKey, object? parameter = null);
public Task ClosePopupAsync();
}public class NavigationService : INavigationService
{
private readonly IServiceProvider _provider;
private readonly IPageRegistry _registry;
private Popup? _currentPopup;
public NavigationService(IServiceProvider provider,
IPageRegistry registry)
{
_provider = provider;
_registry = registry;
}
public async Task NavigateToAsync(string pageKey, object? parameter = null)
{
try
{
var (pageType, vmType, isPopup) = _registry.GetRegistration(pageKey);
var page = (Page)_provider.GetRequiredService(pageType);
if (page == null)
throw new InvalidOperationException($"Failed to resolve page of type {pageType.Name}");
var vm = _provider.GetRequiredService(vmType);
if (vm == null)
throw new InvalidOperationException($"Failed to resolve view model of type {vmType.Name}");
page.BindingContext = vm;
// Ensure we have a NavigationPage
var navPage = Application.Current?.MainPage as NavigationPage;
if (navPage == null)
{
navPage = new NavigationPage(Application.Current?.MainPage ?? throw new InvalidOperationException("MainPage not found"));
Application.Current.MainPage = navPage;
}
if (navPage.CurrentPage?.BindingContext is INavigationAware oldVm)
oldVm.OnNavigatedFrom();
await navPage.PushAsync(page);
if (vm is INavigationAware vmAware)
{
await vmAware.InitializeAsync(parameter);
vmAware.OnNavigatedTo(parameter);
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Navigation error for page key '{pageKey}': {ex.Message}");
throw;
}
}
public async Task GoBackAsync()
{
try
{
var navPage = Application.Current?.MainPage as NavigationPage;
if (navPage == null)
throw new InvalidOperationException("NavigationPage not found in MainPage.");
if (navPage.CurrentPage?.BindingContext is INavigationAware oldVm)
oldVm.OnNavigatedFrom();
await navPage.PopAsync();
if (navPage.CurrentPage?.BindingContext is INavigationAware newVm)
newVm.OnNavigatedTo(null);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"GoBackAsync error: {ex.Message}");
throw;
}
}
public async Task PopupAsync(string pageKey, object? parameter = null)
{
try
{
var (pageType, vmType, isPopup) = _registry.GetRegistration(pageKey);
var popup = (Popup)_provider.GetRequiredService(pageType);
if (popup == null)
throw new InvalidOperationException($"Failed to resolve popup of type {pageType.Name}");
var vm = _provider.GetRequiredService(vmType);
if (vm == null)
throw new InvalidOperationException($"Failed to resolve view model of type {vmType.Name}");
popup.BindingContext = vm;
_currentPopup = popup;
// Show the popup using the CommunityToolkit extension
await Application.Current.MainPage.ShowPopupAsync(popup);
if (vm is INavigationAware vmAware)
{
await vmAware.InitializeAsync(parameter);
vmAware.OnNavigatedTo(parameter);
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Popup error for popup key '{pageKey}': {ex.Message}");
throw;
}
}
public async Task ClosePopupAsync()
{
if (_currentPopup != null)
{
await _currentPopup.CloseAsync();
_currentPopup = null;
}
}
}Register service in MauiProgram.cs
//Services
builder.Services.AddSingleton<INavigationService, NavigationService>();\
builder.Services.AddSingleton<IPageRegistry>(s =>
{
var registry = new PageRegistry();
registry.Register(PageKeys.MainPage, typeof(MainPage), typeof(MainPageViewModel));
registry.Register(PageKeys.FoodPage, typeof(FoodPage), typeof(FoodPageViewModel));
registry.Register(PageKeys.FoodDetailsPage, typeof(FoodDetailsPage), typeof(FoodDetailsPageViewModel));
registry.Register(PageKeys.InfoPopup, typeof(InfoPopupView), typeof(InfoPopupViewModel), isPopup: true);
return registry;
});First page resolve
First page should be manualy resolved with DI page - viewmodel
public partial class App : Application
{
private readonly MainPage _mainPage;
public App(MainPage mainPage)
{
InitializeComponent();
_mainPage = mainPage;
}
protected override Window CreateWindow(IActivationState? activationState)
{
return new Window(_mainPage);
}
}public partial class MainPage : ContentPage
{
public MainPageViewModel _viewModel { get; set; }
public MainPage(MainPageViewModel viewModel)
{
InitializeComponent();
_viewModel = viewModel;
BindingContext = _viewModel;
}
}Example & Usage in view model
First resolve navigation service via DI in view model constructor
public class MainPageViewModel : BaseViewModel
{
public readonly INavigationService _navigationService;
public readonly IFoodService _foodService;
public ICommand NavigateToFoodDetails { get; set; }
public FoodPageViewModel(INavigationService navigationService,
IFoodService foodService)
{
_navigationService = navigationService;
_foodService = foodService;
NavigateToFoodDetails = new Command<string>(async (data) => { await OnNavigateToFoodDetails(data); });
}
}After resolving, we use command to navigate between pages and Dictionary to pass complex arguments and models.
private async Task OnNavigateToFoodDetails(string id)
{
var parameters = new Dictionary<string, object>
{
["Id"] = id
};
await _navigationService.NavigateToAsync(PageKeys.FoodDetailsPage, parameters);
}Conclusion and tips
This lightweight Prism-like system gives you:
This gives a lightweight, Prism-like navigation system without installing Prism, keeping your app fast, MVVM-compliant, and easy to maintain.
As well you can check my github repo for detail lookup on this approach.
https://github.com/MarioNaumoski07/MauiCustomNavigation.git