MVVM Source Generators: Advanced Scenarios
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 calledPrintAddressCommand
(instead ofPrintAddressAsyncCommand
).
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 theirCommand
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.
Subscribe to my newsletter
Read articles from Julian Ewers-Peters directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Julian Ewers-Peters
Julian Ewers-Peters
I am a passionate mobile app and software developer with a focus on C# .NET, Xamarin.Forms and .NET MAUI.