Skip to content

1 Using the PageResolver

Matt Goldman edited this page Nov 22, 2023 · 9 revisions

1.1 Install Nuget package

Install the Nuget package

dotnet add package Goldie.MauiPlugins.PageResolver

1.2 Register dependencies

Your services, view models, and pages all need to be registered in the service collection. Update the Configure method in your Startup.cs as follows

using Microsoft.Maui;
using Microsoft.Maui.Hosting;
using Microsoft.Maui.Controls.Compatibility;
// Add reference to PageResolver
using Maui.Plugins.PageResolver;
// Add any required references to your services, view models, etc.
using MyApp.Services;
using MyApp.ViewModels;
using MyApp.Views;

namespace MyApp
{
    public static class MauiProgram
    {
        public static MauiApp CreateMauiApp()
        {
            var builder = MauiApp.CreateBuilder();
            builder
            .UseMauiApp<App>()				
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
            });

            // register your services
            builder.Services.AddSingleton<IMyService, MyService>();

            // register your view models
            builder.Services.AddTransient<MyViewModel>();

            // register your views
            builder.Services.AddTransient<MyPage>();

            // register the page resolver
            builder.Services.UsePageResolver();

            return builder.Build();
        }
    }
}

You can also register the PageResolver using the fluent API:

MauiAppBuilder? builder = MauiApp.CreateBuilder();
builder
    .UseMauiApp<App>()
    .UseMauiCommunityToolkit()
    .UsePageResolver()
    .ConfigureFonts(fonts =>
    {
        fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
        fonts.AddFont("la-solid-900.ttf", "LASolid");
    });

return builder.Build();

(Thanks to @IeuanWalker). Just like the first approach, you need to make sure to register your dependencies before registering the PageResolver.

1.3 Inject your dependencies

Use constructor injection to add your dependencies to your pages and view models. E.g.:

public class MyPage
{
    public MyViewModel ViewModel { get; set; }

    public MyPage(MyViewModel viewModel)
    {
        ViewModel = viewModel;
        BindingContext = ViewModel;
    }
}

And in your view models:

public class MyViewModel
{
    public IMyService Service { get; set; }

    public MyViewModel(IMyService myService)
    {
        Service = myService;
    }
}

1.4 Use in your code

In your code, navigate to your page by calling:

await Navigation.PushAsync<MyPage>();

This will return a fully resolved instance of MyPage including all dependencies.

Modal pages are also supported:

await Navigation.PushModalAsync<MyPage>();

It might be helpful to add a global using for the PageResolver so that you don't have to reference it in every file. If you have an _Imports.cs file in your project (or other file somewhere that you register all your global usings), add the reference in there:

global using Maui.Plugins.PageResolver;

If you don't have a file for registering all your global usings, you can add this anywhere in your project. But a single global usings file is a good idea.

You don't have to use the navigation extensions to use PageResolver. You can also use a XAML markup extension and set the binding context to your ViewModel. PageResolver will return a fully resolved instance of the ViewModel, with all it's injected dependencies.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MyApp.Pages.MyPage"
             xmlns:resolver="clr-namespace:Maui.Plugins.PageResolver;assembly=Maui.Plugins.PageResolver"
             xmlns:vm="clr-namespace:MyApp.ViewModels">
    <ContentPage.BindingContext>
        <resolver:ResolveViewModel x:TypeArguments="vm:MyViewModel" />
    </ContentPage.BindingContext>
</ContentPage>

Or, even simpler, this works too:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MyApp.Pages.MarkupPage"
             xmlns:resolver="clr-namespace:Maui.Plugins.PageResolver;assembly=Maui.Plugins.PageResolver"
             xmlns:vm="clr-namespace:MyApp.ViewModels"
             BindingContext="{resolver:ResolveViewModel x:TypeArguments=vm:MyViewModel}">

1.5 Use Page Parameters

You can pass parameters required by your Page's constructor. For example, consider the following constructor:

