Open sandboxFocusImprove this doc

Metalama.Patterns.Observability

The Observable pattern is widely used for binding user interface controls to their underlying data, especially in projects that adhere to the MVVM architecture. In .NET, the standard interface for the Observable pattern is INotifyPropertyChanged. Typically, this interface is implemented by the types of the Model and View-Model layers. When a property of a Model object changes, it triggers the PropertyChanged event. This Model event is observed by the View-Model layer. If a property of a View-Model object is affected by this change, it subsequently triggers the PropertyChanged event. The View-Model event is eventually observed by the View layer, which updates the UI.

A second common element of the Observable pattern is the OnPropertyChanged method, the name of which can vary across different MVVM frameworks. A third element of this pattern is conventions about how the property setters should be implemented, possibly with some helper methods.

Metalama provides an open-source implementation of the Observable pattern in the Metalama.Patterns.Observability package. The principal artifacts in this package are:

Benefits

The primary benefits of using Metalama.Patterns.Observability include:

  • Dramatic reduction of the boilerplate code linked to INotifyPropertyChanged.
  • Safety from human errors:
    • Never forget to raise a notification again.
    • The package reports warnings if a dependency or code construct is not supported.
  • Idiomatic source code.
  • Almost idiomatic code generation.
  • Support for complex code constructs:
    • Automatic properties,
    • Explicitly-implemented properties,
    • Field-backed properties,
    • Properties that depend on child objects, a common scenario in MVVM architectures,
    • Properties that depend on methods,
    • Constant methods and immutable objects.
  • Compatibility with most MVVM frameworks.

Implementing INotifyPropertyChanged for a class hierarchy

  1. Add a reference to the Metalama.Patterns.Observability package.
  2. Add the [Observable] attribute to each class requiring the INotifyPropertyChanged interface. Note that the Observable aspect is automatically inherited; you don't need to manually add the attribute to derived classes if the aspect has been applied to a base class.
  3. Consider making these classes partial if you need the source code to "see" that these classes now implement the INotifyPropertyChanged interface.
  4. Check for LAMA51** warnings in your code. They highlight situations that are not supported by the Observable aspect and require manual handling. This is described in the next section.

Here is an example of the code generated by the Observable aspect for a simple case:

Source Code
1using Metalama.Patterns.Observability;
2

3namespace Doc.ComputedProperty;
4
5[Observable]
6public class Person
7{
8    public string? FirstName { get; set; }
9


10    public string? LastName { get; set; }


















11
12    public string FullName => $"{this.FirstName} {this.LastName}";





13}
Transformed Code
1using System.ComponentModel;
2using Metalama.Patterns.Observability;
3
4namespace Doc.ComputedProperty;
5
6[Observable]
7public class Person : INotifyPropertyChanged
8{
9    private string? _firstName;
10
11    public string? FirstName
12    {
13        get
14        {
15            return _firstName;
16        }
17
18        set
19        {
20            if (!object.ReferenceEquals(value, _firstName))
21            {
22                _firstName = value;
23                OnPropertyChanged("FullName");
24                OnPropertyChanged("FirstName");
25            }
26        }
27    }
28
29    private string? _lastName;
30
31    public string? LastName
32    {
33        get
34        {
35            return _lastName;
36        }
37
38        set
39        {
40            if (!object.ReferenceEquals(value, _lastName))
41            {
42                _lastName = value;
43                OnPropertyChanged("FullName");
44                OnPropertyChanged("LastName");
45            }
46        }
47    }
48
49    public string FullName => $"{this.FirstName} {this.LastName}";
50
51    protected virtual void OnPropertyChanged(string propertyName)
52    {
53        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
54    }
55
56    public event PropertyChangedEventHandler? PropertyChanged;
57}
58

Understanding and working around limitations

Before transforming a type, the Observable aspect analyzes the dependencies between different properties in this type; it builds a dependency graph, and this graph becomes the input of the source generation algorithm.

As stated in the introduction, the graph analysis understands references to fields, properties, properties of child objects (and recursively), and some methods. When a situation is not supported, the Observable aspect reports a warning.

Here is an example of code where a computed property depends on an unsupported method.

Source Code
1using Metalama.Patterns.Observability;
2using System;
3
4namespace Doc.Warning;

