The Observable pattern is widely used for binding user interface controls to their underlying data, especially in projects that follow 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 then 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:
- The [Observable] aspect, which automatically implements the INotifyPropertyChanged interface for the target type
- The ConfigureObservability extension methods, designed to be called from a fabric
- The [Constant] attribute, which ensures that the output of a method is constant for identical parameters
Benefits
The primary benefits of using Metalama.Patterns.Observability include:
- Dramatic reduction of boilerplate code linked to INotifyPropertyChanged
- Safety from human errors—never forget to raise a notification, and get warnings if a dependency or code construct isn't 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
- Add a reference to the
Metalama.Patterns.Observabilitypackage. - Add the [Observable] attribute to each class requiring the INotifyPropertyChanged interface. Note that the Observable aspect is automatically inherited; you don't need to add the attribute to derived classes if the aspect has been applied to a base class.
- Consider making these classes
partialif you need the source code to see that these classes now implement the INotifyPropertyChanged interface. - Check for
LAMA51xxwarnings in your code. They highlight situations that aren't supported by the Observable aspect and require manual handling. This is described in the next section.
Here's an example of the code generated by the Observable aspect for a simple case:
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}
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 isn't supported, the Observable aspect reports a warning.
Here's an example of code where a computed property depends on an unsupported method.
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 observed, 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}
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 {
Warning LAMA5162: The 'VectorHelper.ComputeNorm(Vector)' method cannot be observed, and has not been configured with an observability contract. Mark this method with [ConstantAttribute] or call ConfigureObservability via a fabric.
14 get
Warning LAMA5162: The 'VectorHelper.ComputeNorm(Vector)' method cannot be observed, and has not been configured with an observability contract. Mark this method with [ConstantAttribute] or call ConfigureObservability via a fabric.
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 address these warnings:
Ignoring the warning
If the warning is a false positive, use the #pragma warning disable syntax to ignore it.
To disable all warnings in a member, use the [SuppressObservabilityWarnings] attribute, which is provided for find-and-replace-all compatibility with PostSharp.
Warning
These warnings indicate that a dependency won't be handled by the generated code. Suppressing the warning has no effect on the generated code.
Example: SuppressObservabilityWarnings
In the following example, we skip the warning using the [SuppressObservabilityWarnings] attribute. Note that this makes the code incorrect because the Observable aspect still doesn't notify a change of the Norm property when X or Y is changed.
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}
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 this property's value changes every instant, you should implement another method of notifying changes—for instance, using a timer.
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}
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're known to be constant—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
thisfor non-static methods) are of an immutable type. Immutability is handled using theMetalama.Patterns.Immutabilitypatterns. For details, see Metalama.Patterns.Immutability. voidmethods withoutoutarguments.- 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, 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
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}
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
1using Metalama.Framework.Fabrics;
2using Metalama.Patterns.Observability;
3using Metalama.Patterns.Observability.Configuration;
4using System;
5
6namespace Doc.Constant_Fabric;
7
8[Observable]
9public class Vector
10{
11 public double X { get; set; }
12
13 public double Y { get; set; }
14
15 public double Norm => VectorHelper.ComputeNorm( this );
16
17 public Vector Direction => VectorHelper.Normalize( this );
18}
19
20public static class VectorHelper
21{
22 public static double ComputeNorm( Vector v ) => Math.Sqrt( (v.X * v.X) + (v.Y * v.Y) );
23
24 public static Vector Normalize( Vector v )
25 {
26 var norm = ComputeNorm( v );
27
28 return new Vector { X = v.X / norm, Y = v.Y / norm };
29 }
30}
31
32public class Fabric : ProjectFabric
33{
34 public override void AmendProject( IProjectAmender amender )
35 {
36 amender.SelectReflectionType( typeof(VectorHelper) )
37 .ConfigureObservability( builder => builder.ObservabilityContract =
38 ObservabilityContract.Constant );
39 }
40}
1using Metalama.Framework.Fabrics;
2using Metalama.Patterns.Observability;
3using Metalama.Patterns.Observability.Configuration;
4using System;
5using System.ComponentModel;
6
7namespace Doc.Constant_Fabric;
8
9[Observable]
10public class Vector : INotifyPropertyChanged
11{
12 private double _x;
13
14 public double X
15 {
16 get
17 {
18 return _x;
19 }
20
21 set
22 {
23 if (_x != value)
24 {
25 _x = value;
26 OnPropertyChanged("X");
27 }
28 }
29 }
30
31 private double _y;
32
33 public double Y
34 {
35 get
36 {
37 return _y;
38 }
39
40 set
41 {
42 if (_y != value)
43 {
44 _y = value;
45 OnPropertyChanged("Y");
46 }
47 }
48 }
49
50 public double Norm => VectorHelper.ComputeNorm(this);
51
52 public Vector Direction => VectorHelper.Normalize(this);
53
54 protected virtual void OnPropertyChanged(string propertyName)
55 {
56 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
57 }
58
59 public event PropertyChangedEventHandler? PropertyChanged;
60}
61
62public static class VectorHelper
63{
64 public static double ComputeNorm(Vector v) => Math.Sqrt((v.X * v.X) + (v.Y * v.Y));
65
66 public static Vector Normalize(Vector v)
67 {
68 var norm = ComputeNorm(v);
69
70 return new Vector { X = v.X / norm, Y = v.Y / norm };
71 }
72}
73
74
75#pragma warning disable CS0067, CS8618, CS0162, CS0169, CS0414, CA1822, CA1823, IDE0051, IDE0052
76
77public class Fabric : ProjectFabric
78{
79 public override void AmendProject(IProjectAmender amender) => throw new System.NotSupportedException("Compile-time-only code cannot be called at run-time.");
80}
81
82#pragma warning restore CS0067, CS8618, CS0162, CS0169, CS0414, CA1822, CA1823, IDE0051, IDE0052
83
84
Working with manual implementations of INotifyPropertyChanged
The Observable aspect also works when the type already implements the INotifyPropertyChanged interface. In this case, the aspect only instruments the fields and properties.
However, if the type already implements the INotifyPropertyChanged interface, 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.