.NET MAUI MVVM

Wednesday, June 8, 2022

So we have a pretty nice start to our app. We can display some data, but that data is not only hard coded, it’s hard coded in the XAML file. There’s a clear separation of concerns when it comes to data and presentation. We should follow that. In this post we’ll focus on a pattern of programming known as Model View ViewModel or MVVM for short. This is in the family of patterns as Model View Controller (MVC) and Model View Presenter (MVP). More specifically it allows us to create a ViewModel that we then bind to our View to display data. The View doesn’t need to know how or where the data comes from, and the ViewModel doesn’t need to care how the data is displayed.

Let’s start by creating a ViewModel. Create a folder called ViewModels and in it create a class called MainViewModel.cs. We name it that because it’s the ViewModel for the Main page. Consistency is king here. Always try to keep naming conventions consistent and you’ll be able to maintain your code better in the future.

There used to be a lot of boilerplate code that we’d have to write to make the ViewModel “active”. What do I mean by “active”? Well when a property changes you want to be notified of that change so the view can update. Also when a command on the View is triggered we want a specific method in the ViewModel to fire. Thankfully there is the Community Toolkit for MVVM available that makes all of this easier. As of this writing the version is 8.0.0-preview4. Open Tools –> NuGet Package Manager –> Manage NuGet Packages for Solution… Then in the tab that opens up select Browse and type CommunityToolkit.Mvvm. If the version is 7.1.2 make sure that the checkbox Include prerelease is checked and install the 8.0 version that comes up. Hopefully it’ll be released software soon, but the preview seems nice and stable for me.

Anyway, back to our new ViewModel. In the class declaration change it to a public partial class instead of an internal class. Then inherit from ObservableObject. This will create some magic for us that I’ll demonstrate in a minute. Next make sure that you add the using CommunityToolkit.Mvvm.ComponentModel; directive at the top. While we’re cleaning things up a bit, if you put a semi-colon after the namespace Visual Studio 2022 will get rid of the brackets and move the class in one tab space. Just something I do, you might like the namespace in a parenthesis.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using maui_to_do.Models;
using System.Collections.ObjectModel;
using System.ComponentModel;

namespace maui_to_do.ViewModels;

public partial class MainViewModel : ObservableObject
{
}

The ViewModel is the model for the view. We want the data that is in there to reflect the data that is displayed to the user. The data that we are showing the user is an array of ActionItem. Let’s add an observable collection of ActionItems to the view model (line 11 below).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using maui_to_do.Models;
using System.Collections.ObjectModel;
using System.ComponentModel;

namespace maui_to_do.ViewModels;

public partial class MainViewModel : ObservableObject
{
    public ObservableCollection<ActionItem> Items { get; } = new();
}

Then let’s create a GetActionItems function and decorate it as a RelayCommand. You will have to pull in using CommunityToolkit.Mvvm.Input for this. Then fill out the rest of the method as follows. This will add our fake ActionItems to the observable collection.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using maui_to_do.Models;
using System.Collections.ObjectModel;
using System.ComponentModel;

namespace maui_to_do.ViewModels;

public partial class MainViewModel : ObservableObject
{
    public ObservableCollection<ActionItem> Items { get; } = new();

    [RelayCommand]
    void GetActionItems()
    {
        if (Items.Count != 0) return;

        Items.Add(new ActionItem
        {
            ID = 0,
            Title = "Install .NET MAUI",
            IsCompleted = true,
        });
        Items.Add(new ActionItem
        {
            ID = 1,
            Title = "Start a Demo App",
            IsCompleted = true,
        });
        Items.Add(new ActionItem
        {
            ID = 2,
            Title = "Make To-Do app",
            IsCompleted = false,
        });
        Items.Add(new ActionItem
        {
            ID = 3,
            Title = "Get To-Do working on Android, iOS, and Mac",
            IsCompleted = false,
        });
    }
}

A quick note about [RelayCommand]: This was simply [ICommand] in previous builds of the Community Toolkit. Why it changed I don’t know. I just know that code that used to use ICommand works with RelayCommand now.

Before we get this running let’s go back to our MainPage.xaml file and remove the CollectionView.ItemsSource and the array it contains. Next, add a new namespace for the view models with xmlns:viewmodels="clr-namespace:maui_to_do.ViewModels" and add a data type for the whole page with: x:DataType="viewmodels:MainViewModel" This will tell the page that all the data comes from a MainViewModel type. Your MainPage.xaml should look like this now:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?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:models="clr-namespace:maui_to_do.Models"
             xmlns:viewmodels="clr-namespace:maui_to_do.ViewModels"
             x:Class="maui_to_do.MainPage"
             x:DataType="viewmodels:MainViewModel">

    <CollectionView ItemsSource="{Binding Items}">
        <CollectionView.ItemTemplate>
            <DataTemplate x:DataType="models:ActionItem">
                <HorizontalStackLayout Padding="10">
                    <CheckBox IsChecked="{Binding IsCompleted}" />
                    <Label VerticalOptions="Center" Text="{Binding Title}" />
                </HorizontalStackLayout>
            </DataTemplate>
        </CollectionView.ItemTemplate>
    </CollectionView>
 
</ContentPage>

This is pretty boring right now because we don’t have a way to trigger the loading of data. Let’s surround the CollectionView with a Grid. The grid will have two rows. The first row will contain a button to load the data and the second will show the data. Your new MainPage.xaml should look like this now:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?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:models="clr-namespace:maui_to_do.Models"
             xmlns:viewmodels="clr-namespace:maui_to_do.ViewModels"
             x:Class="maui_to_do.MainPage"
             x:DataType="viewmodels:MainViewModel">
    <Grid RowDefinitions="auto,*">
        <Button Text="Load Data" Command="{Binding GetActionItemsCommand}" />
        <CollectionView ItemsSource="{Binding Items}" Grid.Row="1">
            <CollectionView.ItemTemplate>
                <DataTemplate x:DataType="models:ActionItem">
                    <HorizontalStackLayout Padding="10">
                        <CheckBox IsChecked="{Binding IsCompleted}" />
                        <Label VerticalOptions="Center" Text="{Binding Title}" />
                    </HorizontalStackLayout>
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>
    </Grid>

</ContentPage>

In the code behind add a using statement to pull in the ViewModels and then in the MainPage constructor add BindingContext = new MainViewModel(); After making this change your MainPage.xaml.cs file will look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
using maui_to_do.ViewModels;

namespace maui_to_do;

public partial class MainPage : ContentPage
{
	public MainPage()
	{
		InitializeComponent();
		BindingContext = new MainViewModel();
	}
}

Run your application. Press the big button to load data and you’ll be presented with almost the same thing we had before.

The new simplified appThe new simplified app

There are a number of problems with this and we’ll tackle them in future blog posts. For now let me know what you think of this series.

.NET MAUIMVVMPatterns

This work is licensed under CC BY-NC-SA 4.0

.NET MAUI Dependency Injection

.NET MAUI Main Page