Skip to content

Latest commit

 

History

History
171 lines (126 loc) · 8.95 KB

File metadata and controls

171 lines (126 loc) · 8.95 KB
uid
sample-notifypropertychanged

Example: INotifyPropertyChanged

[!metalama-project-buttons .]

xref:System.ComponentModel.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 xref:System.ComponentModel.INotifyPropertyChanged.PropertyChanged event. Implementing this interface in data models increases their reusability across different views. The xref:System.ComponentModel.INotifyPropertyChanged is supported by most .NET UI frameworks including WPF, WinForms, WinUI and Blazor.

However, implementing xref:System.ComponentModel.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 xref:System.ComponentModel.INotifyPropertyChanged.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 xref:System.ComponentModel.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.

[!metalama-compare MovingVertex.cs]

Implementation

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 xref:System.ComponentModel.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 xref:System.ComponentModel.INotifyPropertyChanged.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.

[!metalama-file NotifyPropertyChangedAttribute.cs]

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

The xref:Metalama.Framework.Aspects.InheritableAttribute?text=[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 xref:aspect-inheritance.

Let's examine the implementation of the BuildAspect method.

[!metalama-file NotifyPropertyChangedAttribute.cs member="NotifyPropertyChangedAttribute.BuildAspect"]

The BuildAspect method first calls xref:Metalama.Framework.Advising.IAdviceFactory.ImplementInterface* to add the xref:System.ComponentModel.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 xref:Metalama.Framework.Advising.IAdviceFactory.ImplementInterface* method requires the interface members to be implemented by the aspect class and to be annotated with the xref:Metalama.Framework.Aspects.InterfaceMemberAttribute?text=[InterfaceMember] custom attribute. Here, our only member is the PropertyChanged event:

[!metalama-file NotifyPropertyChangedAttribute.cs member="NotifyPropertyChangedAttribute.PropertyChanged"]

To read more about this, see xref: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 xref:Metalama.Framework.Aspects.InterfaceMemberAttribute?text=[InterfaceMember] custom attribute but by using the xref:Metalama.Framework.Aspects.IntroduceAttribute?text=[Introduce] attribute. We again assign the Ignore value to the xref:Metalama.Framework.Aspects.IntroduceAttribute.WhenExists property, so we skip this step if the target type already contains this method.

[!metalama-file NotifyPropertyChangedAttribute.cs member="NotifyPropertyChangedAttribute.OnPropertyChanged"]

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 xref: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 xref:Metalama.Framework.Advising.IAdviceFactory.OverrideAccessors* using OverridePropertySetter as a template for the new property setter. For further details, see xref:overriding-fields-or-properties..

Let's have a look at this template:

[!metalama-file NotifyPropertyChangedAttribute.cs member="NotifyPropertyChangedAttribute.OverridePropertySetter"]

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. Note that all trivial implementations of xref:System.ComponentModel.INotifyPropertyChanged suffer from the same limitations. The only robust implementation we know about is the one of PostSharp.

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}"
}

Currently, Metalama doesn't provide any way to analyze dependent properties. This will be remediated in a future version.

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.

[!div class="see-also"] xref:aspect-inheritance xref:implementing-interfaces xref:overriding-fields-or-properties xref:introducing-members

Limitation 3. Insufficient precondition checking

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