Burkus.Mvvm.Maui 0.1.0-preview5

Prefix Reserved
This is a prerelease version of Burkus.Mvvm.Maui.
There is a newer version of this package available.
See the version list below for details.
dotnet add package Burkus.Mvvm.Maui --version 0.1.0-preview5                
NuGet\Install-Package Burkus.Mvvm.Maui -Version 0.1.0-preview5                
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="Burkus.Mvvm.Maui" Version="0.1.0-preview5" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Burkus.Mvvm.Maui --version 0.1.0-preview5                
#r "nuget: Burkus.Mvvm.Maui, 0.1.0-preview5"                
#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.
// Install Burkus.Mvvm.Maui as a Cake Addin
#addin nuget:?package=Burkus.Mvvm.Maui&version=0.1.0-preview5&prerelease

// Install Burkus.Mvvm.Maui as a Cake Tool
#tool nuget:?package=Burkus.Mvvm.Maui&version=0.1.0-preview5&prerelease                

Burkus.Mvvm.Maui

Burkus.Mvvm.Maui (experimental)

Burkus.Mvvm.Maui is an MVVM (Model–view–viewmodel) framework for .NET MAUI. The library has some key aims it wants to provide:

  • Be lightweight and only provide the parts of MVVM that MAUI needs 👟
    • MAUI has dependency injection built-in now, Burkus.Mvvm.Maui takes advantage of this.
    • CommunityToolkit.Mvvm provides excellent: commanding, observable properties, source generating attributes, and fast messaging. Burkus.Mvvm.Maui does not compete with any of this and the idea is that you should pair both libraries together (or another library that does those things). This is not forced upon you, however.
    • MAUI [without Shell] needs: navigation, passing parameters, lifecycle events, and modals. Burkus.Mvvm.Maui wants to provide these things.
  • Be unit testable 🧪
    • .NET MAUI itself is difficult to unit test outside the box and sometimes third-party MAUI libraries can be too.
    • You should be easily able to assert that when you press a button, that the command that fires navigates you to a particular page.
  • Be easy to understand and setup 📄
    • The APIs and syntax should be easy to setup/understand
    • The library should be well documented (the current plan is to document the library in this README)
  • Be dependable for the future 🔮
    • The library is OSS and released under the MIT license. Contributors do not need to sign a CLA.
    • Individuals and businesses can fork it if it ever doesn't meet their needs.

⚠️ WARNING: Burkus.Mvvm.Maui is currently an experimental library. The API will change frequently and there will be frequent backwards compatibility breaking changes. This library will be versioned as "0.y.z" until a well-liked, stable API has been found. Only then would a version "1.y.z" and beyond be released.

Documentation 📗

See the DemoApp in the /samples folder of this repository for a full example of this library in action. The demo app has examples of different types of navigation, configuring the library, using lifecycle events, passing parameters, and showing native dialogs. The test project for the demo app demonstrates how you can write tests with code that calls this library.

Getting started

  1. Install Burkus.Mvvm.Maui into your main MAUI project from NuGet: https://www.nuget.org/packages/Burkus.Mvvm.Maui NuGet
  2. In your shared project's App.xaml.cs, remove any line where MainPage is set to a Page or an AppShell. Make App inherit from BurkusMvvmApplication. You should be left with a simpler App class like this:
public partial class App : BurkusMvvmApplication
{
    public App()
    {
        InitializeComponent();
    }
}
  1. Update App.xaml in your shared project to be a burkus:BurkusMvvmApplication.
<?xml version="1.0" encoding="UTF-8" ?>
<burkus:BurkusMvvmApplication
    ...
    xmlns:burkus="http://burkus.co.uk">
    ...
</burkus:BurkusMvvmApplication>
  1. In your MauiProgram.cs file, call .UseBurkusMvvm() in your builder creation e.g.:
public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder()
            .UseMauiApp<App>()
            .UseBurkusMvvm(burkusMvvm =>
            {
                burkusMvvm.OnStart(async (navigationService) =>
                {
                    await navigationService.Push<LoginPage>();
                });
            })
            ...
  1. 💡 RECOMMENDED: This library pairs great with the amazing CommunityToolkit.Mvvm. Follow its Getting started guide to add it.

Registering views, viewmodels, and services

A recommended way to register your views, viewmodels, and services is by creating extension methods in your MauiProgram.cs file.

public static MauiAppBuilder RegisterViewModels(this MauiAppBuilder mauiAppBuilder)
{
    mauiAppBuilder.Services.AddTransient<HomeViewModel>();
    mauiAppBuilder.Services.AddTransient<SettingsViewModel>();

    return mauiAppBuilder;
}

public static MauiAppBuilder RegisterViews(this MauiAppBuilder mauiAppBuilder)
{
    mauiAppBuilder.Services.AddTransient<HomePage>();
    mauiAppBuilder.Services.AddTransient<SettingsPage>();

    return mauiAppBuilder;
}

