Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
vermeir_nico_introducing_net_6_getting_started_with_blazor_m.pdf
Скачиваний:
19
Добавлен:
26.06.2023
Размер:
11.64 Mб
Скачать

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