The Metalama.Patterns.Observability
package supports the following scenarios:
- Automatic properties;
- Explicitly-implemented properties whose getter references:
- fields,
- other properties,
- non-virtual instance methods;
- Child objects, i.e., properties whose getter references properties of another object, referred to as a child object, stored in a field or an automatic property of the current type (if this child object is itself observable);
- Class inheritance.
In this section, we present the code generation patterns for each supported scenario.
Automatic properties
The code pattern for automatic properties is straightforward. The automatic property is transformed into a field-backed property. A new OnPropertyChanged
method is introduced unless it already exists.
1using Metalama.Patterns.Observability;
2
3namespace Doc.Simple;
4
5[Observable]
6public class Person
7{
8 public string? FirstName { get; set; }
9
10 public string? LastName { get; set; }
11}
1using System.ComponentModel;
2using Metalama.Patterns.Observability;
3
4namespace Doc.Simple;
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("FirstName");
24 }
25 }
26 }
27
28 private string? _lastName;
29
30 public string? LastName
31 {
32 get
33 {
34 return _lastName;
35 }
36
37 set
38 {
39 if (!object.ReferenceEquals(value, _lastName))
40 {
41 _lastName = value;
42 OnPropertyChanged("LastName");
43 }
44 }
45 }
46
47 protected virtual void OnPropertyChanged(string propertyName)
48 {
49 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
50 }
51
52 public event PropertyChangedEventHandler? PropertyChanged;
53}
54
Explicitly-implemented properties
The [Observable] aspect analyzes the dependencies between all properties in the type and calls the OnPropertyChanged
method for computed properties (also known as read-only properties).
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
Field-backed properties
Mutable fields are converted into properties of the same name, and the setter of the new property calls OnPropertyChanged
for all relevant dependent properties.
1// ReSharper disable ConvertToAutoProperty
2
3using Metalama.Patterns.Observability;
4
5namespace Doc.FieldBacked;
6
7[Observable]
8public class Person
9{
10 private string? _firstName;
11 private string? _lastName;
12
13 public string? FirstName
14 {
15 get => this._firstName;
16 set => this._firstName = value;
17 }
18
19 public string? LastName
20 {
21 get => this._lastName;
22 set => this._lastName = value;
23 }
24}
1// ReSharper disable ConvertToAutoProperty
2
3using System.ComponentModel;
4using Metalama.Patterns.Observability;
5
6namespace Doc.FieldBacked;
7
8[Observable]
9public class Person : INotifyPropertyChanged
10{
11 private string? _firstName1;
12
13 private string? _firstName
14 {
15 get
16 {
17 return _firstName1;
18 }
19
20 set
21 {
22 if (!object.ReferenceEquals(value, _firstName1))
23 {
24 _firstName1 = value;
25 OnPropertyChanged("FirstName");
26 }
27 }
28 }
29
30 private string? _lastName1;
31
32 private string? _lastName
33 {
34 get
35 {
36 return _lastName1;
37 }
38
39 set
40 {
41 if (!object.ReferenceEquals(value, _lastName1))
42 {
43 _lastName1 = value;
44 OnPropertyChanged("LastName");
45 }
46 }
47 }
48
49 public string? FirstName
50 {
51 get
52 {
53 return FirstName_Source;
54 }
55 set
56 {
57 if (!object.ReferenceEquals(value, FirstName_Source))
58 {
59 FirstName_Source = value;
60 OnPropertyChanged("FirstName");
61 }
62 }
63 }
64
65 private string? FirstName_Source {
66 get => this._firstName;
67 set => this._firstName = value;
68 }
69
70 public string? LastName
71 {
72 get
73 {
74 return LastName_Source;
75 }
76 set
77 {
78 if (!object.ReferenceEquals(value, LastName_Source))
79 {
80 LastName_Source = value;
81 OnPropertyChanged("LastName");
82 }
83 }
84 }
85
86 private string? LastName_Source {
87 get => this._lastName;
88 set => this._lastName = value;
89 }
90
91 protected virtual void OnPropertyChanged(string propertyName)
92 {
93 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
94 }
95
96 public event PropertyChangedEventHandler? PropertyChanged;
97}
98
Derived types
When a derived type has a property whose getter references a property of the base type, the [Observable] aspect overrides the OnPropertyChanged
method, filters the property name, and recursively calls the OnPropertyChanged
method for all dependent properties.
1using Metalama.Patterns.Observability;
2
3namespace Doc.Derived;
4
5[Observable]
6public class Person
7{
8 public string? FirstName { get; set; }
9
10 public string? LastName { get; set; }
11}
12
13public class PersonWithFullName : Person
14{
15 public string FullName => $"{this.FirstName} {this.LastName}";
16}
1using System.ComponentModel;
2using Metalama.Patterns.Observability;
3
4namespace Doc.Derived;
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("FirstName");
24 }
25 }
26 }
27
28 private string? _lastName;
29
30 public string? LastName
31 {
32 get
33 {
34 return _lastName;
35 }
36
37 set
38 {
39 if (!object.ReferenceEquals(value, _lastName))
40 {
41 _lastName = value;
42 OnPropertyChanged("LastName");
43 }
44 }
45 }
46
47 protected virtual void OnPropertyChanged(string propertyName)
48 {
49 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
50 }
51
52 public event PropertyChangedEventHandler? PropertyChanged;
53}
54
55public class PersonWithFullName : Person
56{
57 public string FullName => $"{this.FirstName} {this.LastName}";
58
59 protected override void OnPropertyChanged(string propertyName)
60 {
61 switch (propertyName)
62 {
63 case "FirstName":
64 OnPropertyChanged("FullName");
65 break;
66 case "LastName":
67 OnPropertyChanged("FullName");
68 break;
69 }
70
71 base.OnPropertyChanged(propertyName);
72 }
73}
Child objects
In MVVM architectures, it is common for a property of the ViewModel to depend on a property of the Model object, which itself is a field or property of the ViewModel object.
When a property getter references a property of an object stored in another field or property (referred to as a child object in this context), the [Observable] aspect generates a SubscribeTo
method for the property containing the child object. This method subscribes to the PropertyChanged event of the child object.
1using Metalama.Patterns.Observability;
2
3namespace Doc.ChildObject;
4
5[Observable]
6public sealed class PersonViewModel
7{
8 public Person Person { get; set; }
9
10 public PersonViewModel( Person model )
11 {
12 this.Person = model;
13 }
14
15 public string? FirstName => this.Person.FirstName;
16
17 public string? LastName => this.Person.LastName;
18
19 public string FullName => $"{this.FirstName} {this.LastName}";
20}
1using System.ComponentModel;
2using Metalama.Patterns.Observability;
3
4namespace Doc.ChildObject;
5
6[Observable]
7public sealed class PersonViewModel : INotifyPropertyChanged
8{
9 private Person _person = default!;
10
11 public Person Person
12 {
13 get
14 {
15 return _person;
16 }
17
18 set
19 {
20 if (!object.ReferenceEquals(value, _person))
21 {
22 var oldValue = _person;
23 if (oldValue != null)
24 {
25 oldValue.PropertyChanged -= _handlePersonPropertyChanged;
26 }
27
28 _person = value;
29 OnPropertyChanged("FirstName");
30 OnPropertyChanged("FullName");
31 OnPropertyChanged("LastName");
32 OnPropertyChanged("Person");
33 SubscribeToPerson(value);
34 }
35 }
36 }
37
38 public PersonViewModel(Person model)
39 {
40 this.Person = model;
41 }
42
43 public string? FirstName => this.Person.FirstName;
44
45 public string? LastName => this.Person.LastName;
46
47 public string FullName => $"{this.FirstName} {this.LastName}";
48
49 private PropertyChangedEventHandler? _handlePersonPropertyChanged;
50
51 [ObservedExpressions("Person")]
52 private void OnChildPropertyChanged(string parentPropertyPath, string propertyName)
53 {
54 }
55
56 private void OnPropertyChanged(string propertyName)
57 {
58 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
59 }
60
61 private void SubscribeToPerson(Person value)
62 {
63 if (value != null)
64 {
65 _handlePersonPropertyChanged ??= HandlePropertyChanged;
66 value.PropertyChanged += _handlePersonPropertyChanged;
67 }
68
69 void HandlePropertyChanged(object? sender, PropertyChangedEventArgs e)
70 {
71 {
72 var propertyName = e.PropertyName;
73 switch (propertyName)
74 {
75 case "FirstName":
76 OnPropertyChanged("FirstName");
77 OnPropertyChanged("FullName");
78 OnChildPropertyChanged("Person", "FirstName");
79 break;
80 case "LastName":
81 OnPropertyChanged("FullName");
82 OnPropertyChanged("LastName");
83 OnChildPropertyChanged("Person", "LastName");
84 break;
85 default:
86 OnChildPropertyChanged("Person", propertyName);
87 break;
88 }
89 }
90 }
91 }
92
93 public event PropertyChangedEventHandler? PropertyChanged;
94}
95
Child objects and derived types
The complexity increases when a type depends on a property of a property of the base type. To support this scenario, the [Observable] aspect generates two additional methods in the base type: OnChildPropertyChanged
and OnObservablePropertyChanged
.
The OnChildPropertyChanged
method is called when a property of a child object has changed. Its role is to prevent derived classes from having to monitor the PropertyChanged event for their own needs. Instead, they can just override the method and add their own logic. This method is only generated if the base type itself has a dependency on a child property.
The OnObservablePropertyChanged
method is called when a property implementing INotifyPropertyChanged
has changed. The value of this method, compared to the standard OnPropertyChanged
, is to supply the old value of the property to allow child classes to subscribe and unsubscribe from the PropertyChanged event. When the OnChildPropertyChanged
is generated, the OnObservablePropertyChanged
may be redundant.
Both methods are annotated with the [ObservedExpressions]. They form a contract between the base type and the derived types and inform the derived types about the properties for which the methods will be invoked.
1using Metalama.Patterns.Observability;
2
3namespace Doc.ChildObject_Derived;
4
5[Observable]
6public class PersonViewModel
7{
8 public Person Person { get; set; }
9
10 public PersonViewModel( Person model )
11 {
12 this.Person = model;
13 }
14
15 public string? FirstName => this.Person.FirstName;
16
17 public string? LastName => this.Person.LastName;
18}
19
20[Observable]
21public class PersonViewModelWithFullName : PersonViewModel
22{
23 public PersonViewModelWithFullName( Person model ) : base( model ) { }
24
25 public string FullName => $"{this.FirstName} {this.LastName}, {this.Person.Title}";
26}
1using System.ComponentModel;
2using Metalama.Patterns.Observability;
3
4namespace Doc.ChildObject_Derived;
5
6[Observable]
7public class PersonViewModel : INotifyPropertyChanged
8{
9 private Person _person = default!;
10
11 public Person Person
12 {
13 get
14 {
15 return _person;
16 }
17
18 set
19 {
20 if (!object.ReferenceEquals(value, _person))
21 {
22 var oldValue = _person;
23 if (oldValue != null)
24 {
25 oldValue.PropertyChanged -= _handlePersonPropertyChanged;
26 }
27
28 _person = value;
29 OnObservablePropertyChanged("Person", oldValue, (INotifyPropertyChanged?)value);
30 OnPropertyChanged("FirstName");
31 OnPropertyChanged("LastName");
32 OnPropertyChanged("Person");
33 SubscribeToPerson(value);
34 }
35 }
36 }
37
38 public PersonViewModel(Person model)
39 {
40 this.Person = model;
41 }
42
43 public string? FirstName => this.Person.FirstName;
44
45 public string? LastName => this.Person.LastName;
46
47 private PropertyChangedEventHandler? _handlePersonPropertyChanged;
48
49 [ObservedExpressions("Person")]
50 protected virtual void OnChildPropertyChanged(string parentPropertyPath, string propertyName)
51 {
52 }
53
54 [ObservedExpressions("Person")]
55 protected virtual void OnObservablePropertyChanged(string propertyPath, INotifyPropertyChanged? oldValue, INotifyPropertyChanged? newValue)
56 {
57 }
58
59 protected virtual void OnPropertyChanged(string propertyName)
60 {
61 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
62 }
63
64 private void SubscribeToPerson(Person value)
65 {
66 if (value != null)
67 {
68 _handlePersonPropertyChanged ??= HandlePropertyChanged;
69 value.PropertyChanged += _handlePersonPropertyChanged;
70 }
71
72 void HandlePropertyChanged(object? sender, PropertyChangedEventArgs e)
73 {
74 {
75 var propertyName = e.PropertyName;
76 switch (propertyName)
77 {
78 case "FirstName":
79 OnPropertyChanged("FirstName");
80 OnChildPropertyChanged("Person", "FirstName");
81 break;
82 case "LastName":
83 OnPropertyChanged("LastName");
84 OnChildPropertyChanged("Person", "LastName");
85 break;
86 default:
87 OnChildPropertyChanged("Person", propertyName);
88 break;
89 }
90 }
91 }
92 }
93
94 public event PropertyChangedEventHandler? PropertyChanged;
95}
96
97[Observable]
98public class PersonViewModelWithFullName : PersonViewModel
99{
100 public PersonViewModelWithFullName(Person model) : base(model) { }
101
102 public string FullName => $"{this.FirstName} {this.LastName}, {this.Person.Title}";
103
104 protected override void OnChildPropertyChanged(string parentPropertyPath, string propertyName)
105 {
106 switch (parentPropertyPath, propertyName)
107 {
108 case ("Person", "Title"):
109 OnPropertyChanged("FullName");
110 base.OnChildPropertyChanged(parentPropertyPath, propertyName);
111 break;
112 }
113
114 base.OnChildPropertyChanged(parentPropertyPath, propertyName);
115 }
116
117 protected override void OnPropertyChanged(string propertyName)
118 {
119 switch (propertyName)
120 {
121 case "FirstName":
122 OnPropertyChanged("FullName");
123 break;
124 case "LastName":
125 OnPropertyChanged("FullName");
126 break;
127 case "Person":
128 OnPropertyChanged("FullName");
129 break;
130 }
131
132 base.OnPropertyChanged(propertyName);
133 }
134}