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:
- Your ViewModels are much more straightforward to test, with the bulk of your logic exposed through properties and commands.
- Your Views are simpler, binding to properties and commands rather than directly to members on the specific
ContentPage
. - 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.