MAUI, or the Multi-Application User Interface, is the next iteration of Microsoft’s Xamarin framework. Xamarin has come a long way since its startup days back in 2011. It has evolved from being a C# wrapper around Java and Objective-C to a world-class cross-platform library, enabling mobile developers to write apps for multiple platforms in the same language, without ever having to switch back to the platform’s native language. All of this was, thanks to Mono, an open-source implementation of .NET. Mono’s original reason of existence was bringing .NET to the Linux world, enabling Mono developers to build Linux-based desktop applications with C# and Visual Basic. Mono quickly evolved to a platform that brings .NET to a wide range of architectures and operating systems, even into the world of embedded systems. Since Android and iOS are *Nix-based operating systems, it wasn’t that far-fetched to get a version of Mono running on these mobile platforms, and thus Xamarin was born.
In 2014 Xamarin released Xamarin.Forms, which adds an extra UI abstraction layer on top of Xamarin. It further abstracts platform differences by enabling developers to write a UI once and run it on different platforms.
Microsoft acquired Xamarin in 2016; they made the framework license free and even gave it a more open open-source license. The Xamarin community has grown large since then. Since the acquisition focus has shifted from more Xamarin Native to Xamarin Forms, pushing XAML as the UI framework of choice. A lot of work was done to improve performance and stability. The tooling greatly increased, bringing us a UI Previewer and telemetry tools.
With MAUI, a new chapter in the life of Xamarin begins. MAUI will aim to shorten the developer loop (develop–build–run) and greatly improve the tooling. MAUI can target Android, iOS, Windows, and MacOS.
Project Structure
A newly created Xamarin.Forms app that can run on Android and iOS consists of three projects: a Xamarin.Android project, a Xamarin.iOS project, and a .NET class library. The Android and iOS project are what we call the “platform heads”; they exist mainly to bootstrap the Xamarin.Forms platform and call into the shared code that lives in the .NET class library, XamarinDemo.Shared. The two platform heads are also used whenever an app needs platform-specific implementations; this might be needed if the cross-platform layer doesn’t suffice.
Exploring MAUI
The contents of this folder should look familiar if you have done Xamarin Forms work before. It looks similar to the contents of the iOS project in a Xamarin.Forms project; it also serves the exact same purpose. Program.cs is the launch class of iOS in this case. It creates a UIApplication instance passing AppDelegate as startup class. These classes are .NET wrappers around the native iOS APIs.
As I’ve mentioned before, the single project system is mostly compiler tricks, which isn’t a bad thing; not having a project for every supported platform greatly simplifies things and makes it easier to maintain an overview once a project grows to a certain size.
The starting point of a MAUI iOS application
iOS AppDelegate.cs
Android MainApplication.cs
The base class is a different, Android-specific, one; and there is a constructor. That constructor is needed for Android to successfully launch the application. But we do see the same call to MauiProgram.CreateMauiApp.
All our target platforms call MauiProgram.CreateMauiApp() in their startup. This method initializes the cross-platform part of MAUI, while the platform-specific class initializes and bootstraps the application according to the platform. These startup classes can be found in their respective folder in the Platforms folder of your project. For Android, it’s called MainApplication, while for iOS and MacOS it’s AppDelegate, and for Windows it’s App.xaml.cs.
All of these platform-specific classes inherit from a platform-specific Maui base class. This class is where FinishedLaunching moved to. Once the OS has bootstrapped and launched the app, this method fires and initializes the MAUI context. Besides FinishedLaunching, this class also handles all lifecycle events, for example, when the app is activated, moved to the background, terminated, and so on. We’ll discuss app lifecycle more a bit further in this chapter.
The Cross-Platform World
MauiProgram Startup class
The Application class
This is the part where we finally get into our own code. Everything is set up and bootstrapped, ready to go. InitializeComponent starts building the visual tree; next we instantiate a new instance of MainPage and set that as the current page. The Application base class contains a MainPage property; whatever page is set to that property is the one shown on screen. Things might get a bit confusing here since we are setting a property called MainPage with an instance of a class called MainPage.
Application Lifecycle
Applications go through different stages while running, stages like starting, stopping, sleeping, and resuming. These differ depending on the platform your app is running on. A mobile application will go to sleep once it’s moved to the background to preserve battery; a WPF application, for example, will keep running even when minimized, choosing efficiency over battery life.
Why is this important to know, and use, as a developer? Consider you’re building a mobile app. The app contains a registration form; one of your users is filling out the form and suddenly gets a call before being able to press the save button. A call pushes your app to the background, giving priority to the phone app. If this is a high-end device, the app might keep its data in the background, but the mobile operating system might decide that the app is taking up too many resources in the background and to remove it from memory while still keeping it in the open apps list. That’s why, as a developer, you need to hook into the pausing event. We can use this event to temporarily store data, like the data already entered in the form, and resume that data when the resuming event fires.
Lifecycle events in MAUI are abstracted away from the platform. That means that there is a common API that will work over all platforms, but they have a different implementation under the hood. It also means that the common API is built mostly on top of structures that are available across all platforms. It’s possible to break out of the shared layer and go into platform-specific code, should you need more fine-grained control of the lifecycle on a specific platform.
Lifecycle events on application level
Lifecycle event | Description |
---|---|
Creating | Fires after the operating system started to create the application in memory |
Created | Fires when the application is created in memory, but before anything is rendered on-screen |
Resuming | Resuming can fire on two occasions: after the Created event or when returning to the app from the background |
Resumed | Current window has finished rendering; app is available for use |
Pausing | Fires when the application is going into background mode |
Paused | Fires when the application has gone into background mode |
Stopping | Fires when a user closed the app |
Stopped | Fires when the app finished closing |
Lifecycle events on window level
Lifecycle event | Description |
---|---|
Creating | Fires after the application has been created, but before the application window is created |
Created | Fires after the application’s native window has been created. The cross-platform window is available after this event, but nothing is rendering yet |
Resuming | A bubble up event from Application.Resuming, giving us access to the resuming event from within a window, enabling specific actions per window |
Resumed | A bubble up event from Application.Resumed, giving us access to the resumed event from within a window, enabling specific actions per window. Also fires whenever a window is being maximized after being minimized on desktop platforms |
Pausing | A bubble up event from Application.Pausing, giving us access to the pausing event from within a window, enabling specific actions per window |
Paused | A bubble up event from Application.Paused, giving us access to the paused event from within a window, enabling specific actions per window. Also fires whenever a window is being on desktop platforms |
Stopping | Fires when a window is closing |
Stopped | Fires when a window is closed |
Lifecycle events on page level
Lifecycle event | Description |
---|---|
NavigatingTo | Fires when a page is going to be navigated to and after NavigatingFrom |
NavigatedTo | Fires when a page has been navigated to through a NavigationPage element |
NavigatingFrom | Fires right before the NavigatingTo event |
NavigatedFrom | Fires after NavigatingTo |
Lifecycle events on view level
Lifecycle event | Description |
---|---|
AttachingHandler | Fires before a view is created that attaches to the native handler |
AttachedHandler | Fires after the native handler has set the view. After this all properties are initialized and ready for use |
DetachingHandler | Fires before a view is being detached from a native platform handler |
DetachedHandler | Fires after a view has been removed from the native handler |
AttachingParent | Fires when a view is about to get connected to a cross-platform visual tree |
AttachedParent | Fires when a parent is set on this view |
DetachingParent | Fires when a parent is about to be removed from the view |
DetachedParent | Fires when a parent is removed from the view |
MVVM
Xamarin.Forms brought XAML into the cross-platform mobile world as an abstraction layer on top of the supported platforms their own UI stack. It’s an XML-based layout and style engine that transform into platform-native elements at compile time by default. Just like the WPF XAML stack, it supports databinding, templating, resource dictionaries, and so on. This book is not a XAML guide, but MAUI has made some changes in the available design patterns for writing cross-platform apps, so I’ll provide a high-level overview.
Model-View-ViewModel (MVVM) was introduced in 2005 by Microsoft architects Ken Cooper and Ted Peters. They developed a pattern that leveraged databinding to decouple data and logic from the view so that the view could be developed separately from the logic and so that the logic could be unit-tested without creating the entire visual tree.
Model: the domain model
View: the XAML pages that contain the visual aspects of the application
ViewModel: the properties and commands that will be used on the view
Setting a BindingContext on a MAUI ContentPage
From this moment on, all bindings that do not specify another BindingContext will turn to the MainViewModel class to resolve their bindings when requested.
Databinding in MAUI with XAML is the same as with Xamarin.Forms and quite similar to databinding in WPF (or Silverlight if you want to go way back). Let’s look at an example using a ViewModel.
MainViewModel
Let’s start with Count; Count is a basic auto-property with a private set, nothing special there so far. It’s a public property which means it’s available to bind to. The datatype here is integer, but it can be anything, even complex objects. IncreaseCountCommand is an ICommand which is an abstraction of reacting on user actions. ICommand is an interface from the System.Windows namespace. It’s used for exactly this use case in WPF, UWP, and every other XAML-based framework. The Command implementation however lives in the Microsoft.Maui assemblies. This way Microsoft is giving us a way to use familiar concepts, while under the covers, it is a brand new implementation. In this case, we will attach the IncreaseCountCommand to a button tap in a minute. We’re using C# 8’s null-coalescing assignment feature to assign an ICommand implementation. This means that when the property’s getter is called, it will check for null and first assign the value if it is null; if not, it will return the value.
The same MainViewModel but with command initialization done in the constructor
This works just the same, so where’s the difference? When binding to an ICommand, the getter is only being called when the command is triggered. Meaning that a page will load, but the IncreaseCountCommand will not be initialized yet as long as the user does not tap the button. On a page with a lot of commands, this shaves of precious time of the viewmodel initialization; we’re basically deferring initializing each command until it’s needed for the first time.
Cleaned up MainPage
There are some parts we still need of course. Notice that this class is partial; the other part of the partial class is the XAML file itself. The XAML compiler transforms that XAML code into a partial C# class.
Do not remove InitializeComponent; this method call triggers the creation of all elements on page. This should typically be the first call in the constructor of a page. After that, we set the BindingContext; as mentioned before, this will bubble down to all elements on that page unless we specifically assign another BindingContext to an element.
Label with binding
Binding a command to a button
For the button, we bind to its Command property. When the element’s default action (Click or Tap in case of a button) is triggered, the system will try to cast the object bound to Command to an ICommand and trigger its Execute method. Note that it is possible to assign both a click event handler and a command to a button; both will fire if the button is tapped.
Implementing INotifyPropertyChanged
MVVM Toolkit
Installing the MVVM Toolkit
The MVVM Toolkit is based on MVVM Light by Laurent Bugnion. There’s been quite some renaming and rethinking of what the API surface of the toolkit looks like. Let’s look at a simple example: a master-detail app that lists some Apress books.
The book service used in the example
BooksViewModel
This viewmodel is using the MVVM Toolkit we have just installed. It inherits from ObservableObject, which is a base class that already implements INotifyPropertyChanged and gives us some helper methods to keep the code in our own viewmodel smaller. An example of this is the SetProperty method used in the Books property’s setter. This method will check if the new value is different from the old one, set the new value, and fire the PropertyChanged event, something we did manually in the MainViewModel.
Few things to note in this class. We are using an ObservableCollection. This type of collection will fire a CollectionChanged event whenever an item is added or removed from the list. UI elements like a CollectionView in MAUI listen to this event and update their list whenever it fires.
In the constructor of the viewmodel, we see _ = LoadBooks(). This underscore is called a discardable in C#. Discardables are throw-away variables that you can assign a value to that you will never use. I am using a discardable here because we are calling an async method from the constructor. We cannot await it so the compiler will complain that we are not awaiting an async operation. By assigning the task to a discardable we clear that compiler warning.
Code-behind for BooksPage
The code-behind is quite empty. The most noteworthy thing here is that we are receiving our viewmodel through the constructor and setting it to the BindingContext. This will all get wired up through dependency injection in a minute.
BooksPage XAML code
We are using a CollectionView in this page. In MAUI a CollectionView is an element that can show collections of data. By default, this is in a vertical list, but this can be changed to a horizontal list or even a grid layout. More information on the CollectionView can be found at https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/collectionview/.
A CollectionView needs a template to know what an item in its collection should look like visually. An ItemTemplate is a property of type DataTemplate. This property contains a XAML snippet that gets inflated for every object in the collection. The bindings in the template have a single collection item as bindingcontext. The template is a grid with two rows; the first row has a height of 30 pixels; this will contain the book’s title. The second row has a height of auto, meaning it will calculate its size based on its contents, in this case a label containing the author’s name. The first label in the template does not have a property set to place it in a specific grid row, meaning it will default to the first row, or row index 0. The second one is using what is called an attached property to place it in the second row, or row index 1. Attached properties are properties that belong to a parent element but are set on a child.
MauiProgram.cs
We have seen this class before, but now we have added some services. We are registering the pages, viewmodels, and services as singletons in our dependency injection system. Don’t forget to add the Microsoft.Extensions.DependencyInjection namespace to get access to the Add* methods.
We are registering pages, viewmodels, and services as singletons, meaning that we will use one instance of each over the lifetime of the app.
Updated App.xaml.cs
The change we made here is that we are injecting the page we want to use as startup page in the constructor. That page is set to the MainPage property.
The platform native application starts.
MAUI gets bootstrapped.
Pages, viewmodels, and services are registered.
BooksPage gets created to inject in App.xaml.cs.
BooksPage needs a BooksViewModel in its constructor so that is instantiated.
BooksViewModel needs an IBookService in its constructor; IBookService is known in our DI system as the interface for BookService, so a new BookService is created and injected.
The page loads and bindings are resolved on the viewmodel.
Wrapping Up
MAUI is the next iteration of Xamarin, Microsoft’s native cross-platform framework. As we have seen in this chapter, Microsoft is doing a lot of work to have a similar way of working across all of .NET 6. This is clearly visible in the startup class when comparing MauiProgram with Program in ASP.NET. Similar startup structure, the same built-in dependency injection with ServiceCollection, and so on.
Next to unifying the way applications load across .NET, they have also greatly simplified the project structure when compared with Xamarin Forms. Instead of three projects in a brand new solution, we now have a single project structure. In that structure, we have platform-specific folders for all supported platforms, iOS, Android, MacOS through MacCatalyst, and Windows through WinUI 3.