Introduction to MVVM Source Generators for C# .NET

Introduction to MVVM Source Generators for C# .NET

Stop writing boilerplate code with the MVVM Community Toolkit

ยท

13 min read

Introduction

I am a huge fan of the Model-View-ViewModel pattern (MVVM) and it's no surprise to me that it keeps gaining popularity (even outside of .NET development). When applied correctly, it helps with writing Clean Code so that the resulting code is easy to read, understand, test and maintain.

In this blog article, which is part of a mini series on MVVM Source Generators, I will show how you can take advantage of the MVVM Community Toolkit and why you should use Source Generators in your development projects.

Important: This article assumes prior knowledge about the MVVM pattern and how it is used in .NET applications.

If you are completely new to MVVM, please consider familiarizing yourself with it first and learn about the INotifyPropertyChanged interface as well as data bindings and commands. There is a popular introductory video for MVVM on YouTube by James Montemagno.

I strongly believe that newcomers should first learn how to implement MVVM in .NET in the conventional way before taking shortcuts using Source Generators in order to avoid frustration.

For the reason of convenience (and because I cannot get enough of it), I will use a .NET MAUI project (check out the sample repository), but the ViewModel could be from any other .NET based application, since the MVVM Community Toolkit is agnostic to application and UI development frameworks such as WPF, Xamarin.Forms or .NET MAUI. Therefore, even if you're not familiar yet with .NET MAUI, you will still want to keep reading on.

Why use Source Generators?

While the MVVM pattern is a fantastic way to write maintainable and unit-testable applications, it requires a lot of boilerplate code. For every property and every command, we need to implement a backing field, setters and getters and raise an event to notify the UI that something has changed, like so:

private string _firstName;
public string FirstName
{
    get => _firstName;
    set
    {
        if (_firstName.Equals(value))
        {
            return;
        }

        _firstName = value;
        OnPropertyChanged();
    }
}

This can quickly become expensive, because on top of the actual business logic there is a lot of additional code that needs to be maintained. This costs valuable development time and increases the risk of bugs, e.g. because developers like to use copy/paste a lot and may occasionally forget to update the backing fields.

Anecdote: A former colleague of mine used to call this behavior "copy pasta" as a loose reference to the messiness of spaghetti code

In order to avoid those common mistakes and help with the Don't Repeat Yourself (DRY) principle, the MVVM Community Toolkit helps us to reduce the amount of this boilerplate code. It does so in several ways:

  • It provides default implementations of the INotifyPropertyChanged and INotifyPropertyChanging interfaces (e.g. ObservableObject).

  • It comes with SetProperty() methods which set the backing field for a property and raise the PropertyChanged and PropertyChanging events for us, but only if the new value differs from the current value of the backing field.

  • Instead of fully implementing each property and each command with all their backing fields, setters and getters, we can just let the Source Generators do it for us.

This drastically reduces development time and our code files look much cleaner. So, by using Source Generators, our code from above can actually be reduced to writing a one-liner like this one:

[ObservableProperty] private string _firstName;

Before getting deeper into this and learning about how all this works, let's have a look at a common ViewModel that implements the INotifyPropertyChanged interface and after that, we'll see how it changes when we use MVVM Source Generators.

Setting

