Back to Blog
.NET MAUI

Light weight prism-like navigation for MAUI

M
Mario Naumoski
Dec 5, 2025⏱️20 min read

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:

  • MVVM-friendly (ViewModels resolved by DI)
  • Fast and light (no heavy frameworks / minimal reflection)
  • Supports InitializeAsync, OnNavigatedTo, OnNavigatedFrom lifecycles
  • Supports popups (CommunityToolkit)
  • Uses page keys + registry for clear navigation contracts
  • 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 identifiers
  • PageRegistry → maps pages to viewmodels
  • INavigationAware → lifecycle hooks for MVVM
  • NavigationService → resolves pages/viewmodels via DI, handles push/pop, popups, and GoBackAsync
  • 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

  • Instead of manually instantiating pages or viewmodels, the service uses the PageRegistry and the DI container to create them.
  • This ensures every page gets the correct ViewModel with all its dependencies injected automatically.
  • Handles Navigation Logic

  • Pushes pages onto the navigation stack (PushAsync)
  • Pops pages (PopAsync) via GoBackAsync()
  • Manages popup pages using CommunityToolkit (ShowPopupAsync / CloseAsync)
  • Maintains clean separation between view and navigation logic.
  • Manages ViewModel Lifecycle

  • Calls InitializeAsync(parameter) for async setup before the page is displayed
  • Calls OnNavigatedTo(parameter) after navigation
  • Calls OnNavigatedFrom() for the previous page
  • This mimics Prism’s lifecycle events, keeping your MVVM flow predictable and clean.
  • 💡 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:

  • tight coupling
  • lots of boilerplate code
  • harder testing
  • 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

  • After set up whole architecture, very important thing is to register the services and viewmodels, so everything can be binded and ready.
  • //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:

  • clear page-key navigation (declarative)
  • DI-driven page & VM resolution
  • Prism-style lifecycle hooks (InitializeAsync, OnNavigatedTo, OnNavigatedFrom)
  • popup support and GoBackAsync()
  • minimal overhead and excellent startup performance
  • 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

    Tags

    #.NET MAUI#Navigation#MVVM#Dependecy injection#Prism