Easily control the executability of Commands using MVVM Source Generators

Easily control the executability of Commands using MVVM Source Generators

ยท

9 min read

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 the CanPrint() 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.

ย