5
6[Observable]
7public class Vector
8{
9    public double X { get; set; }
10


11    public double Y { get; set; }















12


    Warning LAMA5162: The 'VectorHelper.ComputeNorm(Vector)' method cannot be analysed, and has not been configured with an observability contract. Mark this method with [ConstantAttribute] or call ConfigureObservability via a fabric.

13    public double Norm => VectorHelper.ComputeNorm( this );





14}
15

















16public static class VectorHelper
17{
18    public static double ComputeNorm( Vector v ) => Math.Sqrt( v.X * v.X + v.Y * v.Y );
19}
Transformed Code
1using Metalama.Patterns.Observability;
2using System;
3using System.ComponentModel;
4
5namespace Doc.Warning;
6
7[Observable]
8public class Vector : INotifyPropertyChanged
9{
10    private double _x;
11
12    public double X
13    {
14        get
15        {
16            return _x;
17        }
18
19        set
20        {
21            if (_x != value)
22            {
23                _x = value;
24                OnPropertyChanged("X");
25            }
26        }
27    }
28
29    private double _y;
30
31    public double Y
32    {
33        get
34        {
35            return _y;
36        }
37
38        set
39        {
40            if (_y != value)
41            {
42                _y = value;
43                OnPropertyChanged("Y");
44            }
45        }
46    }
47
48    public double Norm => VectorHelper.ComputeNorm(this);
49
50    protected virtual void OnPropertyChanged(string propertyName)
51    {
52        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
53    }
54
55    public event PropertyChangedEventHandler? PropertyChanged;
56}
57
58public static class VectorHelper
59{
60    public static double ComputeNorm(Vector v) => Math.Sqrt(v.X * v.X + v.Y * v.Y);
61}

Here are different ways to cope with these warnings:

Ignoring the warning

If you consider the warning to be a false positive, you can ignore it using the classic #pragma warning disable syntax.

If you want to disable all warnings in a member, you can also use the [SuppressObservabilityWarnings] attribute, which is provided for find-and-replace-all compatibility with PostSharp.

Warning

These warnings indicate that a dependency will not be handled by the generated code. Suppressing the warning, of course, has no effect on the generated code.

Example: SuppressObservabilityWarnings

In the following example, we skip the warning using [SuppressObservabilityWarnings] attribute. Note that this makes the code incorrect because the Observable aspect still does not notify a change of the Norm property when X or Y is changed.

Source Code
1using Metalama.Patterns.Observability;
2using System;
3
4namespace Doc.SuppressObservabilityWarnings;

5
6[Observable]
7public class Vector
8{
9    public double X { get; set; }
10


11    public double Y { get; set; }















12


13    // Note that we are suppressing the warning, but dependencies to X and Y are not





14    // taken into account!










15    [SuppressObservabilityWarnings]
16    public double Norm => VectorHelper.ComputeNorm( this );
17}
18







19public static class VectorHelper
20{
21    public static double ComputeNorm( Vector v ) => Math.Sqrt( (v.X * v.X) + (v.Y * v.Y) );
22}
Transformed Code
1using Metalama.Patterns.Observability;
2using System;
3using System.ComponentModel;
4
5namespace Doc.SuppressObservabilityWarnings;
6
7[Observable]
8public class Vector : INotifyPropertyChanged
9{
10    private double _x;
11
12    public double X
13    {
14        get
15        {
16            return _x;
17        }
18
19        set
20        {
21            if (_x != value)
22            {
23                _x = value;
24                OnPropertyChanged("X");
25            }
26        }
27    }
28
29    private double _y;
30
31    public double Y
32    {
33        get
34        {
35            return _y;
36        }
37
38        set
39        {
40            if (_y != value)
41            {
42                _y = value;
43                OnPropertyChanged("Y");
44            }
45        }
46    }
47
48    // Note that we are suppressing the warning, but dependencies to X and Y are not
49    // taken into account!
50    [SuppressObservabilityWarnings]
51    public double Norm => VectorHelper.ComputeNorm(this);
52
53    protected virtual void OnPropertyChanged(string propertyName)
54    {
55        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
56    }
57
58    public event PropertyChangedEventHandler? PropertyChanged;
59}
60
61public static class VectorHelper
62{
63    public static double ComputeNorm(Vector v) => Math.Sqrt((v.X * v.X) + (v.Y * v.Y));
64}

