As a developer advocate at JetBrains, I find myself exploring technologies in nooks and crannies of the ecosystem previously out of reach. As a lifelong proponent of the web, mobile development has always been on my list to explore, but I rarely found the time to do so. One of the latest technologies I am dabbling with is Multi-platform App UI, more commonly referred to as MAUI.

In this post, I’ll show you how to use the packages CommunityToolkit.Mvvm and Scrutor to quickly register your XAML views and the corresponding ViewModels for a more convenient approach.

CommunityToolkit.Mvvm and the MVVM pattern

The Model-View-View-Model pattern, also referred to as MVVM, is an approach that separates the logic of your views from the language of the View. In the case of MAUI, that view language is typically XAML. Utilizing the pattern leads to a few positive side effects:

  1. Your ViewModels are much more straightforward to test, with the bulk of your logic exposed through properties and commands.
  2. Your Views are simpler, binding to properties and commands rather than directly to members on the specific ContentPage.
  3. In MAUI, both ContentPage and ViewModels can support dependency injection arguments, allowing for the composition of app functionality.

I’m sure there are other benefits to using the MVVM pattern, but these immediately spring to mind when compared to the alternative of dumping all logic in a ContentPage directly.

The CommunityToolkit.Mvvm package includes helpers to make adopting the MVVM pattern more straightforward and performant. CommunityToolkit.Mvvm uses source generators to generate the tedious parts of building MAUI apps, most notably the implementation of INotifyPropertyChanged and ICommand instances.

Let’s start with a simple update of the default MAUI template. Next, we’ll add a new MainPageViewModel and move most of the app’s logic from the MainPage.xaml.cs file to our new class.

Note: This sample is from a blog post by Mark Timmings. Check it out here.

using System.Diagnostics;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

namespace MyFirstMauiApp.ViewModels;

public partial class MainPageViewModel : ObservableObject
{
    [ObservableProperty] int count;
    [ObservableProperty] private string text;
    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(IncrementCountCommand))]
    bool canIncrement;
    
    bool CanExecuteIncrement() => canIncrement;

    [RelayCommand(CanExecute = nameof(CanExecuteIncrement))]
    void IncrementCount()
    {
        Count++;
        Text = Count == 1 
            ? $"Clicked {count} time" 
            : $"Clicked {count} times";
        
        SemanticScreenReader.Announce(Text);
    }
    
    partial void OnCanIncrementChanged(bool value)
    {
        Debug.WriteLine("OnCanIncrementChanged called");
    } 
}

Next, we’ll update our MainPage.xaml.cs code.

using MyFirstMauiApp.ViewModels;

namespace MyFirstMauiApp;

public partial class MainPage : ContentPage
{
    public MainPage(MainPageViewModel model)
    {
        InitializeComponent();
        BindingContext = model;
    }
}

We’ll also need to update the XAML to take advantage of our new MainPageViewModel class.

<?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"
             xmlns:viewModels="clr-namespace:MyFirstMauiApp.ViewModels"
             x:Class="MyFirstMauiApp.MainPage"
             x:DataType="viewModels:MainPageViewModel">

    <ScrollView>
        <VerticalStackLayout
            Spacing="25"
            Padding="30,0"
            VerticalOptions="Center">

            <Image
                Source="dotnet_bot.png"
                SemanticProperties.Description="Cute dot net bot waving hi to you!"
                HeightRequest="200"
                HorizontalOptions="Center" />

            <Label
                Text="Hello, World!"
                SemanticProperties.HeadingLevel="Level1"
                FontSize="32"
                HorizontalOptions="Center" />

            <Label
                Text="{Binding Text}"
                SemanticProperties.HeadingLevel="Level2"
                SemanticProperties.Description="Welcome to dot net Multi platform App U I"
                FontSize="18"
                HorizontalOptions="Center" />
            
            <CheckBox
                IsChecked="{Binding CanIncrement}" />

            <Button
                x:Name="CounterBtn"
                Text="{Binding Count, StringFormat='Click me ({0})'}"
                SemanticProperties.Hint="Counts the number of times you click"
                Command="{Binding IncrementCountCommand}"
                HorizontalOptions="Center" />

        </VerticalStackLayout>
    </ScrollView>

</ContentPage>

If you were to run this application now, you’d get the following MissingMethodException exception.

Unhandled Exception:
System.MissingMethodException: No parameterless constructor defined for type 'MyFirstMauiApp.MainPage'.
   at ObjCRuntime.Runtime.ThrowException(IntPtr gchandle)
   at UIKit.UIApplication.UIApplicationMain(Int32 argc, String[] argv, IntPtr principalClassName, IntPtr delegateClassName)
   at UIKit.UIApplication.Main(String[] args, Type principalClass, Type delegateClass)
   at MyFirstMauiApp.Program.Main(String[] args) in /Users/khalidabuhakmeh/RiderProjects/MyFirstMauiApp/MyFirstMauiApp/Platforms/iOS/Program.cs:line 13
2023-01-17 14:15:13.288973-0500 MyFirstMauiApp[40948:8367336] Unhandled managed exception: No parameterless constructor defined for type 'MyFirstMauiApp.MainPage'. (System.MissingMethodException)

MAUI doesn’t know how to instantiate any classes mentioned in this sample yet. Let’s fix that.

Using Scrutor to Register Views and ViewModels

In the context of an MVVM-powered MAUI app, there are two essential elements: The View and the ViewModel. These concepts are represented by the types ContentPage and ObservableObject.

Scrutor can scan for those types and register the instances as themselves with singleton lifetimes.

using Microsoft.Extensions.Logging;
using CommunityToolkit.Mvvm.ComponentModel;

namespace MyFirstMauiApp;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();

        // The important part
        builder.Services.Scan(s => s
            .FromAssemblyOf<App>()
            .AddClasses(f => f.AssignableToAny(
                    typeof(ContentPage), 
                    typeof(ObservableObject))
                )
                .AsSelf()
                .WithSingletonLifetime()
        );
        // end of important part
        
        builder
            .UseMauiApp<App>().ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
            });
        
#if DEBUG
        builder.Logging.AddDebug();
#endif
        return builder.Build();
    }
}

When we start the application, we should see our MVVM-powered MAUI application.

The advantage to using Scrutor in this instance is that we can continue to expand our application’s functionality with Views and ViewModels. They should all be part of the services collection and work with dependency injection.

I’ve chosen to register all elements as singelton since, in most cases, a mobile application is limited to a single user, and having types registered as scoped or transient is a waste of resources.

I hope you enjoyed this blog post, and please let me know what kind of MAUI apps you’re building. As this is still a burgeoning community and technology, there’s still a lot to learn.

As always, thanks for reading and sharing my posts.