Skip to content
Home » Basics of .NET MAUI – Part 18 – The MVVM Toolkit

Basics of .NET MAUI – Part 18 – The MVVM Toolkit

Spread the love

In the previous part of the series we introduced the MVVM pattern in its traditional form. As you remember, there are three components, the model, the view, and the view model. The view model delivers data to the view, so whenever a property defined in the view model changes, the view is notified of this change and updates itself. With two-way bindings it works also in the opposite direction. The model, on the other hand, contains the domain classes that are used by the view model and is not required at all if only simple data is supposed to be displayed.

We used the TestPage as our view and we created the TestViewModel as the view model. We only defined two properties in the view model and we implemented the INotifyChanged interface.

One of the properties we defined is a one-liner that depends on the other property, but even so there’s quite a lot of typing. We’re going to have much more properties in our app, so let’s make our lives easier by using an MVVM framework that will do some of the tedious work for us. In this series, we’ll be using the MVVM Toolkit, which is part of the .NET Community Toolkit. It uses source generators to generate optimized C# code that is additive to our own code. This just means that the view model class we create consists of two parts – the code we write ourselves and the code generated by the source generators. As such, the class must be marked as partial.

So, without further ado, let’s jump right in and install the framework.

Installing the MVVM Toolkit

To install the MVVM Toolkit, right-click on the project and select Manage NuGet Packages. In the Browse tab search for CommunityToolkit.Mvvm and install it.

If you open the project file (by double-clicking on the project name), you will see the toolkit there:

Naturally, depending on when you install it, you may end up with a different version of the package.

Implementing the MVVM Toolkit

Before we start implementing the MVVM Toolkit, let’s have another look at what the TestViewModel class as it looks right now:

We need 13 lines of code to implement the FavoriteColor property. But not if we use the MVVM Toolkit. Let’s actually implement the framework and see the difference.

So, first let’s mark the class as partial and make it inherit from ObservableObject. Also, we don’t need to implement the INotifyPropertyChanged interface anymore because it will be implemented for us by the ObservableObject. If you right-click on it and select Go To Definition, you’ll see it implements the interface:

So, here’s the first part of the class:

Remove the PropertyChangedEventHandler and the OnPropertyChanged method. Next, remove the public FavoriteColor property and add the ObservableProperty attribute to the private backing field. This will cause the source generators to implement the public property for us. Have a look at the code:

We’re not defining the FavoriteColor property anymore, and yet, there’s no error in the code below where we define the LetterCount property, which in turn uses the FavoriteColor property. This is because the public FavoriteColor property is generated for us behind the scenes and its name is automatically capitalized, so we can use it as if we had defined it ourselves.

But don’t take my word for it. You can see the generated code.

Go to Dependencies, select one of the platforms, for instance Android, and under:

Analyzers

-> CommunityToolkit.Mvvm.SourceGenerators

-> CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator

you’ll find the Slugrace.ViewModels.TestViewModel.g.cs generated file:

Open the file and you will see that the public property is indeed created for us:

So, the public property exists and we can still bind to it in the view. If you run the app, it will still work:

But, as you can see, the length of the string is not displayed anymore, even though we defined the LetterCount property ourselves. This is because, as you may remember, the original version of the FavoriteColor property called the OnPropertyChanged method not only on itself, but also on the LetterCount property. This is now gone. But don’t worry, we just have to tell the generator to generate this functionality for us. To this end we have to add another attribute to the backing field, NotifyPropertyChangedFor, and specify the name of the property we want it to work for:

Now everything should work as before.

We’re using commands to call methods defined in the view model. These are built-in .NET MAUI commands, but we can use MVVM Toolkit commands instead.

MVVM Toolkit Commands

By using the MVVM Toolkit commands, we can leverage source generators to further reduce our code. We don’t have to create the UseColorCommand in the view model anymore and we don’t have to instantiate it in the constructor. Instead, we just have to add the RelayCommand attribute to the method that is supposed to be called when the button is clicked. Now the code should look like this:

If you open the file with the generated code (the file ends with RelayCommandGenerator), you’ll see the command has been generated for us:

As you can see, the command is named after the method. We have to use this name, UseFixedColorCommand, in the view:

The code in the view model is now much more concise. And, what’s important, the app works just like before. Try it out.

Now, we’ve covered the most important stuff related to the MVVM Toolkit. Naturally, there’s much more to it, but this is enough to start implementing it in our application. We’ll be creating the models and view models progressively, adding properties and command as we proceed and as we need them. And we’ll be modifying the views accordingly. Let’s start by adding some models.

Project Upgrade to .NET 8

Before we start implementing the MVVM pattern in our Slugrace application, let’s upgrade it to the latest version of the framework. It so happened that .NET 8 was just released on November 14th, 2023, so why not use the latest changes and bug fixes? Yes, there is a bug in our app, which you may have noticed when running the app on Windows. In the SettingsPage there are two groups of radio buttons, so it should be possible to check one radio button in each group. However, if you set the IsChecked property of one button in each group to True, only the last radio button will be checked. It works correctly on Android, but our app will be deployed to Windows as well, so it’s worth fixing.

OK, so how do you upgrade your project? First of all, you have to download and install the framework from the official Microsoft website. Then, double click the name of the project (Slugrace) in Visual Studio:

Save the file. After a while the dependencies will be updated:

In order for this to work correctly on Android, I also added the following ItemGroup:

That’s it. The app now works correctly on both Windows and Android.

Models in the Slugrace Application

Our application is a 2D game. There are players who play the game and place bets on slugs. So, we’re going to need three model classes: Player, Slug and Game.

The Player class will contain properties and methods related to the player, so a human being playing the game. Properties like Name, InitialMoney, CurrentMoney, etc. probably come to your mind. A player must also be able to place a specified amount of money on a slug, etc.

The Slug class will contain properties and methods related to the slug. Each slug needs a name, runs at a specified speed, can win or lose a race.

These are just some examples. In fact, our models are going to need more properties and methods as we proceed, but we’ll be updating the models as we go. For now, let’s create the three classes in the Models folder and add some basic properties to them.

So, the Player.cs file looks like this:

For now, we just have a bunch of properties with self-explanatory names.

The Slug.cs file looks like this:

Yes, I know, this isn’t much, but don’t worry, we’ll add quite a few properties and methods in this class. All in due time.

And here’s the Game.cs file:

We’re keeping it simple for now. I defined an enumeration here that will be used only in this class and later in the view model. By default the game is supposed to end when there’s only one player with any money left or when there’s none.

Let’s now move on to views and view models. When we were creating the pages, we included content views in some of them. We’ll create a separate view model for the PlayerSettings content view, but generally we’ll create a separate view model for each page. Actually, some of the pages will share the same view model. Anyway, let’s start with the PlayerSettings content view.

PlayerSettings View and View Model

There is one content view embedded in the SettingsPage, PlayerSettigs. It represents a single player. There may be between one and four players in the game. There always is at least one. In the SettingsPage there will always be four instances of PlayerSettings, but later in the game there will be as many players as we decide.

Anyway, PlayerSettings will be our view. Let’s now create a view model for the view to bind to. In the ViewModels folder create a new class and name it PlayerSettingsViewModel. Here’s the code:

This code is pretty straightforward, although there are a few fragments that may seem a little obscure, like when the messages are sent. Don’t worry, we’re going to talk about them in a moment. But, what do we have here?

The Player model is stored as a private field. We instantiate a Player object in the constructor and use its properties inside the view model’s properties.

We don’t have any methods here, just a bunch of properties. We also defined some constants that are used by the properties. The PlayerId property is an integer number. It will be set to 1, 2, 3 or 4 because there are going  to be up to four players.

The PlayerName property, if changed, will also notify of changes in other properties where it’s used. This way, not only all bindings to PlayerName will be updated, but also bindings to NameIsValid and PlayerIsValid. It will also send a message to SettingsViewModel (which we are going to create soon) to make sure the other view model knows about this change.

PlayerInitialMoney is very similar. If changed, it will notify of the change, as well as of the changes in two other properties, and it will sent a message to SettingsViewModel.

PlayerIsInGame will be used to mark each potential player as taking or not taking part in the game. So, for example, if we decide that only two players should play in the game, the first two players will have this property set to true, and the other two to false.

