© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
N. VermeirIntroducing .NET 6https://doi.org/10.1007/978-1-4842-7319-7_6

6. MAUI

Nico Vermeir1  
(1)
Merchtem, Belgium
 

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

The most prominent change is the new project structure. A new Xamarin.Forms solution in Visual Studio already contained three projects before any code was written, as shown in Figure 6-1.
Figure 6-1

A classic Xamarin Forms project

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.

Figure 6-2 shows a newly created MAUI application using The Blank MAUI app template in Visual Studio in .NET 6.
Figure 6-2

A new .NET 6 MAUI project

This app supports Android, iOS, Windows, and Mac Catalyst. You’ll notice that the multiple platform heads are gone; instead, we get one class library that contains a Platforms folder containing a folder for each platform we’re supporting with our app. This is mostly done through compiler magic; we still get an APK file for Android and an IPA for iOS, but our code is easier to manage since it’s all in one place. Selecting which platform to launch or debug on is built into the new Visual Studio tooling, as shown in Figure 6-3.
Figure 6-3

Selecting the debug target

Figure 6-4 shows the newly created app running on an Android and Windows device.
Figure 6-4

MAUI App running on Android

Exploring MAUI

The startup of a MAUI project looks quite similar to how ASP.net has been operating ever since .NET Core. It uses a Microsoft.Extensions.HostBuilder to bootstrap and launch an app. However, this is where the multiplatform part starts; just like with Xamarin before, we first need to launch the actual application on the operating system that we’re targeting; that’s what the platform-specific folders are for. Let’s take the iOS folder as an example.
Figure 6-5

The iOS platform folder

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.

A Maui.iOS application starts with Program.cs
public class Program
{
      // This is the main entry point of the application.
      static void Main(string[] args)
      {
            UIApplication.Main(args, null, typeof(AppDelegate));
      }
}
Listing 6-1

The starting point of a MAUI iOS application

Nothing exciting going on here, just a basic .NET application starting point. It calls into UIApplication.Main to start the application loop. The parameters passed into this method determine the actual starting point of the iOS application, AppDelegate in this case. Listing 6-2 shows the content of AppDelegate.cs.
[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
      protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}
Listing 6-2

iOS AppDelegate.cs

This is where things are starting to really get different from the classic Xamarin way of working. Instead of overriding the FinishedLaunching method and calling into Xamarin.Forms, from there we make sure that AppDelegate inherits from MauiUIApplicationDelegate and calls CreateMauiApp on the MauiProgram class included in the project. The other platforms work in a very similar way. Listing 6-3 shows the corresponding Android file.
[Application]
public class MainApplication : MauiApplication
{
      public MainApplication(IntPtr handle, JniHandleOwnership ownership)
            : base(handle, ownership)
      {
      }
      protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}
Listing 6-3

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

Once the operating system has bootstrapped our app, we enter MAUI cross-platform space. MauiProgram.cs creates the MAUI context in a way which should look familiar; it’s very similar to the Program.cs we have seen in other frameworks within .NET 6.
public static class MauiProgram
{
      public static MauiApp CreateMauiApp()
      {
            var builder = MauiApp.CreateBuilder();
            builder
                  .UseMauiApp<App>()
                  .ConfigureFonts(fonts =>
                  {
                        fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                  });
            return builder.Build();
      }
}
Listing 6-4

MauiProgram Startup class

The CreateMauiApp method creates a builder. With that builder, we can set our startup object to the App class; keep in mind that at the point we reach this code, our application is already launched. We are now setting up the cross-platform world. We can use the builder for several other things like configuring external fonts that are packaged with the project, for example. Finally we call the Build method on the builder. From that moment on, control will be passed to the class defined as startup. Listing 6-5 shows the App class.
public partial class App : Application
{
      public App()
      {
            InitializeComponent();
            MainPage = new MainPage();
      }
}
Listing 6-5

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.

There are a number of lifecycle events available, on different parts of an application. The following tables Lifecycle events on application level (Table 6-1), Lifecycle events on window level (Table 6-2), Lifecycle events on page level (Table 6-3), and Lifecycle events on view level (Table 6-4) provide a complete list of the available lifecycle events at the time of writing.
Table 6-1

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

Table 6-2

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

Table 6-3

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

Table 6-4

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.

MVVM consists of three layers:
  • 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

