Open sandboxFocusImprove this doc

Scenarios Supported by the Observable Aspect

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.

Source Code
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}
Transformed Code
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).

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

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.

Source Code
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}
Transformed Code
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.

Source Code
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}
Transformed Code
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.

Source Code
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}
Transformed Code
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.

Source Code
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}
Transformed Code
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}