Let's assume that we want to implement an application that let's us enter a name and address to generate an address label. The application should have a live preview for the label which updates automatically when any of the input data changes as well as a printing function (we won't actually print anything; instead, for simplicity, I just open a popup displaying the entered data).

Our ViewModel therefore needs a couple of properties and a command:

public string FirstName { get; set; }
public string LastName { get; set; }
public string StreetAddress { get; set; }
public string PostCode { get; set; }
public string City { get; set; }
public string FullAddress { get; }
public ICommand PrintAddressCommand { get; }

We can bind to those properties and the command in our XAML and this will not change throughout this blog post, even with the Source Generators:

<Label Text="{Binding FullAddress}" />
<Button Text="Print Address" Command="{Binding PrintAddressCommand}" />

Whenever the FirstName, LastName, StreetAddress, PostCode or City property changes, the FullAddress property should be live-updated in our View, which looks like this:

When the Print Address button is clicked, the PrintAddressCommand will open a popup that displays the address (which we will see further down).

Note: I am skipping the rest of the UI code and only focus on the ViewModel in this blog post. If you want to see the whole code in action including the actual Views, check out the sample repository.

Let's have a look at what the fully implemented ViewModel looks like.

Full ViewModel without Source Generators

I've called it AddressViewModel and implemented the INotifyPropertyChanged interface without using any of the existing helper classes, just to demonstrate what a manually implemented ViewModel commonly looks (or better used to look) like:

using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text;
using System.Windows.Input;

namespace MauiSamples.ViewModels;

public sealed class AddressViewModel : INotifyPropertyChanged
{
    public delegate void PrintAddressDelegate(string address);
    public PrintAddressDelegate OnPrintAddress = null;

    private ICommand _showAddressCommand;
    public ICommand ShowAddressCommand => _showAddressCommand ??= new Command(PrintAddress);

    private string _firstName;
    public string FirstName
    {
        get => _firstName;
        set
        {
            if (SetField(ref _firstName, value))
            {
                OnPropertyChanged(nameof(FullAddress));
            }
        }
    }

    private string _lastName;
    public string LastName
    {
        get => _lastName;
        set
        {
            if (SetField(ref _lastName, value))
            {
                OnPropertyChanged(nameof(FullAddress));
            }
        }
    }

    private string _streetAddress;
    public string StreetAddress
    {
        get => _streetAddress;
        set
        {
            if (SetField(ref _streetAddress, value))
            {
                OnPropertyChanged(nameof(FullAddress));
            }
        }
    }

    private string _postCode;
    public string PostCode
    {
        get => _postCode;
        set
        {
            if (SetField(ref _postCode, value))
            {
                OnPropertyChanged(nameof(FullAddress));
            }
        }
    }

    private string _city;
    public string City
    {
        get => _city;
        set
        {
            if (SetField(ref _city, value))
            {
                OnPropertyChanged(nameof(FullAddress));
            }
        }
    }

    public string FullAddress
    {
        get
        {
            var stringBuilder = new StringBuilder();

            stringBuilder
                .AppendLine($"{FirstName} {LastName}")
                .AppendLine(StreetAddress)
                .AppendLine($"{PostCode} {City}");

            return stringBuilder.ToString();
        }
    }

    private void PrintAddress()
    {
        OnPrintAddress?.Invoke(FullAddress);
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    private bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value))
        {
            return false;
        }

        field = value;
        OnPropertyChanged(propertyName);
        return true;
    }
}

Uff, that's a lot of code for just a couple of properties and one measly command! So much repetition... It could be even worse, if the equality comparison had been implemented separately in each property, as well.

The code above technically is fine and sound, but doing the same thing over and over again for all the different ViewModels an application might have, eventually becomes quite tedious and is error-prone.

Note: For the example scenario, we don't need the INotifyPropertyChanging interface, so I didn't implement that here.

Before we can reduce the amount of code and use the Source Generators, we have to set up a few things and understand how to use the MVVM Community Toolkit. Let's do that next.

How to use the MVVM Community Toolkit

Setup

Before we can start, we need to download and install the CommunityToolkit.MVVM package from nuget.org in our C# project. At the time of writing, the latest stable release is 8.0.0, but we will already dig into the 8.1.0-preview2, so let's install that one (make sure to tick the Include prerelease checkbox next to the search bar), because it comes with some additional features (which I will explore in a separate blog post):

Disclaimer: Preview packages may be unstable or come with breaking changes

Once installed, we can pull in the required classes and attributes by adding the following using statement to a ViewModel:

using CommunityToolkit.Mvvm.ComponentModel;

Note: The MVVM Community Toolkit also provides regular MVVM functionality and base classes that can be used to simplify ViewModels and I highly recommend using it with or without the Source Generators.

Afterwards, we can use the ObservableObject class as a base class or use the [INotifyPropertyChanged] attribute on a partial class to automagically generate the INotifyPropertyChanged implementation for us.

Use the ObservableObject base class if your class does not need to inherit from another base class:

using CommunityToolkit.Mvvm.ComponentModel;

namespace MauiSamples.ViewModels;

public partial class AddressViewModel : ObservableObject { /*...*/ }

