Using CommunityToolkit.MAUI in your .NET MAUI project to simplify your code.

You can use the CommunityToolkit.MAUI library to simplify your .NET MAUI code.
This library has simple annotations that allow you to annotate a property for commands and observable properties for your Controls which makes it so that you don’t have to define all of the boilerplate code that are typically required for commands and observable properties.

To use the CommunityToolkit.Mvvm library you will need to add the library package to your project using Nuget:

https://www.nuget.org/packages/CommunityToolkit.MAUI

You will also need to register the library with your MAUI project from the MauiProgram.cs configuration file.

Registering the CommunityToolkit library with your MAUI project

Using the CommunityToolkit.MAUI library to define Controls using annotations

You can replace standard .NET commands with the annotation [RelayCommand] to help reduce boilerplate code. In the below XAML markup we have a VerticalStackLayout with 2 input controls, Entry and Editor. We also have 2 Buttons, one for saving and one for deleting.

<?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="ScoreKeepersBoard.Views.TeamPage"
             xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
             Title="TeamPage">
    <VerticalStackLayout Spacing="10" Margin="5">
        <Entry
            Placeholder="Enter your team name"
            Text="{Binding CurrentTeam.TeamName}"
            FontSize="16">
            
            <Entry.Behaviors>
                <toolkit:EventToCommandBehavior
                EventName="TextChanged"
                Command="{Binding TeamNameTextChangedCommand}"
                x:TypeArguments="WebNavigatedEventArgs"/>
            </Entry.Behaviors>
            
        </Entry>
        
       <Label           
            FontSize="16"
            Text="{Binding NameErrorMessage}"/>

        <Editor x:Name="TextEditor"
        Placeholder="Team Description"
        Text="{Binding CurrentTeam.TeamDetails}"
        HeightRequest="100" />

        <Grid ColumnDefinitions="*,*" ColumnSpacing="4">
            <Button Text="Save"
                    IsEnabled="{Binding SaveButtonEnabled}"
                    Command="{Binding SaveButtonClickedCommand}"/>

            <Button Grid.Column="1"
                    Text="Delete"
                    IsEnabled="{Binding DeleteButtonEnabled}"
                    Command="{Binding DeleteButtonClickedCommand}"/>
        </Grid>
    </VerticalStackLayout>
</ContentPage>

Not all Controls have the native ability to define Commands. To determine if a Control you want to use allows Commands just check the documentation on the Control. Buttons do allow Commands. You will see that for example the delete button above we are using Command and binding the Command to a Command called “DeleteButtonClickedCommand”.

We will now define the [RelayCommand] Annotation in our ViewModel.

using System;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ScoreKeepersBoard.Database;
using ScoreKeepersBoard.DTO;
using SQLite;

namespace ScoreKeepersBoard.ViewModels;

public partial class TeamViewModel : ObservableObject
{
    private ITeamDataAccessSync teamDataAccessSync;

    public TeamViewModel(ITeamDataAccessSync teamDataAccessSyncInjected)
	{
        teamDataAccessSync = teamDataAccessSyncInjected;
	}

    [ObservableProperty]
    public bool saveButtonEnabled = true;

    [ObservableProperty]
    public bool deleteButtonEnabled;

    [ObservableProperty]
    public string nameErrorMessage;

    [ObservableProperty]
    private Team currentTeam;

    public void LoadTeam(Team team)
    {
        if (team == null)
        {
            CurrentTeam = new Team();
            DeleteButtonEnabled = false;
        }
        else
        {

            CurrentTeam = team;
            DeleteButtonEnabled = true;
        }
    }

    [RelayCommand]
    public void TeamNameTextChanged()
    {
        CheckIfGameTypeNameExists();
    }
    public void CheckIfGameTypeNameExists()
    {
        try
        {
            if (CurrentTeam == null)
                return;

            Team gameType = teamDataAccessSync.GetTeamByNameAscending(CurrentTeam.TeamName);
            if (gameType != null)
            {
                NameErrorMessage = "Game type name '" + gameType.TeamName + "' already taken.";
                SaveButtonEnabled = false;

            }
            else
            {
                NameErrorMessage = "";
                SaveButtonEnabled = true;
            }
        }
        catch(Exception e)
        {
            string message = e.Message;
        }
    }

    public void NavigateFrom()
    {
        CurrentTeam = null;
        DeleteButtonEnabled = true;
        SaveButtonEnabled = true;
        NameErrorMessage = "";

    }
    public bool IsSaveButtonEnabled()
    {
        if (SaveButtonEnabled)
            return true;
        else
            return false;
    }
    public bool IsDeleteButtonEnabled()
    {
        if (DeleteButtonEnabled)
            return true;
        return false;
    }

    [RelayCommand(CanExecute = nameof(IsSaveButtonEnabled))]
    public async void SaveButtonClicked()
    {

        nameErrorMessage = "";
        try
        {          
            int numSaved = teamDataAccessSync.SaveTeam(CurrentTeam);
            await Shell.Current.GoToAsync("..");
        }
        catch (SQLiteException e)
        {
            string error = e.Message;
            if (e.Message.Contains("UNIQUE constraint failed"))
            {
                string test = error + " - FAIL";
                NameErrorMessage = "Name already taken";
            }
            else
            {
                nameErrorMessage = "Database error saving name";
            }
        }
        catch(Exception e)
        {
            string message = e.Message;
        }
    }

