As I mentioned in the previous part of the series, we’re going to structure our application using the MVVM pattern. This pattern is often used in XAML-based frameworks like WPF or Xamarin.Forms. It’s also used in .NET MAUI.
The MVVM acronym stands for:
M – Model (used to define the basic domain classes)
V – View (used to display stuff to the user)
VM – ViewModel (used like a code-behind, but completely decoupled, to provide data for the View).
We’re going to talk about all these elements in this series. But let’s take it slowly.
Well, in .NET MAUI (as well as in the other aforementioned frameworks) you don’t have to use the MVVM pattern in your app. You can put all the logic in the code-behind files and it will still work fine. If your app is small, this will do. But if your app is more complex or you think it might grow in complexity in the future, maintenance problems may become an issue. If you put all the code in the code-behind files, the app will be harder to maintain and test. Besides all the views and pages will be tightly coupled with the logic. MVVM allows you to decouple logic from presentation. As MVVM is used in many modern apps, we’re also going to implement it in our app.
We’ll need some folders to implement the pattern. We already have the Views folder (A). Add two more folders to the root of the project and name them Models (B) and ViewModels (C):
Before we create any models and view models, let’s have a look at how MVVM actually works.
Table of Contents
How MVVM Works
It’s important to understand how the three components, model, view and view model, relate to one another. Have a look at this diagram:
The dashed arrows demonstrate how data and information flows between the components. They go in only one direction, so the view has direct access to the view model and the view model has direct access to the model. The view doesn’t have direct access to the model, though. What does it mean? It means the view can bind to properties in the view model and the view model can use classes defined in the model.
The model knows nothing about the view model and the view model knows nothing about the view. The view model separates the other two components so that the model and the view can be developed independently.
What about the solid arrows? They indicate that there is some data flow in the opposite direction as well. In case of TwoWay
binding or commands (we’re going to talk about commands soon), data flows from the view to the view model and the view model may update the model.
Let’s briefly characterize the three components.
The Three Components of MVVM
The model and the view model are coded in C#. The view is usually coded in XAML, although it can also be coded in C# if this is what you prefer. Anyway, what exactly is each of the three components responsible for?
The model represents the app’s domain model. It contains classes that represent data models and contain business logic.
The view is responsible for the visual representation of data. It defines the structure of a page. It also may contain some logic in the code-behind, although this should be limited to visual behaviors, not the business logic of the app.
The view model implements properties and commands the view can bind to. It also notifies the view if any property changes so that it can update itself. The view model prepares data it takes from the model for the view to consume. It sometimes requires data conversions.
MVVM is pretty flexible. It means you don’t need a model for each and every view model and you don’t need a separate view model for each view. A view model may use multiple models and each model can be used in multiple view models. If all your view needs is a string, you can just define the string in the view model – there’s no need to create a model class for that. Also, if more than one view requires the same data, they can share one view model. Besides, if a view only displays static data that doesn’t require any data binding, there’s no need to create a view model for this view.
In our app, we’re going to be using the .NET Community MVVM Toolkit framework. There are also some other MVVM frameworks. They are used to facilitate our work by, for example, simplifying the implementation of property change notification or handling commands, navigation, dependency injection, etc. But before we start using the toolkit framework, let’s have a look at a basic implementation of the pattern, without any frameworks. This way you will know what is going on behind the scenes and you will be even more grateful for what the framework does for you when we start using it.
Basic MVVM
So, let’s practice the basic, traditional approach on the TestPage
. The TestPage
is already in the Views folder and it’s going to be our view. Let’s simplify the code in the TestPage.xaml file like so:
<?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"
x:Class="Slugrace.Views.TestPage"
Title="TestPage"
Padding="50">
<VerticalStackLayout>
<Label Text="What is your favorite color?" />
<Entry WidthRequest="100" />
<Label Text="" />
</VerticalStackLayout>
</ContentPage>
Here we have an entry and two labels. We don’t actually need a model, but let’s create a view model that will contain the data our TestPage
requires. It definitely requires a string the second label will bind to. In case of the entry there’s two-way binding, so the entry’s Text
property will both consume the string from the view model and update it there.
In the ViewModels folder create a new class and name it TestViewModel
:
Make the class public:
namespace Slugrace.ViewModels;
public class TestViewModel
{
}
As you can see, I’m also using a file-scoped namespace, but this isn’t necessary. The view model is going to contain some data and also notify the view when this data changes. We can achieve that by implementing the INotifyPropertyChanged
interface:
using System.ComponentModel;
namespace Slugrace.ViewModels;
public class TestViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
}
As you can see, the interface provides an event. Let’s create a method that will invoke the event when a property changes:
...
public class TestViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
void OnPropertyChanged(string propertyName) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
To see how it works, let’s create the FavoriteColor
property of type string
that the entry in the view will bind to. We’ll also create a backing field for it. The getter will just return the value of the backing field. The setter will first check if the value really has changed and if so, it will set the new value and call the OnPropertyChanged
method. The method takes the name of the property as an argument:
using System.ComponentModel;
namespace Slugrace.ViewModels;
public class TestViewModel : INotifyPropertyChanged
{
string favoriteColor;
public string FavoriteColor
{
get => favoriteColor;
set
{
if (favoriteColor != value)
{
favoriteColor = value;
OnPropertyChanged(nameof(FavoriteColor));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
void OnPropertyChanged(string propertyName) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
We can simplify the code slightly by using the CallerMemberName
attribute. Then we won’t have to pass the name of the property to the OnPropertyChanged
method:
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Slugrace.ViewModels;
public class TestViewModel : INotifyPropertyChanged
{
...
public string FavoriteColor
{
get => favoriteColor;
set
{
...
OnPropertyChanged();
}
}
}
...
void OnPropertyChanged([CallerMemberName] string propertyName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
The CallerMemberName
attribute can only be applied to parameters with default values, so we set the propertyName
parameter to null
. As you can see, we can now call the OnPropertyChanged
method without the name of the property. This may be useful if there are many properties in the class.
The OnPropertyChange
method will notify the view of the change and the entry’s text will be automatically updated. And vice versa, if we set the Text
property in the entry, it will be automatically updated in the view model due to its two-way binding mode.
But first, we must set the binding context of the view to the view model and bind the entry’s Text
property to the FavoriteColor
property defined in the view model:
<?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:viewmodels="clr-namespace:Slugrace.ViewModels"
x:Class="Slugrace.Views.TestPage"
Title="TestPage"
Padding="50">
<ContentPage.BindingContext>
<viewmodels:TestViewModel />
</ContentPage.BindingContext>
<VerticalStackLayout>
<Label Text="What is your favorite color?" />
<Entry WidthRequest="100" Text="{Binding FavoriteColor}"/>
<Label Text="{Binding FavoriteColor}" />
</VerticalStackLayout>
</ContentPage>
As you can see, I also bound the second label’s Text
property to FavoriteColor
. If you now start entering text into the entry, the FavoriteColor
property in the view model will be updated to the new value. The label will display the new value because it’s bound to it. So, here the data goes from the entry to the view model and then from the view model to the label:
Sometimes one property in the view model depends on another property. Then, when one of them changes, the other should change too. Let’s modify our view model:
...
public class TestViewModel : INotifyPropertyChanged
{
string favoriteColor;
public string FavoriteColor
{
get => favoriteColor;
set
{
if (favoriteColor != value)
{
favoriteColor = value;
OnPropertyChanged();
OnPropertyChanged(nameof(LetterCount));
}
}
}
public int? LetterCount => FavoriteColor?.Length;
public event PropertyChangedEventHandler PropertyChanged;
...
}
Here we have the LetterCount
property that simply returns the length of the string that we enter in the entry. This property will change whenever the FavoriteColor
property changes. We can achieve this by calling the OnPropertyChange
method as many times as there are properties that should be affected. In our case it means twice. We don’t have to pass the name of the property on which the method is called, but we have to provide the names of other properties, which is why we pass LetterCount
to it.
Now let’s add another label in the view and bind it to the newly created property:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...>
...
<VerticalStackLayout>
...
<Label Text="{Binding FavoriteColor}" />
<Label Text="{Binding LetterCount}" />
</VerticalStackLayout>
</ContentPage>
If we now enter some text in the entry, the label will display its length:
Let’s say we want to add a button to our view, which, when pressed, will set the FavoriteColor
property to a fixed value, like for instance “red
”. The problem is, we can’t use events anymore. Events work in the code-behind and we now put the logic in the view model. So, there must be another way to do it. And there is – commands.
Commands
Let’s start by creating a public property of type ICommand
in the view model. This is the property we can bind to. Then, in the constructor, let’s create a new Command
instance and pass to it the method that we want to be invoked when the button is pressed. We don’t have the method yet, so let’s create it. The method is named UseFixedColor
and it just sets the FavoriteColor
property to a fixed value. Here’s the code:
...
public class TestViewModel : INotifyPropertyChanged
{
...
public int? LetterCount => FavoriteColor?.Length;
public ICommand UseColorCommand { get; private set; }
public event PropertyChangedEventHandler PropertyChanged;
public TestViewModel()
{
UseColorCommand = new Command(UseFixedColor);
}
private void UseFixedColor()
{
FavoriteColor = "red";
}
void OnPropertyChanged([CallerMemberName] string propertyName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
Now we have to create the button in the view and bind its Command
property to the property we just created in the view model:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...>
...
<VerticalStackLayout WidthRequest="200">
...
<Label Text="{Binding LetterCount}" />
<Button
Text="RED"
Command="{Binding UseColorCommand}" />
</VerticalStackLayout>
</ContentPage>
That’s it. If you now run the app and press the button, the FavoriteColor
property will change to “red
”. This change will be immediately reflected in all controls in the view that bind to FavoriteColor
:
We can also use the command with a parameter. To this end we use the CommandParameter
property. Let’s add two more buttons that will pass a different color name to the command as an argument:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...>
...
<VerticalStackLayout WidthRequest="200">
...
<Button
Text="RED"
Command="{Binding UseColorCommand}"
CommandParameter="red" />
<Button
Text="BLUE"
Command="{Binding UseColorCommand}"
CommandParameter="blue" />
<Button
Text="YELLOW"
Command="{Binding UseColorCommand}"
CommandParameter="yellow" />
</VerticalStackLayout>
</ContentPage>
As now the method takes a parameter, we have to use the generic version of Command
in the view model with the type corresponding to the type of the parameter. Let’s modify the code like this:
...
public class TestViewModel : INotifyPropertyChanged
{
...
public TestViewModel()
{
UseColorCommand = new Command<string>(UseFixedColor);
}
private void UseFixedColor(string color)
{
FavoriteColor = color;
}
...
}
Now you can press the buttons to change the FavoriteColor
property:
It works fine because we set the binding context to TestViewModel
. Let’s have a closer look at the binding context.
Binding Context
We set the binding context in XAML, but we often do it in the code-behind using dependency injection. In the TestPage.xaml.cs file we’ll set the binding context in the constructor:
using Slugrace.ViewModels;
namespace Slugrace.Views;
public partial class TestPage : ContentPage
{
public TestPage(TestViewModel testViewModel)
{
InitializeComponent();
BindingContext = testViewModel;
}
}
We’re using here an instance of TestViewModel
through dependency injection. We need to register the page and the view model with the dependency service in the MauiProgram.cs file:
using Microsoft.Extensions.Logging;
using Slugrace.Views;
using Slugrace.ViewModels;
namespace Slugrace;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
...
#if DEBUG
builder.Logging.AddDebug();
#endif
builder.Services.AddSingleton<TestPage>();
builder.Services.AddSingleton<TestViewModel>();
return builder.Build();
}
}
We’re using the AddSingleton
method here, which means the same instance will be used throughout the application. If we used AddTransient
instead, a new instance of TestPage
and TestViewModel
would be delivered whenever requested. It’s important to register the classes for dependency injection before the Build
method on builder
is called.
Finally, remove the part of the markup in the TestPage.xaml file where the binding context was set. I mean the following part:
<ContentPage.BindingContext>
<viewmodels:TestViewModel />
</ContentPage.BindingContext>
We don’t need it anymore because now the binding context is set in the code-behind.
The app works as before, but we have a problem with Intellisense in the XAML file. It looks like the properties defined in the view model are not listed in it. We can fix it by setting the x:DataType
property on the page to the view model class:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...
xmlns:viewmodels="clr-namespace:Slugrace.ViewModels"
x:DataType="viewmodels:TestViewModel"
x:Class="Slugrace.Views.TestPage"
...
Now Intellisense works as before.
This is it as far as the basics of MVVM are concerned. We could implement our Slugrace application in the same way – just add view models and models to the already existing views. However, if you look at the TestViewModel
class, you will see that we need quite a lot of code to implement a single property. We also have to implement the INotifyPropertyChanged
interface for each view model.
We could simplify it a bit by creating a base view model where the interface would be implemented and then deriving the other view models from this base class. But there is even a better solution. We can use a framework that will do some of the tedious work for us. I already mentioned the .NET Community MVVM Toolkit framework that we are going to use. It relies on source generators, so it will generate optimized C# code with all the required functionality for you. We’re going to implement it in the next part of the series. We’ll first use it for the TestPage
so that you can see the difference and then we’ll start implementing it for the actual application.