The most important part is the ViewModel; you can compare it to a controller in MVC, but stateful. A ViewModel for a specific view gathers all the data the view needs, shapes it into bindable properties, and makes sure to react to property changes where needed. A view in MAUI has a BindingContext property which needs to contain a reference to the correct ViewModel, as shown in Listing 6-6.
public MainPage()
{
    InitializeComponent();
    BindingContext = new MainViewModel();
}
Listing 6-6

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.

I’ve created a new project using the default MAUI XAML template. I’ve added a ViewModels folder and created a MainViewModel inside that folder. Listing 6-7 shows the created MainViewModel.
public class MainViewModel
{
    private ICommand _increaseCountCommand;
    public int Count { get; private set; }
    public ICommand IncreaseCountCommand => _increaseCountCommand ??= new Command(IncreaseCount);
    private void IncreaseCount()
    {
        Count++;
    }
}
Listing 6-7

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.

We could also initialize the command using the constructor as shown in Listing 6-8.
public class MainViewModel
{
    public int Count { get; private set; }
    public ICommand IncreaseCountCommand { get; private set; }
    public MainViewModel()
    {
        IncreaseCountCommand = new Command(IncreaseCount);
    }
    private void IncreaseCount()
    {
        Count++;
    }
}
Listing 6-8

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.

In MainPage.xaml, we’ll transform the basic example from the MAUI template into an MVVM example. We’ll start by removing most of the code in MainPage.xaml.cs, as that logic now lives in the MainViewModel.
public partial class MainPage : ContentPage
{
      public MainPage()
      {
            InitializeComponent();
            BindingContext = new MainViewModel();
      }
}
Listing 6-9

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.

In XAML, we need to change the label and the button to bind to the properties on our ViewModel. Listing 6-10 shows the label.
<Label
      Text="{Binding Count}"
      Grid.Row="2"
      FontSize="18"
      FontAttributes="Bold"
      x:Name="CounterLabel"
      HorizontalOptions="Center" />
Listing 6-10

Label with binding

Let’s zoom into the binding; all other properties speak for themselves. A binding statement is always set between squiggly brackets. We use the Binding keyword to tell MAUI that we want to dynamically bind to a property on the BindingContext. Keep in mind that there is no IntelliSense here because we’ve set the BindingContext in C# land; the XAML editor does not know this. Since we’re compiling our XAML code, we do get compile-time errors on typos. So, by putting Text="{Binding Count}" we’re telling MAUI that we want the value of BindingContext.BindingProperty or in this case MainViewModel.Count to be put into the Text property of this label. Listing 6-11 shows the bindings for the Button.
<Button
    Text="Click me"
    Command="{Binding IncreaseCountCommand}"
    FontAttributes="Bold"
    Grid.Row="3"
    SemanticProperties.Hint="Counts the number of times you click"
    HorizontalOptions="Center" />
Listing 6-11

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.

Let’s try it out! Launch the app and you should see something similar to Figure 6-6 (your version might look different depending on the platform you’re targeting).
Figure 6-6

Running the MVVM-based app

Notice the “0” on top of the page? That’s our databinding at work; the Count property has a default value of 0, and thanks to databinding, we see that value reflected on page. Click the button and you’ll see that… nothing happens? Have we made a mistake? If you put a breakpoint in the IncreaseCount method in the MainViewModel, you’ll see that it hits whenever we click the button, and the integer’s value does increase, so why doesn’t the new value show on the page? The answer is simple: the databinding system does not constantly monitor its BindingContext for changes; this would simply cost too many resources. Instead it trusts us as developers to tell it when a value changes so that it can refresh that property; this is done by using the InotifyPropertyChanged interface. We can implement this interface on our MainViewModel as shown in Listing 6-12.
Public class MainViewModel : InotifyPropertyChanged
{
    private int _count;
    private Icommand _increaseCountCommand;
    public int Count
    {
        get => _count;
        set
        {
            if (_count == value)
                return;
            _count = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count)));
        }
    }
    public Icommand IncreaseCountCommand => _increaseCountCommand ??= new Command(IncreaseCount);
    private void IncreaseCount()
    {
        Count++;
    }
    public event PropertyChangedEventHandler PropertyChanged;
}
Listing 6-12

Implementing INotifyPropertyChanged