There are two properties used to ensure the PlayerName and PlayerInitialMoney properties are valid. We don’t have to set the name. If we don’t, the name will be later set to a generic one like Player 1 or Player 2. If we decide to set the name, it shouldn’t be too long. In the PlayerInitialMoney property we’re using a method defined in the Helpers class. It will be also used in other classes in the app. I implemented the Helpers.ValueIsInRange method like so:

Finally, the PlayerIsValid property will be set to true in two cases: if the player is not in the game (like players 3 and 4 in the example above) and if they are in the game and have a valid name and initial money. This property will be used to make sure the button in the SettingsPage is enabled only if all players have valid data. This doesn’t have to be true about players who will not take part in the game because they will not be used in the following pages in the app.

Now we can set the view model as the binding context of the view and bind to its properties. Here’s the modified PlayerSettings view:

Again, most of this code is straightforward, but there are a couple new things we haven’t discussed so far. We’re going to have a look at them soon. But first things first…

In MVVM we usually tend to leave as little code in the code-behind as possible. Ideally, we should only initialize the component and set the binding context. In our case we don’t have to do the latter, because we set the binding context in XAML. All or most of the logic should go to the view model. But there’s one exception to this rule, which I already mentioned before. We should keep in the code-behind all the logic that is responsible for the visual aspect of the view. Here we want to set different visual states on the entries depending on the text entered, so we’ll leave the two TextChanged events and handle them in the PlayerSettings.xaml.cs file:

Now we’re using the properties defined in the view model to set the entries’ visual state. We’re also using a static method, HandleNumericEntryState, that I defined in the Helpers class:

The code-behind file is pretty short and it takes care just of the visual aspect of the view. But there are some interesting new elements in the XAML file. We have a converter and a behavior. You know what converters are, but behaviors are probably not so familiar to you. Anyway, let’s have a look at both.

ZeroToEmptyStringConverter

In some entries we’ll be entering text, like for example in the nameEntry. In others, like in the initialMoneyEntry (but not only) we’ll be entering numbers. We want the entry to be empty if the value is zero. So, if you see nothing except the placeholder test in the entry, it means the value is zero, not null. We’ll need a converter for that. In the Converters folder add a new class and name it ZeroToEmptyStringConverter. Here’s the code:

The converter is used in a two-way binding, so we must implement both methods. And here’s, again, how the converter is used in the view:

So, we must add the namespace first. Then we add the converter to the content view’s resources and reference it in the binding.

OK, the converter is easy to understand, but what about the NumericInputBahavior on the initialMoneyEntry?

Behaviors

Behaviors are a nice feature in .NET MAUI that you can use to extend your control classes. Normally, to extend a class, you inherit from it and add some functionality in the derived class. With behaviors it’s not necessary. When you attach a behavior to a control, the functionality defined in it acts as if it was part of the control itself.

We could implement additional functionality in the code-behind as well. But then it would be coupled with that particular view. As we want it to be reusable, we’ll implement it as a behavior that we can then attach to other controls.

The functionality that we want to add is ensuring that only numeric characters can be entered in the entry. So, if you try to type any other character, like a letter or a special character, this character will not show up.

We wouldn’t have to implement this functionality if our app was deployed only to Android, because we set the Keyboard property to Numeric. This will prevent you from entering other characters. But it doesn’t work on Windows, where you can still enter anything.

So, let’s create a new folder and name it Behaviors. In the folder let’s create a new class and name it NumericInputBehavior. Here’s the code:

As you can see, the behavior is meant to be attached to Entry controls. In particular, it’s added to the TextChanged event. The implementation is simple. If the last character you entered is not a number, the Text property of the entry is set to the old text, so the text before you typed that character.

And, once again, let’s have a look at how this behavior is added to the initialMoneyEntry:

You won’t be able to type non-numeric characters anymore.

The PlayerSettings view is embedded in the SettingsPage. So, let’s take care of this view and create a corresponding view model.

SettingsViewModel

We’ll need a view model for the SettingsPage (which is a view). Go to the ViewModels folder and create a new class. Name it SettingsViewModel.

