Easily control the executability of Commands using MVVM Source Generators
Introduction
Welcome to the third part of my mini-series about MVVM Source Generators for C# .NET and the awesomeness of the MVVM Community Toolkit. So far, we have seen how we can save ourselves from writing too much boilerplate code when applying the MVVM pattern in .NET-based app technologies and UI frameworks.
In this part, I will give a short overview of yet another two amazing Source Generators which can be used to control the executability of Commands. In classic MVVM without Source Generators this would usually be done using the CanExecute predicate of a Command, which enables or disables it. Even with Source Generators you can still achieve exactly the same behavior as you could with classic MVVM implementations.
In this scenario, we do not actually save as much boilerplate code as with the other Source Generators that I have explored previously. However, the attributes we will use are required in situations where you need to enable and disable Commands based on specific conditions, such as the validity of the CommandParameter or of properties in the ViewModel when using Source Generators.
Special thanks to my colleague and friend Marco Seraphin for pointing out that this topic would be a great addition to my blog. Marco has inspired this article and has helped by reviewing it.
Like in my previous posts on the topic, 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.
Setting
I am reusing the project from the sample repository with the address label printing functionality. In the previous post I have added an ActivityIndicator
as well as a Stepper
control to the project to demonstrate busyness using AsyncRelayCommand
.
If you remember from before, the Button
to trigger the PrintAddressCommand
was always enabled, but nothing would happen if the number of copies was set to 0. Since Commands can have a CanExecute
predicate, which is used to determine whether they can be executed or not, they can also be used to enable and disable buttons automatically.
So, for the updated setting, I want the Button
only to be enabled when the Command is actually executable, otherwise it should be disabled and thus be grayed out when the number of copies is 0 and the address is empty:
Therefore, I have added a new method to the AddressViewModel
which can be used as the CanExecute
predicate for the PrintAddressCommand
:
private bool CanPrint(string address) => Copies > 0 && !string.IsNullOrWhiteSpace(address);
This method checks if the argument that is passed into the Command is valid (for simplicity, I am only checking for null and whitespaces or an empty string) and if the number of copies to print is larger than 0.
When using the CanPrint()
method as a predicate in the classic version of the AddressViewModel
, the Command would then look like follows:
private IAsyncRelayCommand _printAddressCommand;
public IAsyncRelayCommand PrintAddressCommand => _printAddressCommand ??= new AsyncRelayCommand<string>(PrintAddressAsync, canExecute: CanPrint);
Our Button
in the XAML of our UI has also been updated to pass in the address as a CommandParameter:
<Button
Command="{Binding PrintAddressCommand}"
CommandParameter="{Binding FullAddress}"
Text="Print Address" />
In this case, I am simply passing in the FullAddress
property.
Note: I have used the
FullAddress
property as a CommandParameter only for demonstration purposes. Since it's a property of the same ViewModel as the Command, I could have also directly accessed the property inside of theCanPrint()
method instead of passing it in.
This already suffices to enable and disable the Button
automatically, we cannot use the IsEnabled
property of the Button
in this setting and it's also not required.
Important: In .NET MAUI and Xamarin.Forms, you should not use the IsEnabled property when using Commands, because it will automatically be overridden when binding to a Command. Find more information in the official documentation.
Controlling Executability
Generally, the CanExecute
predicate is only evaluated by the Command during instantiation or when an argument is passed to the Command via the CommandParameter property. In the latter case, the CanExecute
predicate is evaluated each time that the CommandParameter, which our Button
binds to, changes.
Important: The method which is used for the predicate must return a
bool
and optionally may have exactly one input parameter which must correspond to the CommandParameter.
However, there are also scenarios in which the predicate depends on properties which are not passed in as an argument of the CommandParameter. In these situations, we need to manually inform the Command that something has changed and that its executability should be re-evaluated.
Let's explore how this is usually done using classic MVVM in .NET and then let's have a look at how this can be done using the previously introduced Source Generators.
Using Classic MVVM
When using the classic MVVM approach to update the Command and re-evaluate its executability, the ViewModel (stripped down to the relevant bits only) would look as follows:
private IAsyncRelayCommand _printAddressCommand;
public IAsyncRelayCommand PrintAddressCommand => _printAddressCommand ??= new AsyncRelayCommand<string>(PrintAddressAsync, canExecute: CanPrint);
private async Task PrintAddressAsync(string address)
{
await Task.Delay(TimeSpan.FromSeconds(2));
OnPrintAddress?.Invoke(address);
}
private bool CanPrint(string address) => Copies > 0 && !string.IsNullOrWhiteSpace(address);
private int _copies;
public int Copies
{
get => _copies;
set
{
if (value == _copies)
{
return;
}
OnPropertyChanging();
_copies = value;
OnPropertyChanged();
PrintAddressCommand.NotifyCanExecuteChanged();
}
}
Here, we pass the CanPrint()
method, which exists in both versions equally, as an argument to the Command's canExecute
parameter. The CanExecute predicate of the Command is evaluated each time when the CommandParameter, the address
string, changes and it is also re-evaluated when the Copies
property changes, because the Command gets notified by calling NotifyCanExecuteChanged()
on it.
This already isn't much code thanks to the NotifyCanExecuteChanged()
method that the IRelayCommand
interface provides.
Using MVVM Source Generators
Since the Source Generators largely take care of the implementation of properties and commands for us, we need a way to pass in the canExecute
parameter to the Command. We also need a way to trigger an update on the Command to re-evaluate its executability. The first attribute might look familiar from prior usage and the second one also has similarities with another attribute that we've already explored before. It's time to look at [RelayCommand]
again and then we'll do a short dive into [NotifyCanExecuteChangedFor]
.
Revisiting [RelayCommand]
In order to configure and update the executability of a Command via the canExecute
parameter, the [RelayCommand]
attribute comes with an optional property of type string?
, which conveniently is called CanExecute
and which is used to provide the name of the method that is used to evaluate the executability of the Command:
[RelayCommand(CanExecute = nameof(CanPrint))]
private async Task PrintAddressAsync(string address) { /* ... */ }
The main difference compared to the classic approach is that we pass in the name of the method to the Source Generator instead of the method itself.
Under the hood, the Source Generator actually generates a Command which looks remarkably similar to the one from the ViewModel without Source Generators:
partial class AddressViewModelSourceGen
{
/// <summary>The backing field for <see cref="PrintAddressCommand"/>.</summary>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.1.0.0")]
private global::CommunityToolkit.Mvvm.Input.AsyncRelayCommand<string>? printAddressCommand;
/// <summary>Gets an <see cref="global::CommunityToolkit.Mvvm.Input.IAsyncRelayCommand{T}"/> instance wrapping <see cref="PrintAddressAsync"/>.</summary>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.1.0.0")]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public global::CommunityToolkit.Mvvm.Input.IAsyncRelayCommand<string> PrintAddressCommand => printAddressCommand ??= new global::CommunityToolkit.Mvvm.Input.AsyncRelayCommand<string>(new global::System.Func<string, global::System.Threading.Tasks.Task>(PrintAddressAsync), CanPrint);
}
As we can see, the name of the method is simply used by the Source Generator to insert it as text into the generated code file and serves as the argument for the canExecute
parameter (all the way at the end, where it says CanPrint
).
Since the generated Command is of type AsyncRelayCommand<string>
, any arguments passed into a Command as a CommandParameter are forwarded to the PrintAddressAsync()
and CanPrint()
methods respectively.
With this in place, the executability of the Command will be re-evaluated every time that the CommandParameter of our Button
changes. But how do we notify the Command about changes to the Copies
property from the ViewModel?
Introducing [NotifyCanExecuteChangedFor]
When a property changes and we want to notify subscribers that another property, e.g. a getter-only one, likely has changed as well, we can use the [NotifyPropertyChangedFor]
attribute when using Source Generators. Fortunately, a similar attribute exists for commands: [NotifyCanExecuteChangedFor]
, which also works in the same fashion.
The [NotifyCanExecuteChangedFor]
attribute is used to decorate any property which may be required to determine the executability of a Command and takes the name of the Command as an argument:
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(PrintAddressCommand))]
private int _copies;
This is all we need to do in order to trigger a re-evaluation of the CanExecute
predicate attached to the PrintAddressCommand
. Every time that the auto-generated Copies
property gets updated, the re-evaluation takes place.
Under the hood, this also looks highly familiar: At the end of the auto-generated property's setter, the NotifyCanExecuteChanged()
method is called on the auto-generated PrintAddressCommand
:
/// <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);
PrintAddressCommand.NotifyCanExecuteChanged();
}
}
}
This is exactly what I have also done in the ViewModel that does not use the Source Generators of the MVVM Community Toolkit: First, the equality comparison of the backing field and the new value takes place, followed by the notification that the property is about to change, then the backing field gets updated, which is then followed by the notification that the property has just changed and finally, the CanExecuteChanged
event of the PrintAddressCommand
is raised by calling NotifyCanExecuteChanged()
.
ViewModel with Source Generators
The completed version of the ViewModel (again stripped down to the relevant bits only) using Source Generators looks as follows:
[RelayCommand(CanExecute = nameof(CanPrint))]
private async Task PrintAddressAsync(string address)
{
await Task.Delay(TimeSpan.FromSeconds(2));
OnPrintAddress?.Invoke(address);
}
private bool CanPrint(string address) => Copies > 0 && !string.IsNullOrWhiteSpace(address);
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(PrintAddressCommand))]
private int _copies;
Amazing, this looks (c)lean and beautiful. My developer heart is happy - again ๐.
Running the sample
In both versions, we can now run the sample app and see that the Button
is only enabled when the address is not empty and at least one copy is set to be printed:
Perfect! ๐ช There's no need to do without the functionality that CanExecute provides when using auto-generated commands, because the MVVM Source Generators have us covered here, as well.
Conclusions and next steps
Evaluating the executability of commands is still possible like before even with Source Generators. 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, especially when dealing with a multitude of different properties and commands.
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. At the same time, you do not have to live without your favorite MVVM features and interfaces, since they are all compatible and complementary, with and without using Source Generators - mix and match, if you will.
In my humble opinion, this is all relatively straightforward, as long as you have a working understanding of the MVVM pattern and its implementation in .NET. I hope that you learned something again from this article.
Last but not least, my friend and colleague Marco has his own sample repository on GitHub with another example using the CanExecute predicate, check it out if you like.
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 and developments.
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.