Open sandboxFocusImprove this doc

Implementing INotifyPropertyChanged without boilerplate

INotifyPropertyChanged is an essential interface in the .NET Framework, especially for applications using data binding or the MVVM pattern. It enables automatic UI updates when properties in the data model change by raising the PropertyChanged event. Implementing this interface in data models increases their reusability across different views. The INotifyPropertyChanged is supported by most .NET UI frameworks including WPF, WinForms, WinUI and Blazor.

However, implementing INotifyPropertyChanged often involves a significant amount of boilerplate code, making it cumbersome and time-consuming to maintain. Each property requires additional code to raise the PropertyChanged event not only for itself, but for all dependent properties, which can quickly become unwieldy in large data models. Fortunately, you can use an aspect to inject the necessary code automatically, reducing the manual effort required to implement the interface. With Metalama, you can maintain cleaner and more manageable code, focusing on the core logic of their applications while still benefiting from the responsive UI updates and improved data binding that INotifyPropertyChanged provides.

In the following example, we show how source code is transformed by the aspect. The code generated by Metalama is displayed in green.

Source Code



1[NotifyPropertyChanged]
2internal partial class MovingVertex
3{

4    public double X { get; set; }



5
6    public double Y { get; set; }

7
8    public double DX { get; set; }

9
10    public double DY { get; set; }
11
12    public void ApplyTime(double time)
13    {
14        this.X += this.DX * time;
15        this.Y += this.DY * time;




16    }

17}
Transformed Code
1using System;
2using System.ComponentModel;
3
4[NotifyPropertyChanged]
5internal partial class MovingVertex
6: INotifyPropertyChanged
7{
8private double _x;
9
10    public double X { get { return this._x; } set { if (value != this._x) { this._x = value; OnPropertyChanged("X"); } return; } }
11    private double _y;
12
13    public double Y { get { return this._y; } set { if (value != this._y) { this._y = value; OnPropertyChanged("Y"); } return; } }
14    private double _dX;
15
16    public double DX { get { return this._dX; } set { if (value != this._dX) { this._dX = value; OnPropertyChanged("DX"); } return; } }
17    private double _dY;
18
19    public double DY { get { return this._dY; } set { if (value != this._dY) { this._dY = value; OnPropertyChanged("DY"); } return; } }
20
21    public void ApplyTime(double time)
22    {
23        this.X += this.DX * time;
24        this.Y += this.DY * time;
25    }
26protected void OnPropertyChanged(string name)
27    {
28        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
29    }
30    public event PropertyChangedEventHandler? PropertyChanged;
31}

[!INFO] The objective of this article is didactic. For a production-ready and battle-tested implementation of caching, use the Metalama.Patterns.Observability package. See Metalama.Patterns.Observability for details.

Implementation

Warning

The objective of this example is to teach you to build aspects of moderate complexity. For an open-source and production-ready implementation, see Metalama.Patterns.Observability.

In this example, we will explain the simplest possible implementation of the NotifyPropertyChanged aspect. As with any aspect, before starting to write code, it is good to take a pause and think a few minutes about the design. Here is how we want this aspect to work:

  1. The aspect will add the INotifyPropertyChanged interface to the target type unless that type already implements this interface.
  2. The aspect will add the OnPropertyChanged method unless the target type already contains it. This method will call the PropertyChanged event.
  3. The aspect will override the setter of all properties and call OnPropertyChanged.
  4. The aspect will automatically propagate from the base class to derived classes.

Note that this is a basic implementation. Specifically, it does not take into account dependent properties, i.e. properties that depend on other properties.

Here's the code for this aspect.

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using System.ComponentModel;
4
5[Inheritable]
6internal class NotifyPropertyChangedAttribute : TypeAspect
7{
8    public override void BuildAspect(IAspectBuilder<INamedType> builder)
9    {
10        builder.Advice.ImplementInterface(builder.Target, typeof(INotifyPropertyChanged),
11            OverrideStrategy.Ignore);
12
13        foreach (var property in builder.Target.Properties.Where(p =>
14                     !p.IsAbstract && p.Writeability == Writeability.All))
15        {
16            builder.Advice.OverrideAccessors(property, null, nameof(this.OverridePropertySetter));
17        }
18    }
19
20    [InterfaceMember] public event PropertyChangedEventHandler? PropertyChanged;
21
22    [Introduce(WhenExists = OverrideStrategy.Ignore)]
23    protected void OnPropertyChanged(string name) =>
24        this.PropertyChanged?.Invoke(meta.This, new PropertyChangedEventArgs(name));
25
26    [Template]
27    private dynamic OverridePropertySetter(dynamic value)
28    {
29        if (value != meta.Target.Property.Value)
30        {
31            meta.Proceed();
32            this.OnPropertyChanged(meta.Target.Property.Name);
33        }
34
35        return value;
36    }
37}