Let’s start by registering the page and the view model with the dependency service in the MauiProgram class, just like we did with the TestPage:

Now we can set the binding context in the code-behind using dependency injection. Here’s the SettingsPage.xaml.cs file:

Now we can implement the SettingsViewModel class:

There are a couple constants, a couple observable properties and a couple regular properties. There are a couple methods, too.

At the beginning of the file, we define a private game variable that we set to a new instance of Game in the constructor. We also define the observable collection called players, which we populate in the constructor, too. There are going to be up to four players in the game, so we need four instances of PlayerSettingsViewModel to allow the user to set the names and initial money of the players. Here, in the constructor, we set the PlayerId property of each player, which will be used to display the player’s generic name (Player 1, Player 2, etc.). We also set the PlayerIsInGame property of the first two players to true, and of the other two players to false. This is because we assume the game is by default for two players, which can be naturally changed in the SettingsPage.

As we’re at the constructor, we register two messages there. We’ll be talking about the messaging system in a moment. And now let’s have a look at the properties that we have in the view model.

The GameEndingCondition, NumberOfRacesSet and GameTimeSet are properties related to the game itself, so we use the game object in them.

The MaxRacesIsValid and MaxTimeIsValid are responsible for ensuring that the user of the app sets the number of races or the time of the game, depending on which ending condition is chosen, to a value within a certain range.

The currentNumberOfPlayers observable property is an important one. Look how many change notifications are involved with it. Its value will be set by the radio buttons in the upper part of the SettingsPage.

The RacesEndingConditionSet property will be used to ensure that if there is only one player selected and the first ending condition (Money) is unavailable (because it wouldn’t make sense to end the game when there’s only one player with any money left in this situation), the default ending condition will be set to Races.

The two observable properties that we can see next, changedPlayerName and changedPlayerInitialMoney, are part of the messaging system and will be discussed later.

The last property is AllSettingsAreValid. There are a couple conditions defined in it to ensure that that everything is set to a valid value in the SettingsPage. Only if this property is true, will the Ready button be enabled.

Next, below the constructor, you can see some methods. The first two, OnPlayerNameChangedMessageReceived and OnPlayerInitialMoneyChangedMessageReceived are related to the messaging system. Next, we have the CreatePlayerList method that will handle the players when we check one of the radio buttons. It will set the players’ PlayerIsInGame property and the CurrentNumberOfPlayers property. If the one-player game mode is selected and the current ending condition is Money, it will be changed to Races, because Money is unavailable in this mode.

Finally, the SetEndingCondition method will do exactly what its name suggests.

We have the view model, so let’s take care of the view. The SettingsPage.xaml file is pretty lengthy, but we’re going to discuss it piece by piece.

SettingsPage View

So, beware! Here comes the SettingsPage. This is the complete code:

And now let’s have a closer look at it.

Let’s start with the PlayerSettings controls. Look how they’re bound to the particular players defined in ObservableCollection in the view model. Here’s the first player as an example:

Next, have a look at the first radio button in the Ending Conditions section. Its IsVisible property is bound to the OnlyOnePlayer property defined in the view model:

We’re using the InvertedBoolConverter that we get from the Community Toolkit. We added it to the page’s resources:

We want the first radio button to be visible only when there are at least two players.

The two entries in the Ending Conditions section should behave like the entries in the PlayerSettings controls. The input must be valid in order to proceed. That’s why we added the TextChanged events. In the code-behind, the SettingsPage.xaml.cs file, they’re implemented like so:

There’s also an interesting behavior that requires some more explanation.

EventToCommandBehavior

Let’s start with the player radio buttons, so the four radio buttons used to select the number of players in the game. They all look similar. The second button has the IsChecked property set to True, because the default number of players is two. Here’s the first button:

We want to implement the method that we’ll use to add and remove players from the Players observable collection. As you remember, the CreatePlayerList method has the RelayCommand attribute, which means it can be used in commanding. In the TestPage we bound the button’s Command property to a method, but now we have to bind the radio buttons. The problem is that, unlike buttons, radio buttons don’t support commands. This means we have to stick with events… unless we use the .NET MAUI Community Toolkit and its EventToCommandBehavior.

