MVVM Source Generators: Advanced Scenarios

MVVM Source Generators: Advanced Scenarios

Intercept property setters and say goodbye to writing busy flags

ยท

11 min read

Introduction

Welcome to the second article of my mini-series about MVVM Source Generators for C# .NET. In the previous post, I discussed the basics and most important features that the MVVM Community Toolkit provides, specifically attributes that can be used to automatically generate properties and commands.

In this second part, I will show you how to intercept property setters in order to provide custom behavior, which you might be used to from developing ViewModels entirely by hand without using Source Generators. By intercepting the setters, we can define custom functionality that can be executed right before and right after changing the backing field of a property.

Last but not least, I will also demonstrate the beauty of auto-generated RelayCommands. In the past, developers added a lot of busy flags (properties) to their ViewModels in order to show some kind of busyness indicator (such as an ActivityIndicator, also known as a spinner) that informs the user that a longer operation is currently taking place.

Like in the previous post, I am using a .NET MAUI project (check out the sample repository) to demonstrate the functionality, but since the MVVM Community Toolkit is independent from UI frameworks, the same things apply to technologies like Windows Presentation Foundation (WPF) and Xamarin.Forms.

Updated scenario

In order to demonstrate customized property setters as well as the awesome new way to indicate activity (or busyness) without flags, I have added a Stepper control to the UI from the previous post to simulate the selection of how many copies of the address label should be printed. When the number of copies is set to 0 the popup won't open. I have also added an ActivityIndicator to be displayed while the popup is being opened:

The AddressViewModel receives two new properties which are called Copies and IsBusy and the PrintAddress() method will be changed to return an async Task in order to simulate a longer running operation:

private int _copies;
public int Copies
{
    get => _copies;
    set => SetField(ref _copies, value);
}

private bool _isBusy;
public bool IsBusy
{
    get => _isBusy;
    set => SetField(ref _isBusy, value);
}

private async Task PrintAddressAsync()
{
    IsBusy = true;

    await Task.Delay(TimeSpan.FromSeconds(2));

    OnPrintAddress?.Invoke(FullAddress);

    IsBusy = false;
}

The ActivityIndicator is bound to the IsBusy flag in the XAML:

<ActivityIndicator
  IsVisible="{Binding IsBusy}"
  IsRunning="{Binding IsBusy}"/>

In the next steps, we will add some custom functionality to the setter of the Copies property before addressing the busy flags. We'll first look at how this is usually done without Source Generators followed by how you can take advantage of the MVVM goodness that the Source Generators provide without having to live without customized property setters.

Custom Property Setters

Using the previously introduced Source Generator attributes may have left some readers who have working MVVM experience with some open questions. For example, there are situations where you would not only add an observable property that raises the PropertyChanging or PropertyChanged events, but you might actually need to customize the property setter to execute additional functionality if and when the property is either about to change or has just changed.

Opinion: In my humble opinion, it's a bad practice to add a lot of functionality to a property setter. By adding a lot of complex statements or even loops to a setter, you're effectively mixing concerns, which makes them difficult to maintain. In the end, a setter is a like a method that updates a value and should have no unexpected side-effects. Adding a lot of extra functionality that belongs into a separate method can lead to the violation of the Single Responsibility Principle (SRP) and should be avoided.

Executing logic in classic setters

Let's say we want to execute some additional logic in the setter of our Copies property, e.g. to log the current and new values to the console, and also notify subscribers about the changing state. Usually, without Source Generators we would write something like this:

private int _copies;
public int Copies
{
    get => _copies;
    set
    {
        if (value == _copies)
        {
            return;
        }

        //do something before property is changing
        Console.WriteLine($"Property {nameof(Copies)} is about to change. Current value: {Copies}, new value: {value}");
        OnPropertyChanging();        

        _copies = value;

        //do something after property changed
        Console.WriteLine($"Property {nameof(Copies)} is has changed. Current value: {Copies}, new value: {value}");
        OnPropertyChanged();
    }
}

Note: For simplicity's sake I went with logging something to the console here instead of some advanced logic. Of course, it's also possible to execute some complex logic inside a property setter, although I recommend separating concerns whenever possible.

Now, how can we achieve something similar using the Source Generators, though?

Executing logic in auto-generated setters

As it turns out, it's actually quite easy to add custom behavior to auto-generated property setters, because the MVVM Source Generators graciously provide partial methods (signature only) for us which we can "hook" into, meaning we can provide an implementation body for these methods.

Let's add the Copies property in the MVVM Source Generator way and then look at what is actually being generated for us. This is the property with the [ObservableProperty] attribute:

[ObservableProperty]
private int _copies;