public static MauiAppBuilder RegisterServices(this MauiAppBuilder mauiAppBuilder)
{
    mauiAppBuilder.Services.AddSingleton<IWeatherService, WeatherService>();

    return mauiAppBuilder;
}

Dependency injection

View setup

In your xaml page, you need to use the ResolveBindingContext markup extension so that the correct viewmodel will be resolved for your view during navigation.

<ContentPage
    ...
    xmlns:burkus="http://burkus.co.uk"
    xmlns:vm="clr-namespace:DemoApp.ViewModels"
    BindingContext="{burkus:ResolveBindingContext x:TypeArguments=vm:HomeViewModel}"
    ...>

NOTE: You may get an error when using the above syntax that will go away when you actually run the app.

Complete example (x:DataType has also been added for improved performance and better auto-complete suggestions in XAML):

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
    x:Class="DemoApp.Views.HomePage"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:burkus="http://burkus.co.uk"
    xmlns:vm="clr-namespace:DemoApp.ViewModels"
    x:DataType="vm:HomeViewModel"
    BindingContext="{burkus:ResolveBindingContext x:TypeArguments=vm:HomeViewModel}">
    ...
</ContentPage>

Viewmodel setup

In your viewmodel's constructor, include references to any services you want to be automatically resolved. In the below example, Burkus.Mvvm.Maui's INavigationService and an example service called IExampleService will be resolved when navigating to HomeViewModel.

public HomeViewModel(
    INavigationService navigationService,
    IExampleService exampleService)
{
    this.navigationService = navigationService;
    this.exampleService = exampleService;
}

You can use the static class ServiceResolver to resolve services elsewhere in your application (for example, inside of converters and inside of xaml.cs files). You should use this sparingly as it will make your code less unit-testable.

Typed service resolution:

ServiceResolver.Resolve<IExampleService>();

Untyped service resolution:

ServiceResolver.Resolve(IExampleService);

INavigationService is automatically registered by .UseBurkusMvvm(...). You can use it to: push pages, pop pages, pop to the root page, replace the top page of the app, reset the navigation stack, switch tabs, and more. See the INavigationService interface in the repository for all possible navigation method options.

This is a simple navigation example where we push a "TestPage" onto the navigation stack:

await navigationService.Push<TestPage>();

Almost all the methods offer an overload where you can pass NavigationParameters navigationParameters. These parameters can be received by the page you are navigating to by using the Burkus MVVM lifecycle events in your viewmodel.

Here is an example where we set three parameters in three different ways and pass them to the next page:

var navigationParameters = new NavigationParameters
{
    // 1. on NavigationParameters object creation, set as many keys as you wish
    { "username", Username },
};

// 2. append an additional, custom parameter
navigationParameters.Add("selection", Selection);

// 3. reserved parameter with a special meaning in the Burkus MVVM library, it has a helper method to make setting it easier
navigationParameters.UseModalNavigation = true;

await navigationService.Push<TestPage>(navigationParameters);

The INavigationService supports URI/URL-based navigation. Use the .Navigate(string uri) or .Navigate(string uri, NavigationParameters navigationParameters) methods to do more complex navigation.

⚠️ WARNING: URI-based navigation behavior is unstable and is likely to change in future releases. Passing parameters, events triggered etc. are all inconsistent at present.

Here are some examples of URI navigation:

// use absolute navigation (starts with a "/") to go to the LoginPage
navigationService.Navigate("/LoginPage");

// push multiple pages using relative navigation onto the stack
navigationService.Navigate("AlphaPage/BetaPage/CharliePage");

// push a page relatively with query parameters
navigationService.Navigate("HomePage?username=Ronan&loggedIn=True");

// push a page with query parameters *and* navigation parameters
// - the query parameters only apply to one segment
// - the navigation parameters apply to the entire navigation
// - query parameters override navigation parameters
var parameters = new NavigationParameters { "example", 456 };
navigationService.Navigate("ProductPage?productid=123", parameters);

// go back one page modally
var parameters = new NavigationParameters();
parameters.UseModalNavigation = true;
navigationService.Navigate("..", parameters);

// go back three pages and push one new page
navigationService.Navigate("../../../AlphaPage");

// it is good practice to use nameof(x) to provide a compile-time reference to the pages in your navigation
navigationService.Navigate($"{nameof(YankeePage)}/{nameof(ZuluPage)}");

Navigation to multiple pages simultaneously and passing parameters to them can start to get complicated quickly. The NavigationUriBuilder is a simple, typed way to build a complex navigation string.

Below is an example where we go back a page (and pass a parameter that instructs the navigation to be performed modally), then push a VictorPage, and then push a YankeePage modally onto the stack:

var parameters = new NavigationParameters();
parameters.UseModalNavigation = true;

var navigationUri = new NavigationUriBuilder()
    .AddGoBackSegment(parameters)
    .AddSegment<VictorPage>()
    .AddSegment<YankeePage>(parameters)
    .Build() // produces the string: "..?UseModalNavigation=True/VictorPage/YankeePage/"