INotifyPropertyChanged has one member, an event called PropertyChanged. That event takes the name of the property that just changed as a string parameter. We are using the nameof() method to prevent typing errors in magic strings. The nameof method gets replaced by the actual name of the member we pass in at compile time, so using this has no impact on the runtime performance. The binding framework listens for this event, takes the name of the property that just changed, looks for it in its bindings, and refreshes that specific binding. Triggering that event can happen wherever you need it; often it happens in a property’s setter, like in this example. In the setter of Count, we first make sure that there really is a new value, to prevent unnecessary events firing. If the value really did change, we set the new value to the backing field of the property and trigger the event by calling its Invoke method. With this code in place, if we run the app again and tap the button, you will see the UI update whenever the property updates.
Figure 6-7

UI is updating

MVVM Toolkit

MVVM can get a bit tedious to set up, implementing INotifyPropertyChanged everywhere and so on. To make this experience a bit simpler, Microsoft has released an MVVM Toolkit as part of its Windows Community Toolkit. The toolkit can be installed from NuGet.
Add-Package Microsoft.Toolkit.Mvvm
Listing 6-13

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.

For the sake of the example, I’ve created a service that fetches a list of five books. The implementation of the service goes beyond the scope of this example; feel free to have a look in the source code that accompanies this book, but it’s mostly hard coded data. The interface is shows in Listing 6-14.
public interface IBookService
{
    Task<BookForList[]> FetchAllBooks();
    Task<BookForDetail> FetchBookDetails();
}
Listing 6-14

The book service used in the example

We will use this sample service to build a viewmodel-backed page. We will start by creating the page. Add a new class to the ViewModels folder called BooksViewModel . Listing 6-15 shows the viewmodel.
public class BooksViewModel : ObservableObject
{
    private readonly IBookService _bookService;
    private ObservableCollection<BookForList> _books;
    public ObservableCollection<BookForList> Books
    {
        get => _books;
        set => SetProperty(ref _books, value);
    }
    public BooksViewModel(IBookService bookService)
    {
        _bookService = bookService;
        _ = LoadBooks();
    }
    private async Task LoadBooks()
    {
        BookForList[] books = await _bookService.FetchAllBooks();
        Books = new ObservableCollection<BookForList>(books);
    }
}
Listing 6-15

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.

Moving on to the view. Add a new ContentPage to the project. Listing 6-16 shows the code-behind.
public partial class BooksPage : ContentPage
{
    public BooksPage(BooksViewModel viewModel)
    {
        InitializeComponent();
        BindingContext = viewModel;
    }
}
Listing 6-16

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.

Listing 6-17 shows the XAML for this page.
<ContentPage x:Class="MauiDemo.BooksPage"
       xmlns:="http://schemas.microsoft.com/dotnet/2021/maui"
       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
    <CollectionView ItemsSource="{Binding Books}">
        <CollectionView.ItemTemplate>
            <DataTemplate>
                <Grid RowDefinitions="30, auto">
                    <Label FontSize="24" Text="{Binding Name}" />
                    <Label Grid.Row="1"
                           Text="{Binding Author}"
                           TextColor="Gray" />
                </Grid>
            </DataTemplate>
        </CollectionView.ItemTemplate>
    </CollectionView>
</ContentPage>
Listing 6-17

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.

Final piece of the puzzle is wiring everything up using the built-in dependency injection. We do this in MauiProgram.cs. Listing 6-18 shows its contents.
public static class MauiProgram
{
      public static MauiApp CreateMauiApp()
      {
            var builder = MauiApp.CreateBuilder();
            builder
                  .UseMauiApp<App>()
                  .ConfigureFonts(fonts =>
                  {
                        fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                  });
            builder.Services.AddSingleton<BooksPage>();
            builder.Services.AddSingleton<BooksViewModel>();
            builder.Services.AddSingleton<IBookService, BookService>();
            return builder.Build();
      }
}
Listing 6-18

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.

The final step is changing App.xaml.cs to make use of dependency injection as well. Listing 6-19 shows the updated class.
public partial class App : Application
{
      public App(BooksPage page)
      {
            InitializeComponent();
            MainPage = page;
      }
}
Listing 6-19

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.

Let’s list what will happen in order.
  • 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.

Figure 6-8 shows the result when running on Windows.
Figure 6-8

Running MAUI MVVM application

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.225.95.248