If you remember from the previous post, in the Under the hood section, there are two methods, specifically, which are generated for us based on the property's name when using the ObservableObject base class together with the [ObservableProperty] attribute:

  • partial void On[PropertyName]Changing(<type> value)

  • partial void On[PropertyName]Changed(<type> value)

For the Copies property these auto-generated methods look like this:

/// <summary>Executes the logic for when <see cref="Copies"/> is changing.</summary>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.1.0.0")]
partial void OnCopiesChanging(int value);

/// <summary>Executes the logic for when <see cref="Copies"/> just changed.</summary>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.1.0.0")]
partial void OnCopiesChanged(int value);

As we can see, these methods are actually partial methods that don't define a body. We will take advantage of that a bit further down, but first let's have a look at when and where those methods are invoked. This is the auto-generated property:

/// <inheritdoc cref="_copies"/>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.1.0.0")]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public int Copies
{
    get => _copies;
    set
    {
        if (!global::System.Collections.Generic.EqualityComparer<int>.Default.Equals(_copies, value))
        {
            OnCopiesChanging(value);
            OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Copies);
            _copies = value;
            OnCopiesChanged(value);
            OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Copies);
        }
    }
}

Right in beginning of the setter, the equality of the _copies backing field and the new value is checked. Only if this comparison evaluates to false, meaning that the values are different, the setter logic is executed.

First, the OnCopiesChanging() method is invoked with the new value as an argument. Since the method doesn't actually have a body, nothing happens - yet. That call is followed by raising the PropertyChanging event of the INotifyPropertyChanging interface.

Second, the value of the _copies backing field gets updated before OnCopiesChanged() is invoked using the new value as an argument. This is followed by raising the PropertyChanged event of the INotifyPropertyChanged interface.

This means that we can "hook" into the setter by providing implementation bodies for the OnCopiesChanging() and OnCopiesChanged() methods in order to achieve the same functionality as we did without the Source Generators:

[ObservableProperty]
private int _copies;

partial void OnCopiesChanging(int value)
{
    Console.WriteLine($"Property {nameof(Copies)} is about to change. Current value: {Copies}, new value: {value}");
}

partial void OnCopiesChanged(int value)
{
    Console.WriteLine($"Property {nameof(Copies)} is has changed. Current value: {Copies}, new value: {value}");
}

That's it. That's all we have to do in order to add custom functionality for our property setters. If we wanted to, we could even access other properties or run some fancy logic. Yeah! ๐ŸŽ‰

Mixing approaches

For common and simple scenarios, the MVVM Source Generators are the easiest way to minimize your coding efforts and increase productivity by reducing this repetitive task to a minimum, but what about non-standard properties that require some special logic which cannot be achieved using the MVVM Source Generators?

In the rare case that you really cannot achieve some highly specialized behavior using the auto-generated properties, such as executing logic inside of getters, or inside setters independent of the result of the equality comparison, you can still implement your properties like you would have without using Source Generators. It is completely valid to mix the two approaches and use the best of both worlds.

Nothing stops you from implementing one property using a Source Generator attribute and another one by implementing it completely manually:

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsOfLegalAge))]
private int _age;

public bool IsOfLegalAge => Nationality == Nation.USA ? Age >= 21 : Age >= 18;

private Nation _nationality;
public Nation Nationality
{
    get => _nationality;
    set
    {
        if(!SetField(ref _nationality)) return;
        OnPropertyChanged(nameof(IsOfLegalAge));
    }
}

Goodbye, busy flags

In the beginning of this article, I have introduced an IsBusy property that is used to show or hide an ActivityIndicator and it is set manually at the beginning and at the end of the PrintAddressAsync() method:

private async Task PrintAddressAsync()
{
    IsBusy = true;

    await Task.Delay(TimeSpan.FromSeconds(2));

    OnPrintAddress?.Invoke(FullAddress);

    IsBusy = false;
}

This is a common scenario in many ViewModels, but often you need to have various flags to indicate busyness for different operations and sometimes you just cannot reuse the same flag for that.

Problems of busy flags

Doing this is problematic, because it scatters the code with flags and you may even run into issues when you have some complex logic that is running in the method that your command invokes.

I have often encountered bugs where busy flags have not been reset correctly, because a developer decided to (rightfully!) jump out of a method early if a certain condition isn't met and the developer (it might have been myself on occasion) forgot to set the flag to false again when the method returns early from execution.

Take the following scenario as an example:

private async Task PrintAddressAsync()
{
    IsBusy = true;

    if (Copies < 1) return;

    await Task.Delay(TimeSpan.FromSeconds(2));

    OnPrintAddress?.Invoke(FullAddress);

    IsBusy = false;
}

