-
Notifications
You must be signed in to change notification settings - Fork 11
1 Using the PageResolver
Install the Nuget package
dotnet add package Goldie.MauiPlugins.PageResolver
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.
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;
}
}
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}">
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.
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.
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.
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.
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.
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.
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.