navigationService.Navigate(navigationUri);

Choosing the start page of your app

It is possible to have a service that decides which page is most appropriate to navigate to. This service could decide to:

  • Navigate to the "Terms & Conditions" page if the user has not agreed to the latest terms yet
  • Navigate to the "Signup / Login" page if the user is logged out
  • Navigate to the "Home" page if the user has used the app before and doesn't need to do anything
public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder()
            .UseMauiApp<App>()
            .UseBurkusMvvm(burkusMvvm =>
            {
                burkusMvvm.OnStart(async (navigationService) =>
                {
                    var appStartupService = ServiceResolver.Resolve<IAppStartupService>();
                    await appStartupService.NavigateToFirstPage();
                });
            })
            ...

Lifecycle events and passing parameters

INavigatedEvents

If your viewmodel inherits from this interface, the below events will trigger for it.

  • OnNavigatedTo(parameters)

    • You can use this lifecycle event to retrieve parameters passed to this page
    • Is similar to MAUI's Page' OnNavigatedTo event.
    public async Task OnNavigatedTo(NavigationParameters parameters)
    {
        Username = parameters.GetValue<string>("username");
    }
    
  • OnNavigatedFrom(parameters)

    • Allows the page you are leaving to add additional parameters to the page you are navigating to
    • Is similar to MAUI's Page's OnNavigatedFrom event.
    public async Task OnNavigatedFrom(NavigationParameters parameters)
    {
        parameters.Add("username", username);   
    }
    

INavigatingEvents

If your viewmodel inherits from this interface, the below events will trigger for it.

  • OnNavigatingFrom(parameters)
    • Allows the page you are leaving to add additional parameters to the page you are navigating to
    • Is similar to MAUI's Page's OnNavigatingFrom event.
    public async Task OnNavigatingFrom(NavigationParameters parameters)
    {
        parameters.Add("username", username);   
    }
    

Reserved navigation parameters

Several parameter keys have been pre-defined and are using by the Burkus.Mvvm.Maui library to adjust how navigation is performed.

  • ReservedNavigationParameters.UseAnimatedNavigation
    • If true, uses an animation during navigation.
    • Type: bool
    • Default: true
  • ReservedNavigationParameters.UseModalNavigation
    • If true, performs the navigation modally.
    • Type: bool
    • Default: false
  • ReservedNavigationParameters.SelectTab
    • If navigating to a TabbedPage, selects the tab with the name of the type passed. ⚠️ WARNING: Not yet implemented.
    • Type: string
    • Default: null

The NavigationParameters object exposes some handy properties .UseAnimatedNavigation and .UseModalNavigation so you can easily set or check the value of these properties.

Dialog service

IDialogService is automatically registered by .UseBurkusMvvm(...). It is a testable service that is an abstraction over the MAUI alerts/pop-ups/prompts/action sheets.

Register the service in your viewmodel constructor:

public HomeViewModel(
    IDialogService dialogService,
    INavigationService navigationService)
{
    this.dialogService = dialogService;
    this.navigationService = navigationService;
}

This is a simple example of showing an error alert message with the DialogService:

dialogService.DisplayAlert(
    "Error",
    "You must enter a username.",
    "OK");

See the IDialogService interface in the repository for all the possible method options.

Advanced / complexities

The below are some things of note that may help prevent issues from arising:

  • When you inherit from BurkusMvvmApplication, the MainPage of the app will be automatically set to a NavigationPage. This means the first page you push can be a ContentPage rather than needing to push a NavigationPage. This may change in the future.

Roadmap 🛣️

Create an issue to add your own suggestions.

Green letters: M V V M laid out vertically

Contributing 💁‍♀️

Contributions are very welcome! Please see the contributing guide to get started.

NuGet release Build for CI Build Demo App for CI

License 🪪

The project is distributed under the MIT license. Contributors do not need to sign a CLA.

Product Compatible and additional computed target framework versions.
.NET net7.0 is compatible.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.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
0.2.1-preview2 336 1/1/2024
0.2.1-preview1 201 11/18/2023
0.2.0 339 11/14/2023
0.2.0-preview8 101 11/11/2023
0.2.0-preview7 103 11/11/2023
0.2.0-preview6 108 11/11/2023
0.2.0-preview5 116 11/11/2023
0.2.0-preview4 155 10/23/2023
0.2.0-preview3 123 10/18/2023
0.2.0-preview2 109 10/16/2023
0.2.0-preview1 117 10/14/2023
0.1.0 169 10/14/2023
0.1.0-preview7 97 10/13/2023
0.1.0-preview6 110 10/12/2023
0.1.0-preview5 104 10/12/2023
0.1.0-preview4 122 10/7/2023
0.1.0-preview3 100 10/5/2023
0.1.0-preview2 111 10/5/2023
0.1.0-preview1 108 10/5/2023