Here, IsBusy would remain true although the method has already returned, if the value of Copies is smaller than 1. The ActivityIndicator would keep spinning forever (figuratively). This can be remedied in a couple of different ways, but none of them are pretty.

For example, we could set the IsBusy flag to false in multiple locations in our code, or we could decide to wrap that code, either by using try-finally blocks or with an extra method which is executed between the two statements that update the IsBusy property.

Option 1: Set flag in multiple locations

private async Task PrintAddressAsync()
{
    IsBusy = true;

    if (Copies < 1)
    {
        IsBusy = false;
        return;
    }

    await Task.Delay(TimeSpan.FromSeconds(2));

    OnPrintAddress?.Invoke(FullAddress);

    IsBusy = false;
}

This is ugly and error-prone, because we may easily forget to add the required statements to all possible code branches that return early from execution.

Option 2: Use a try-finally block

private async Task PrintAddressAsync()
{
    try
    {
        IsBusy = true;

        if (Copies < 1) return;

        await Task.Delay(TimeSpan.FromSeconds(2));

        OnPrintAddress?.Invoke(FullAddress);
    }
    finally
    {
        IsBusy = false;
    }
}

This solution is abusing try-finally blocks for non-intended purposes, but it will work.

Note: Usually, you would use a finally block to clean up resources

Option 3: Introduce a separate method

private async Task PrintAddressAsync()
{
    IsBusy = true;

    await PrintAsync();

    IsBusy = false;
}

private async Task PrintAsync()
{
    if (Copies < 1) return;

    await Task.Delay(TimeSpan.FromSeconds(2));

    OnPrintAddress?.Invoke(FullAddress);
}

This is by far the best option when using flags, but it's still not pretty having to deal with several different busy flags in more elaborate ViewModels.

Introducing AsyncRelayCommand

There is a solution to this and it's called AsyncRelayCommand.

The [RelayCommand] attribute of the MVVM Source Generators will generate different types of commands for us depending on the method signature.

When using a void method it will create a regular RelayCommand, but when the return type is an async Task, for example, it will actually generate an AsyncRelayCommand for us:

//this will create a RelayCommand called "PrintAddressCommand"
[RelayCommand]
private void PrintAddress() {} 

//this will create an AsyncRelayCommand called "PrintAddressCommand"
[RelayCommand]
private async Task PrintAddressAsync() {}

Important: When using async methods, it's common practice to use the "Async" suffix for the method name, e.g. PrintAddressAsync(). However, the Source Generators will recognize the suffix and remove it from the command name, so the command will still be called PrintAddressCommand (instead of PrintAddressAsyncCommand).

This is splendid, because the AsyncRelayCommand comes with its own type of busy flag in the form of a property called IsRunning. We can use this property instead of implementing our own IsBusy property. All we need to do is change the bindings of the ActivityIndicator from IsBusy to PrintAddressCommand.IsRunning:

<ActivityIndicator
  IsVisible="{Binding PrintAddressCommand.IsRunning}"
  IsRunning="{Binding PrintAddressCommand.IsRunning}"/>

Note: Buttons in .NET MAUI automatically use the IsRunning flag when their Command property is bound to an asynchronous command. This is useful, because the button will be disabled until the command finishes execution.

We don't need to set any busy flags in the ViewModel anymore:

[RelayCommand]
private async Task PrintAddressAsync()
{
    if (Copies < 1) return;

    await Task.Delay(TimeSpan.FromSeconds(2));

    OnPrintAddress?.Invoke(FullAddress);
}

๐Ÿ‘‹ Goodbye, busy flags - AsyncRelayCommand FTW! I love it! ๐Ÿคฉ

Conclusions and next steps

Writing clean and maintainable ViewModels has never been easier thanks to the Source Generators of the MVVM Community Toolkit. We can still write exactly the same kind of logic like we did before while saving loads of valuable time and tremendously reducing the risk of bugs.

Source Generators can help you with decreasing the amount of boilerplate code and make ViewModels much more legible and comprehensible, provided you have a basic understanding of the MVVM pattern.

It's easier than ever to quickly write up a ViewModel with many different properties and set up commands and bindings with busy indicators without sacrificing any of the flexibility of manually implementing the INotifyPropertyChanging and INotifyPropertyChanged interfaces.

I hope you're just as excited as I am about this major development in the .NET realm. James Montemagno has recently summarized these amazing features of the MVVM Community Toolkit in another great YouTube video which is definitely worth checking out, as well.

If you enjoyed this blog post, then follow me on LinkedIn, subscribe to this blog and star the GitHub repository for this post so you don't miss out on any future posts.

ย