Otherwise, if your class already inherits from another base class (C# only supports single inheritance), then you can use the [INotifyPropertyChanged] attribute (and optionally also the [INotifyPropertyChanging] attribute) instead:

using CommunityToolkit.Mvvm.ComponentModel;

namespace MauiSamples.ViewModels;

[INotifyPropertyChanged]
[INotifyPropertyChanging] //optional
public partial class AddressViewModel : SomeOtherBaseClass { /*...*/ }

Note: When using the [INotifyPropertyChanged] attribute or ObservableObject base class, your class must be declared as partial, because Source Generators create code that complements the class. The [INotifyPropertyChanged] attribute should only be used when your ViewModel already inherits from another base class.

Attributes

The MVVM Community Toolkit uses C# attributes to let the MVVM Source Generators know that something should be auto-generated. Attributes can be placed above or in front of fields and method signatures.

When using the Source Generators, you'll probably be using the following attributes more frequently than any of the others:

[ObservableProperty]

This attribute is used to auto-generate a property for a backing field. The resulting property, by convention, always begins with an uppercase letter. Backing fields must be declared private and start with a lowercase letter or with an underscore (_):

//generates a property called "Age"
[ObservableProperty]
private int age; 

//generates a property called "Year"
[ObservableProperty] private int _year;

We only specify the backing fields for the properties, but to access them, we still need to use the appropriate property identifiers. They can be bound to via accessing the Age and Year from within the XAML code:

<Label Text="{Binding Age}" />
<Label Text="{Binding Year}" />

[RelayCommand]

In order to auto-generate a command, you can simply place this attribute above or in front of a method. The resulting command will have the same name as the method, but with the Command suffix:

//generates a RelayCommand called "SayHelloCommand"
[RelayCommand]
private void SayHello()
{
    Console.WriteLine("Hello);
}

The [RelayCommand] attribute can also be used on asynchronous methods, which will actually create an instance of AsyncRelayCommand under the hood, which is very convenient when using MVVM with .NET MAUI:

//generates an AsyncRelayCommand called "SayHelloAsyncCommand"
[RelayCommand] private async Task SayHelloAsync()
{
    await MessageService.ShowMessageAsync("Hello");
}

The resulting commands can be bound to via accessing SayHelloCommand and SayHelloAsyncCommand from the XAML code:

<Button Command="{Binding SayHelloCommand}" />
<Button Command="{Binding SayHelloAsyncCommand}" />

[NotifyPropertyChangedFor]

Sometimes, you also want to inform the consumer of property that another property has changed as well and that any bindings should be updated. That's what [NotifyPropertyChangedFor] attribute is for and it only works in combination with the [ObservableProperty] attribute. While the other attributes do not require an argument, this one requires the name of the property for which a PropertyChanged event should be raised:

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
private string _firstName;

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
private string _lastName;

//a PropertyChanged event will be raised for this property
//whenever either the FirstName or LastName property changes
public string FullName => $"{FirstName} {LastName}";

Updating the ViewModel with Source Generators

Now that we've learned about how the Source Generators can be used, let's apply them to reduce the size of our AddressViewModel step-by-step.

Hint: In the sample repository, I've actually added both versions of the ViewModel, so that you can compare them.

Properties

For each of the properties, we will change the following:

private string _firstName;
public string FirstName
{
    get => _firstName;
    set
    {
        if (SetField(ref _firstName, value))
        {
            OnPropertyChanged(nameof(FullAddress));
        }
    }
}

into:

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullAddress))]
private string _firstName;

and so forth.

Command

We can safely remove the following:

private ICommand _showAddressCommand;
public ICommand ShowAddressCommand => _showAddressCommand ??= new Command(PrintAddress);

and instead simply add the [RelayCommand] attribute above the PrintAddress() method:

[RelayCommand]
private void PrintAddress()
{
    OnPrintAddress?.Invoke(FullAddress);
}

Resulting ViewModel with Source Generators

The final result for the AddressViewModel with all the changes looks like this:

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Text;

namespace MauiSamples.ViewModels;