Here we have another behavior. Remember the NumericInputBehavior in the PlayerSettings view? We defined that behavior ourselves. Here we’re using a behavior defined in the Community Toolkit.

So, let’s do it. First, we have to install a NuGet package. Right-click the project and select Manage NuGet Packages… Search for the CommunityToolkit.Maui package (A) and install it (B):

When the package is installed, you will see some instructions to follow. We have to initialize the package. Go to the MauiProgram.cs file, add the appropriate using statement at the top of the file and call the UseMauiCommunityToolkit extension method on the builder object:

Also in the instructions, you can see how to use the toolkit in XAML. You have to add the following namespace:

After adding this namespace in the SettingsPage, we can make use of the EventToCommandBehavior. It allows us to invoke a command through an event.

We’re passing a parameter to the command, which is the number of players this button is supposed to set.

We also added the EventToCommandBehavior to the ending condition radio buttons below where the Command property is set to the SetEndingCondition method in the view model.

Make sure the toolkit is added to the namespaces:

Here we have just the first radio button, but make sure to set the behavior on all four radio buttons. Just set the CommandParameter to 1, 2, 3 and 4 respectively.

There’s one more piece of the puzzle we have to talk about, the messaging system. We’re sending and receiving messages between view models.

The Messaging System

Views bind their properties to properties in view models, but sometimes two view models have to communicate with each other. It’s possible to implement a messaging system that sends messages from one part of the application to any other part, even if the two parts are completely decoupled.

In our app, the button in the SettingsPage must respond to what happens in each particular PlayerSettings control. More precisely, it has to be disabled if the player’s name or initial money gets invalid. Let’s take the player’s name as an example. In this property’s setter a couple notifications are sent so that other properties can be updated if necessary. Here’s the PlayerSettingsViewModel.cs file:

But the button in the SettingsPage also needs to be notified in order to adjust its IsEnabled property. We can bind this property to a property in the SettingsViewModel, but not to a property in the PlayerSettingsViewModel. Fortunately, a property in one view model can send a message that will be received by another view model and a property in the other view model, to which the button can bind, will be set appropriately. Looks a bit complicated, so let’s analyze it step by step.

The first step is to create the message. We’re going to need two messages: one will be sent by the PlayerName property when it changes, the other by the PlayerInitialMoney when it changes, so let’s create a folder in the root of our app and name it Messages. In the folder let’s create two classes and name them PlayerNameChangedMessage and PlayerInitialMoneyChangedMessage. Here’s how the former is implemented:

The other one looks almost the same:

The MVVM Toolkit supports two types of messengers. The one we’re interested in is WeakReferenceMessenger. As the name suggests, it uses weak references internally. It offers automatic memory management for the recipients, so you don’t have to remember to unsubscribe the recipients.

Now, with our messages created, the two properties of the PlayerSettingsViewModel class can send them:

Sending is one thing. Another thing is receiving. Which object is supposed to receive the messages? Well, definitely the SettingsViewModel. The message must be registered there and, naturally, handled. So, let’s open the SettingsViewModel.cs file and register the messages in the constructor:

As you can see, there are two methods that will handle the messages. Here’s how they are implemented:

These two methods set the ChangedPlayerName and ChangedPlayerInitialMoney properties respectively to the value passed by the message.

Let’s have a look at how these properties are implemented:

As you can see, whenever one of these properties changes, the AllSettingsAreValid property is notified and this is the property the button’s IsEnabled property binds to. The button can now react to any changes in the properties that sent the messages from a different object.

Now the app works as expected. For example, the button gets disabled when the initial money of one of the players is invalid:

It’s also disabled if you choose the Races ending condition but don’t specify the number of races. This time let’s check it out on Android:

OK, we’ve spent so much time trying to enable or disable the button, depending on the data delivered in the entries, that we didn’t even have time to think about what this button is for in the first place…

And the button is for navigation. If you click it, it will navigate to the RacePage where the actual game begins. Navigation is going to be the subject of the next part of the series. And don’t worry we only implemented a couple view models. We’re going to implement the other view models in the next part too.


Spread the love

Leave a Reply