Skipping a property

To exclude a property from the change-notification mechanism, use the [NotObservable] attribute.

Example: NotObservable

In this example, we exclude a property that depends on DateTime.Now. Since the value of this property changes every instance, another method of notifying changes should be implemented — for instance, using a timer.

Source Code
1using Metalama.Patterns.Observability;
2using System;
3
4namespace Doc.Skipping;

5
6[Observable]
7public class DateTimeViewModel
8{
9    public DateTime DateTime { get; set; }
10


11    [NotObservable]















12    public double MinutesFromNow => (DateTime.Now - this.DateTime).TotalMinutes;
13}
Transformed Code
1using Metalama.Patterns.Observability;
2using System;
3using System.ComponentModel;
4
5namespace Doc.Skipping;
6
7[Observable]
8public class DateTimeViewModel : INotifyPropertyChanged
9{
10    private DateTime _dateTime;
11
12    public DateTime DateTime
13    {
14        get
15        {
16            return _dateTime;
17        }
18
19        set
20        {
21            if (_dateTime != value)
22            {
23                _dateTime = value;
24                OnPropertyChanged("DateTime");
25            }
26        }
27    }
28
29    [NotObservable]
30    public double MinutesFromNow => (DateTime.Now - this.DateTime).TotalMinutes;
31
32    protected virtual void OnPropertyChanged(string propertyName)
33    {
34        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
35    }
36
37    public event PropertyChangedEventHandler? PropertyChanged;
38}
39

Marking methods as constant

Calls to methods of different types are supported only if they are known to be constant, i.e., if subsequent calls with the exact same arguments are guaranteed to always return the same value.

The following methods are considered constant:

  • Methods where all input parameters (including this in case of non-static methods) are of an immutable type. Immutability is handled using the Metalama.Patterns.Immutability patterns. For details, see Metalama.Patterns.Immutability.
  • void methods without out arguments.
  • Methods marked as constants using the [Constant] custom attribute or using a fabric (see below).

One way to mark a method as constant is to add the [Constant] custom attribute.

If you want to mark many methods as constant, it may be more convenient to use the ConfigureObservability fabric method instead of adding the [Constant] attribute to each of them, and set the ObservabilityContract property to ObservabilityContract.Constant.

Example: marking a method as constant using a custom attribute

Source Code
1using Metalama.Patterns.Observability;
2using System;
3
4namespace Doc.Constant;

5
6[Observable]
7public class Vector
8{
9    public double X { get; set; }
10


11    public double Y { get; set; }





12













13    public double Norm => VectorHelper.ComputeNorm( this.X, this.Y );
















14}
15







16public static class VectorHelper
17{
18    //[Constant]
19    public static double ComputeNorm( double x, double y ) => Math.Sqrt( (x * x) + (y * y) );
20}
Transformed Code
1using Metalama.Patterns.Observability;
2using System;
3using System.ComponentModel;
4
5namespace Doc.Constant;
6
7[Observable]
8public class Vector : INotifyPropertyChanged
9{
10    private double _x;
11
12    public double X
13    {
14        get
15        {
16            return _x;
17        }
18
19        set
20        {
21            if (_x != value)
22            {
23                _x = value;
24                OnPropertyChanged("Norm");
25                OnPropertyChanged("X");
26            }
27        }
28    }
29
30    private double _y;
31
32    public double Y
33    {
34        get
35        {
36            return _y;
37        }
38
39        set
40        {
41            if (_y != value)
42            {
43                _y = value;
44                OnPropertyChanged("Norm");
45                OnPropertyChanged("Y");
46            }
47        }
48    }
49
50    public double Norm => VectorHelper.ComputeNorm(this.X, this.Y);
51
52    protected virtual void OnPropertyChanged(string propertyName)
53    {
54        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
55    }
56
57    public event PropertyChangedEventHandler? PropertyChanged;
58}
59
60public static class VectorHelper
61{
62    //[Constant]
63    public static double ComputeNorm(double x, double y) => Math.Sqrt((x * x) + (y * y));
64}