public partial class AddressViewModel : ObservableObject
{
    public delegate void PrintAddressDelegate(string address);
    public PrintAddressDelegate OnPrintAddress = null;

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(FullAddress))]
    private string _firstName;

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(FullAddress))]
    private string _lastName;

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(FullAddress))]
    private string _streetAddress;

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(FullAddress))]
    private string _postCode;

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(FullAddress))]
    private string _city;

    public string FullAddress
    {
        get
        {
            var stringBuilder = new StringBuilder();

            stringBuilder
                .AppendLine($"{FirstName} {LastName}")
                .AppendLine(StreetAddress)
                .AppendLine($"{PostCode} {City}");

            return stringBuilder.ToString();
        }
    }

    [RelayCommand]
    private void PrintAddress()
    {
        OnPrintAddress?.Invoke(FullAddress);
    }
}

๐ŸŽ‰ WOW, we've just saved about 50% of the lines of code compared to the initial version of the ViewModel. Pretty impressive, isn't it?

When we run the code again, our application still behaves exactly like before. Pressing the button will open the popup with the address:

Awesome, MVVM Source Generators FTW! ๐Ÿ†

Under the hood

Now, what actually happens when the properties and commands are generated? Since we've marked our ViewModel as being a partial class, the Source Generators can create more parts for it in separate files, like the properties and commands based on the attributes we've provided.

Since Source Generators in C# are built on top of the Code Analyzers of the .NET Compiler Platform (Roslyn), they run after making an edit to an open C# file. That way, the generated sources are always available to the C# compiler. We can find the auto-generated files (which always end in .g.cs) for our project in the Solution Explorer under Dependencies -> net7.0 -> Analyzers -> CommunityToolkit.Mvvm.SourceGenerators.

If we select the ObservablePropertyGenerator we can find a file ending in AddressViewModel.g.cs, which contains our auto-generated properties with their setters and getters, e.g. for our FirstName property:

partial class AddressViewModel
{
    /// <inheritdoc cref="_firstName"/>
    [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.1.0.0")]
    [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
    public string FirstName
    {
        get => _firstName;
        set
        {
            if (!global::System.Collections.Generic.EqualityComparer<string>.Default.Equals(_firstName, value))
            {
                OnFirstNameChanging(value);
                OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.FirstName);
                _firstName = value;
                OnFirstNameChanged(value);
                OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.FirstName);
                OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.FullAddress);
            }
        }
    }

    // ...
}

As we can see, a default EqualityComparer is used for the _firstName backing field which is of type string. Only if the strings of the backing field and the value object differ, the PropertyChanging event is raised before updating the backing field to the provided value. Once the backing field was updated, the PropertyChanged event is raised, but not just for our FirstName property, it is also raised for the FullAddress property, because we added the [NotifyPropertyChangedFor(nameof(FullAddress))] attribute above our _firstName backing field.

If you look closely, you'll notice that there are not just OnPropertyChanging() and OnPropertyChanged() method calls, but also calls to an OnFirstNameChanging() and an OnFirstNameChanged() method. The first two methods raise the PropertyChanging and PropertyChanged events respectively, while the other two are actually partial methods without a body, which are declared further down in the auto-generated source file:

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

๐Ÿ’ก These can be used to add custom functionality to our auto-generated property setters. How this can be done will be part of an upcoming blog post, so stay tuned!

Note: The OnPropertyChanging() and OnFirstNameChanging() methods were created in this example, because the ViewModel inherits from ObservableObject, which implements both the INotifyPropertyChanging and the INotifyPropertyChanged interfaces.

Conclusion and next steps

Reducing boilerplate code in your MVVM application has become even simpler now with the MVVM Source Generators of the MVVM Community Toolkit. As I have demonstrated, they are very convenient and super easy to use - provided that you are already somewhat familiar with the MVVM pattern in .NET applications and know what you are doing.

So far, I have shown the most straightforward and simple use cases with the common [ObservableProperty], [RelayCommand] and [NotifyPropertyChangedFor] attributes. If you would like to learn more about this topic, then stay tuned, because I will soon write another blog post about advanced scenarios, such as adding custom functionality to auto-generated property setters and how to get rid of annoying busy flags that technically don't belong into the business logic ๐Ÿคฏ.

Microsoft's brilliant James Montemagno also made a cool YouTube video about the MVVM Source Generators of the MVVM Community Toolkit. Check it out, if you like. It helped me get started, as well.

A big thank you to Gerald Versluis for reviewing this blog post for me. Your input was truly valuable. ๐Ÿ’

This blog post was written and published as part of the 2022 .NET Advent Calendar by Dustin Moris.

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.

ย