public MyPage(MyViewModel viewModel, string userName, int productId)
{
    ...
}

You can use PageResolver to pass these parameters when pushing a MyPage onto the navigation stack:

await Navigation.PushAsync<MyPage>("bob", 4);

Note that the MyViewModel doesn't need to be passed as a parameter. Anything registered with the service provider will be resolved automatically.

1.6 Use ViewModel Parameters (.NET 8 version only)

You can also pass parameters required by your ViewModel's constructor. If you want to use ViewModel parameters, you need a little extra setup (see below).

For example, consider the following constructors:

public MyPage(MyViewModel viewModel)
{
    ...
}
public MyViewModel(string userName, int productId)
{
    ...
}

You can use PageResolver to pass these parameters when pushing a MyPage onto the navigation stack:

await Navigation.PushAsync<MyPage>("bob", 4);

PageResolver will find the matching constructor for either your page or ViewModel and will resolve and return the appropriate instance.

1.6.1 Required setup for ViewModel parameters

Navigation with ViewModel parameters uses on a dictionary for mapping Page types to ViewModel types. There are three ways to provide this dictionary to PageResolver: generated at startup, manually supplied, or using the source generator.

The two methods that generate these for you (either at startup or with the source generator) rely on a convention (see below). If your app does not follow this convention, you can supply the dictionary yourself.

Mapping convention

The convention is straightforward it is simply [name]Page <--> [name]ViewModel

The following table shows how Pages and ViewModels might be mapped in a sample app.

Page name ViewModel name Match status
HomePage HomeViewModel
LoginPage LoginViewModel
ProfilePage ProfilePageViewModel

You can still use paramaterised ViewModel navigation if you don't follow this convention in your app, but you will need to supply the mapping dictionary yourself.

Option 1: Generated at startup

This is the easiest approach. To use this, simply set the UseParamaterisedViewModels parameter to true in the UsePageResolver extension method:

MauiAppBuilder? builder = MauiApp.CreateBuilder();
builder
    .UseMauiApp<App>()
    .UseMauiCommunityToolkit()
    .UsePageResolver(true) // <-- add true here
    .ConfigureFonts(fonts =>
    {
        fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
        fonts.AddFont("la-solid-900.ttf", "LASolid");
    });

return builder.Build();

This works for either the MauiAppBuilder extension (shown above) or the IServiceCollection extension:

builder.Services.UsePageResolver(true);

Note that this is disabled by default as it uses reflection at runtime to generate the mapping dictionary. During testing, this approach was found to add approximately 150 milliseconds to app startup time. This may be ok for most apps, but potentially not for others. Also, this would likely increase in an app with a significant number of Pages and ViewModels; hence disabled by default.

If you want to use paramaterised ViewModel navigation and have a large number of Pages and ViewModels, I recommend using one of the other approaches.

Option 2: Generated by the source generator

If you're using the source generator, it will automatically generate this dictionary for you. This means it is compiled and not generated at startup, so has no startup impact. See the documentation on the source generator for more information. If you're following the Page and ViewModel naming convention and using the source generator, you don't need to do anything else to use paramaterised ViewModel navigation.

Option 3: Pass the dictionary manually

You can create your own Page to ViewModel mapping dictionary and pass it to PageResolver:

var pageMappings = new Dictionary<Type, Type>()
{
	{ typeof(HomePage), typeof(HomeViewModel) },
	{ typeof(LoginPage), typeof(LoginViewModel) },
	{ typeof(ProfilePage), typeof(ProfilePageModel) },
};

builder.Services.UsePageResolver(pageMappings);

Note that this only works on the IServiceCollection extension, not the MauiAppBuilder extension.


WARNING: Note that parameters are not type checked, so passing an incorrect set of parameters can lead to runtime errors.

Also note that when using PageResolver with parameters, reflection is used to match the parameters to a constructor with a matching signature. So Pages with parameters will take a few nanoseconds more than without.