Example: marking several methods as constant using a fabric

Source Code
1using Metalama.Framework.Aspects;
2using Metalama.Framework.Fabrics;
3using Metalama.Patterns.Observability;
4using Metalama.Patterns.Observability.Configuration;
5using System;
6
7namespace Doc.Constant_Fabric;

8
9[Observable]
10public class Vector
11{
12    public double X { get; set; }
13


14    public double Y { get; set; }

















15
16    public double Norm => VectorHelper.ComputeNorm( this );



















17
18    public Vector Direction => VectorHelper.Normalize( this );



19}
20
21public static class VectorHelper
22{
23    public static double ComputeNorm( Vector v ) => Math.Sqrt( (v.X * v.X) + (v.Y * v.Y) );
24
25    public static Vector Normalize( Vector v )
26    {
27        var norm = ComputeNorm( v );
28
29        return new Vector { X = v.X / norm, Y = v.Y / norm };
30    }
31}
32
33public class Fabric : ProjectFabric



34{
35    public override void AmendProject( IProjectAmender amender )
36    {
37        amender.SelectReflectionType( typeof(VectorHelper) )
38            .ConfigureObservability(
39                builder => builder.ObservabilityContract = ObservabilityContract.Constant );
40    }
41}
Transformed Code
1using Metalama.Framework.Aspects;
2using Metalama.Framework.Fabrics;
3using Metalama.Patterns.Observability;
4using Metalama.Patterns.Observability.Configuration;
5using System;
6using System.ComponentModel;
7
8namespace Doc.Constant_Fabric;
9
10[Observable]
11public class Vector : INotifyPropertyChanged
12{
13    private double _x;
14
15    public double X
16    {
17        get
18        {
19            return _x;
20        }
21
22        set
23        {
24            if (_x != value)
25            {
26                _x = value;
27                OnPropertyChanged("X");
28            }
29        }
30    }
31
32    private double _y;
33
34    public double Y
35    {
36        get
37        {
38            return _y;
39        }
40
41        set
42        {
43            if (_y != value)
44            {
45                _y = value;
46                OnPropertyChanged("Y");
47            }
48        }
49    }
50
51    public double Norm => VectorHelper.ComputeNorm(this);
52
53    public Vector Direction => VectorHelper.Normalize(this);
54
55    protected virtual void OnPropertyChanged(string propertyName)
56    {
57        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
58    }
59
60    public event PropertyChangedEventHandler? PropertyChanged;
61}
62
63public static class VectorHelper
64{
65    public static double ComputeNorm(Vector v) => Math.Sqrt((v.X * v.X) + (v.Y * v.Y));
66
67    public static Vector Normalize(Vector v)
68    {
69        var norm = ComputeNorm(v);
70
71        return new Vector { X = v.X / norm, Y = v.Y / norm };
72    }
73}
74
75
76#pragma warning disable CS0067, CS8618, CS0162, CS0169, CS0414, CA1822, CA1823, IDE0051, IDE0052
77
78public class Fabric : ProjectFabric
79{
80    public override void AmendProject(IProjectAmender amender) => throw new System.NotSupportedException("Compile-time-only code cannot be called at run-time.");
81}
82




83#pragma warning restore CS0067, CS8618, CS0162, CS0169, CS0414, CA1822, CA1823, IDE0051, IDE0052
84
85

Coping with manual implementations of INotifyPropertyChanged

The Observable aspect also works when the type already implements the INotifyPropertyChanged interface. In this case, the aspect will only instrument the fields and properties.

However, if the type already implements the INotifyPropertyChanged interface, then the type must contain a method with exactly the following signature:

protected void OnPropertyChanged( string propertyName );

For compatibility with MVVM frameworks, this method can be named NotifyOfPropertyChange or RaisePropertyChanged instead of OnPropertyChanged.

This method will be used to raise notifications.

To react to notifications raised by the base class, the Observable aspect relies on overriding a virtual method with one of these signatures:

protected virtual void OnPropertyChanged( string propertyName );
protected virtual void OnPropertyChanged( PropertyChangedEventArgs args );

This method can also be named NotifyOfPropertyChange or RaisePropertyChanged instead of OnPropertyChanged.