As can be seen from the code, the NotifyPropertyChangedAttribute class inherits the TypeAspect because this is an aspect that applies to types.

The [Inheritable] at the top of the class indicates that the aspect should be inherited from the base class to derived classes. For further details, see Applying aspects to derived types.

Let's examine the implementation of the BuildAspect method.

8public override void BuildAspect(IAspectBuilder<INamedType> builder)
9{
10    builder.Advice.ImplementInterface(builder.Target, typeof(INotifyPropertyChanged),
11        OverrideStrategy.Ignore);
12
13    foreach (var property in builder.Target.Properties.Where(p =>
14                 !p.IsAbstract && p.Writeability == Writeability.All))
15    {
16        builder.Advice.OverrideAccessors(property, null, nameof(this.OverridePropertySetter));
17    }
18}
19

The BuildAspect method first calls ImplementInterface to add the INotifyPropertyChanged interface to the target type. The whenExists parameter is set to Ignore, indicating that this call will just be ignored if the target type or any base type already implements the interface. The ImplementInterface method requires the interface members to be implemented by the aspect class and to be annotated with the [InterfaceMember] custom attribute. Here, our only member is the PropertyChanged event:

20[InterfaceMember] public event PropertyChangedEventHandler? PropertyChanged;
21

To read more about this, see Implementing interfaces.

Note that the OnPropertyChanged method is not a part of the System.ComponentModel.INotifyPropertyChanged interface. So we introduce it not by using the [InterfaceMember] custom attribute but by using the [Introduce] attribute. We again assign the Ignore value to the WhenExists property, so we skip this step if the target type already contains this method.

22[Introduce(WhenExists = OverrideStrategy.Ignore)]
23protected void OnPropertyChanged(string name) =>
24    this.PropertyChanged?.Invoke(meta.This, new PropertyChangedEventArgs(name));
25

The OnPropertyChanged method invokes the PropertyChanged event. Note that the expression meta.This is translated into simply this by Metalama. It represents the run-time object, while the this keyword in the aspect would represent the aspect itself. For further details about adding members, see Introducing members.

Now, moving back to the BuildAspect method. The next action it performs is to iterate through all properties that have a setter. It does this by calling the OverrideAccessors using OverridePropertySetter as a template for the new property setter. For further details, see Overriding fields or properties..

Let's have a look at this template:

26[Template]
27private dynamic OverridePropertySetter(dynamic value)
28{
29    if (value != meta.Target.Property.Value)
30    {
31        meta.Proceed();
32        this.OnPropertyChanged(meta.Target.Property.Name);
33    }
34
35    return value;
36}

The expression meta.Target.Property.Value gives the current value of the property. When Metalama applies the aspect to an automatic property, it turns it into a field-backed property, and this expression resolves the backing field.

meta.Proceed() invokes the original implementation of the property. In the case of an automatic property, this means that the backing field is set to the value parameter.

Finally, the template calls the OnPropertyChanged method. meta.Target.Property.Name translates to the name of the current property.

Limitations

This implementation has limitations that you should be aware of.

Limitation 1. Dependent properties

The first limitation of our implementation is that dependent properties are silently ignored. For instance, the following code won't raise any notification for the FullName property:

class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    // Notification never raised!
    public string FullName => $"{FirstName} {LastName}"
}
Tip

For an open-source and production-ready implementation that support dependent properties, see Metalama.Patterns.Observability.

Limitation 2. Timing of notifications

The second limitation is more subtle. When you have a method that modifies several properties, the notifications with the current implementation will raise notification in the middle of the modifications, as individual properties are being modified, but a correct implementation would need to raise the notifications at the end, after all properties have been modified.

Consider for instance the following code:

class InvoiceLine
{
    public decimal UnitPrice {get; private set; }
    public decimal Units {get; private set; }
    public decimal TotalPrice { get; private set; }

    public void Update( decimal unitPrice, decimal totalPrice )
    {
        this.UnitPrice = unitPrice;
        // PropertyChanged raised with broken invariants.
        this.Units = units;
        // PropertyChanged raised with broken invariants.
        this.TotalPrice = unitPrice * units;
    }
}

This class has an invariant TotalPrice = UnitPrice * Units. The PropertyChanged event will be raised in the middle of the Update class at a moment when class invariants are invalid. A listener that would process the event synchronously would see the InvoiceLine object in an invalid state. A proper implementation would buffer the events and raise them at the end of the Update method when all invariants are valid.

Contrarily to the first limitation, it's possible to address this limitation, but this is quite complex.

Limitation 3. Insufficient precondition checking

If the target type already implements the INotifyPropertyChanged interface but does not implement the OnPropertyChanged method, the aspect will generate invalid code. This limitation can be easily addressed with Metalama.