- •Table of Contents
- •About the Author
- •Acknowledgments
- •Introduction
- •Version Support
- •Supported Versions
- •A Unified Platform
- •Roadmap
- •Supported Operating Systems
- •Command Line Interface
- •Desktop Development
- •Blazor
- •MAUI
- •Wrapping Up
- •.NET 6 Architecture
- •Runtimes
- •CoreCLR
- •Mono
- •WinRT
- •Managed Execution Process
- •Desktop Packs
- •Wrapping Up
- •Dotnet New
- •Dotnet Restore
- •NuGet.config
- •Dotnet Build
- •Dotnet Publish
- •Dotnet Run
- •Dotnet Test
- •Using the CLI in GitHub Actions
- •Other Commands
- •Wrapping Up
- •WinAPI
- •WinForms
- •STAThread
- •WinForms Startup
- •DPI Mode
- •Responding to Scale Events
- •Visual Styles
- •Text Rendering
- •The Message Loop
- •The Form Designer
- •WPF Startup
- •XAML Layout
- •Visual Tree
- •Data Binding
- •Windows App SDK
- •Building a Windows App SDK application
- •Using Windows APIs with Windows App SDK
- •Packaging
- •Migrating to .NET 6
- •Upgrade Assistant
- •Wrapping Up
- •Blazor WebAssembly
- •Creating a Blazor Wasm Project
- •Blazor Progressive Web Apps
- •Exploring the Blazor Client Project
- •Blazor in .NET 6
- •Blazor Component System
- •Creating Blazor Pages
- •Running a Blazor App
- •Blazor Server
- •SignalR
- •Blazor Desktop
- •Wrapping Up
- •Project Structure
- •Exploring MAUI
- •The Cross-Platform World
- •Application Lifecycle
- •MVVM
- •MVVM Toolkit
- •Wrapping Up
- •Model-View-Controller
- •Routing
- •Views
- •Controllers
- •Controller-Based APIs
- •Minimal APIs
- •Wrapping Up
- •Web Apps
- •Creating an App Service
- •Static Web Apps
- •Web App for Containers
- •Docker
- •Azure Functions
- •Deploying Azure Functions
- •Wrapping Up
- •Record Types
- •Monolith Architecture
- •Microservices
- •Container Orchestration
- •Kubernetes
- •Docker Compose
- •Dapr
- •Installing Dapr
- •Dapr State Management
- •Wrapping Up
- •Roslyn
- •Compiler API
- •Diagnostic API
- •Scripting API
- •Workspace API
- •Syntax Tree
- •Roslyn SDK
- •Source Generators
- •Writing a Source Generator
- •Debugging Source Generators
- •Wrapping Up
- •Garbage Collector
- •The Heap
- •The Stack
- •Garbage Collection
- •A Look at the Threadpool
- •Async in .NET 6
- •Await/Async
- •Cancellations
- •WaitAsync
- •Conclusion
- •Index
Chapter 6 MAUI
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.
Listing 6-13. Installing the MVVM Toolkit
Add-Package Microsoft.Toolkit.Mvvm
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.
170
Chapter 6 MAUI
Listing 6-14. The book service used in the example
public interface IBookService
{
Task<BookForList[]> FetchAllBooks(); Task<BookForDetail> FetchBookDetails();
}
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.
Listing 6-15. BooksViewModel
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);
}
}
171
Chapter 6 MAUI
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.
Listing 6-16. Code-behind for BooksPage
public partial class BooksPage : ContentPage
{
public BooksPage(BooksViewModel viewModel)
{
InitializeComponent(); BindingContext = viewModel;
}
}
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.
172
Chapter 6 MAUI
Listing 6-17 shows the XAML for this page.
Listing 6-17. BooksPage XAML code
<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>
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.
173
Chapter 6 MAUI
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.
Listing 6-18. MauiProgram.cs
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();
}
}
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.
174
Chapter 6 MAUI
Listing 6-19. Updated App.xaml.cs
public partial class App : Application
{
public App(BooksPage page)
{
InitializeComponent();
MainPage = page;
}
}
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. |
175