    [RelayCommand(CanExecute = nameof(IsDeleteButtonEnabled))]
    public async void DeleteButtonClicked()
    {
        try
        {
            teamDataAccessSync.DeleteTeam(CurrentTeam);
            await Shell.Current.GoToAsync("..");
        }
        catch(Exception e)
        {
            string message = e.Message;
        }
    }
}

In the code above we define an asynchronous method called “DeleteButtonClicked” and give it the Annotation:
[RelayCommand(CanExecute = nameof(IsDeleteButtonEnabled))]
Any time you use [RelayCommand] Annotation to define a Command using the CommunityToolkit.Mvvm library the library automatically generates a Command for you when the code is compiled and this Command automatically has the word “Command” added to the end of your method name. This is why when on the front end we are looking for a Command on the delete Button called “DeleteButtonClickedCommand” but in the ViewModel we define the annotated RelayCommand “DeleteButtonClicked”. You can also give the Annotation the property “CanExecute” as seen above and bind that to a boolean field or property of your ViewModel. If the bound boolean is set to false then even if the Command is called it won’t be executed.

Some Controls do not inherently support Commands. If you want to use a Control like this then the CommunityToolkit.Mvvm library has a special markup for your XAML that defines extra behaviors that you can attach to your Control. In this excerpt from the XAML markup above we are looking at the Entry Control which does not support Control inherently:

<?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="ScoreKeepersBoard.Views.TeamPage"
             xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
             Title="TeamPage">
    <VerticalStackLayout Spacing="10" Margin="5">
        <Entry
            Placeholder="Enter your team name"
            Text="{Binding CurrentTeam.TeamName}"
            FontSize="16">
            
            <Entry.Behaviors>
                <toolkit:EventToCommandBehavior
                EventName="TextChanged"
                Command="{Binding TeamNameTextChangedCommand}"
                x:TypeArguments="WebNavigatedEventArgs"/>
            </Entry.Behaviors>
            
        </Entry>
....

We can add markup that defines extra Control behaviors and in this markup we can use the CommunityToolkit.Mvvm “EventToCommandBehavior”. In order to use this you will need to define an extra xmlns namespace that points to the toolkit:

xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"

and then use that xmlns namespace in your Controls extra behavior markup:

                <toolkit:EventToCommandBehavior
                EventName="TextChanged"
                Command="{Binding TeamNameTextChangedCommand}"
                x:TypeArguments="WebNavigatedEventArgs"/>

This allows you to define a Command inside the extra Control behavior markup.

Using the CommunityToolkit.MAUI to define observable properties

Using this new toolkit library you can use Annotations to define observable properties which greatly helps you reduce boilerplate code. In the excerpt below from our ViewModel we have a field “nameErrorMessage”

using System;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ScoreKeepersBoard.Database;
using ScoreKeepersBoard.DTO;
using SQLite;

namespace ScoreKeepersBoard.ViewModels;

public partial class TeamViewModel : ObservableObject
{
    private ITeamDataAccessSync teamDataAccessSync;

    public TeamViewModel(ITeamDataAccessSync teamDataAccessSyncInjected)
	{
        teamDataAccessSync = teamDataAccessSyncInjected;
	}

    [ObservableProperty]
    public bool saveButtonEnabled = true;

    [ObservableProperty]
    public bool deleteButtonEnabled;

    [ObservableProperty]
    public string nameErrorMessage;

......

We define an annotation on the property called “nameErrorMessage” that binds to a label in the XAML. Using the [ObservableProperty] annotation on this field creates a property when your code is compiled that is observable and any changes made to the property are reflected in the front end. If you were to not use this annotation you would have to create the property yourself with all of the boilerplate code required:

public string? NameErrorMessage
{
    get => name;
    set
    {
        if (!EqualityComparer<string?>.Default.Equals(nameErrorMessage, value))
        {
            OnNameChanging(value);
            OnPropertyChanging();
            nameErrorMessage = value;
            OnNameChanged(value);
            OnPropertyChanged();
        }
    }
}

When using the annotation always have your field start in lowercase and the property generated upon compile time will start upper case. For instance here is the front end Control that this field binds to:

        <Label           
            FontSize="16"
            Text="{Binding NameErrorMessage}"/>

You will notice that the binding is to the property that is generated at compile time rather than your field name. Make sure to use the expected property name rather than your field name.

Make sure your class is partial and uses the abstract class ObservableObject

In order to use the ObservableProperty or RelayCommand annotations you have to make sure your ViewModel is an partial class and inherits from ObservableObject as seen in the code excerpt below:

using System;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ScoreKeepersBoard.Database;
using ScoreKeepersBoard.DTO;
using SQLite;

namespace ScoreKeepersBoard.ViewModels;

public partial class TeamViewModel : ObservableObject
{
    ....
}

Enjoy the freedom of having to write less boilerplate code which will free up time for you to get more .NET MAUI projects completed!!!!

Comments

  1. […] of using a Control you will have to use Commands for your controls. To simplify your code check out this CodeShadowHand tutorial on using the CommunityToolkit.Mvvm library to replace your Commands and Observable Object […]

Leave a Reply

Your email address will not be published. Required fields are marked *