Open sandboxFocusImprove this doc

LAMA5163: The property can't be observed through a computed property

Severity Warning
Reported by Metalama.Patterns.Observability
Message Changes to the children of the '<Property>' property cannot be observed because '<Property>' is not an auto-property. Access the child through an auto-property or a field of '<DeclaringType>' instead.

Cause

The [Observable] aspect supports computed properties that read properties of child objects, such as this.Address.City, when the type of the child object implements INotifyPropertyChanged. To track changes to City, the aspect subscribes to the PropertyChanged event of the object stored in Address, and it updates this subscription when Address changes.

This tracking only works when the parent property is an auto-property or a field. Metalama reports LAMA5163 when a computed property reads a child of a non-auto property declared on the same type, for example a property computed from other properties. The aspect doesn't detect that the child's value changes, so the computed property isn't notified. In the message, <Property> is the non-auto property and <DeclaringType> is the type that declares it.

For a property of a base type that doesn't implement INotifyPropertyChanged, Metalama reports LAMA5166 instead.

In the following example, DeliveryCity reads City through DeliveryAddress, which is a computed property.

Source Code
1using Metalama.Patterns.Observability;
2

3namespace Doc.LAMA5163.Error;
4
5[Observable]
6public partial class Customer
7{
8    public bool ShipToBillingAddress { get; set; } = true;
9




































10    public Address BillingAddress { get; set; } = new();












11
12    public Address ShippingAddress { get; set; } = new();





13
14    public Address DeliveryAddress => this.ShipToBillingAddress ? this.BillingAddress : this.ShippingAddress;








15








16    // Warning: DeliveryAddress isn't an auto-property, so changes to its City can't be observed.
    Warning LAMA5163: Changes to the children of the 'Customer.DeliveryAddress' property cannot be observed because 'Customer.DeliveryAddress' is not an auto-property. Access the child through an auto-property or a field of 'Customer' instead.

17    public string DeliveryCity => this.DeliveryAddress.City;
18}
19
Transformed Code
1using System.ComponentModel;
2using Metalama.Patterns.Observability;
3
4namespace Doc.LAMA5163.Error;
5
6[Observable]
7public partial class Customer : INotifyPropertyChanged
8{
9    private bool _shipToBillingAddress = true;
10
11    public bool ShipToBillingAddress
12    {
13        get
14        {
15            return _shipToBillingAddress;
16        }
17
18        set
19        {
20            if (_shipToBillingAddress != value)
21            {
22                _shipToBillingAddress = value;
23                OnPropertyChanged("DeliveryAddress");
24                OnPropertyChanged("ShipToBillingAddress");
25            }
26        }
27    }
28
29    private Address _billingAddress = new();
30
31    public Address BillingAddress
32    {
33        get
34        {
35            return _billingAddress;
36        }
37
38        set
39        {
40            if (!object.ReferenceEquals(value, _billingAddress))
41            {
42                var oldValue = _billingAddress;
43                if (oldValue != null)
44                {
45                    oldValue.PropertyChanged -= _handleBillingAddressPropertyChanged;
46                }
47
48                _billingAddress = value;
49                OnObservablePropertyChanged("BillingAddress", oldValue, (INotifyPropertyChanged?)value);
50                OnPropertyChanged("DeliveryAddress");
51                OnPropertyChanged("BillingAddress");
52                SubscribeToBillingAddress(value);
53            }
54        }
55    }
56
57    private Address _shippingAddress = new();
58
59    public Address ShippingAddress
60    {
61        get
62        {
63            return _shippingAddress;
64        }
65
66        set
67        {
68            if (!object.ReferenceEquals(value, _shippingAddress))
69            {
70                var oldValue = _shippingAddress;
71                if (oldValue != null)
72                {
73                    oldValue.PropertyChanged -= _handleShippingAddressPropertyChanged;
74                }
75
76                _shippingAddress = value;
77                OnObservablePropertyChanged("ShippingAddress", oldValue, (INotifyPropertyChanged?)value);
78                OnPropertyChanged("DeliveryAddress");
79                OnPropertyChanged("ShippingAddress");
80                SubscribeToShippingAddress(value);
81            }
82        }
83    }
84    public Address DeliveryAddress => this.ShipToBillingAddress ? this.BillingAddress : this.ShippingAddress;
85
86    // Warning: DeliveryAddress isn't an auto-property, so changes to its City can't be observed.
87    public string DeliveryCity => this.DeliveryAddress.City;
88
89    private PropertyChangedEventHandler? _handleBillingAddressPropertyChanged;
90    private PropertyChangedEventHandler? _handleShippingAddressPropertyChanged;
91
92    public Customer()
93    {
94        SubscribeToBillingAddress(BillingAddress);
95        SubscribeToShippingAddress(ShippingAddress);
96    }
97
98    [ObservedExpressions("BillingAddress", "ShippingAddress")]
99    protected virtual void OnChildPropertyChanged(string parentPropertyPath, string propertyName)
100    {
101    }
102
103    [ObservedExpressions("BillingAddress", "ShippingAddress")]
104    protected virtual void OnObservablePropertyChanged(string propertyPath, INotifyPropertyChanged? oldValue, INotifyPropertyChanged? newValue)
105    {
106    }
107
108    protected virtual void OnPropertyChanged(string propertyName)
109    {
110        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
111    }
112
113    private void SubscribeToBillingAddress(Address value)
114    {
115        if (value != null)
116        {
117            _handleBillingAddressPropertyChanged ??= HandlePropertyChanged;
118            value.PropertyChanged += _handleBillingAddressPropertyChanged;
119        }
120
121        void HandlePropertyChanged(object? sender, PropertyChangedEventArgs e)
122        {
123            {
124                var propertyName = e.PropertyName;
125                switch (propertyName)
126                {
127                    default:
128                        OnChildPropertyChanged("BillingAddress", propertyName);
129                        break;
130                }
131            }
132        }
133    }
134
135    private void SubscribeToShippingAddress(Address value)
136    {
137        if (value != null)
138        {
139            _handleShippingAddressPropertyChanged ??= HandlePropertyChanged;
140            value.PropertyChanged += _handleShippingAddressPropertyChanged;
141        }
142
143        void HandlePropertyChanged(object? sender, PropertyChangedEventArgs e)
144        {
145            {
146                var propertyName = e.PropertyName;
147                switch (propertyName)
148                {
149                    default:
150                        OnChildPropertyChanged("ShippingAddress", propertyName);
151                        break;
152                }
153            }
154        }
155    }
156
157    public event PropertyChangedEventHandler? PropertyChanged;
158}
159
1using Metalama.Patterns.Observability;
2
3namespace Doc.LAMA5163.Error;
4
5[Observable]
6public partial class Address
7{
8    public string City { get; set; } = "";
9}
10

Resolution

Do one of the following:

  • Access the child property through auto-properties or fields, as in the following example. If needed, repeat the condition of the computed property.
  • If the computed property never changes after the object is constructed, turn it into a get-only auto-property initialized in the constructor.

Source Code
1using Metalama.Patterns.Observability;
2

3namespace Doc.LAMA5163.Fixed;
4
5[Observable]
6public partial class Customer
7{
8    public bool ShipToBillingAddress { get; set; } = true;
9


































10    public Address BillingAddress { get; set; } = new();














11
12    public Address ShippingAddress { get; set; } = new();







13
14    public Address DeliveryAddress => this.ShipToBillingAddress ? this.BillingAddress : this.ShippingAddress;








15









16    // Fixed: City is accessed through the auto-properties.
17    public string DeliveryCity => this.ShipToBillingAddress ? this.BillingAddress.City : this.ShippingAddress.City;
18}
19
Transformed Code
1using System.ComponentModel;
2using Metalama.Patterns.Observability;
3
4namespace Doc.LAMA5163.Fixed;
5
6[Observable]
7public partial class Customer : INotifyPropertyChanged
8{
9    private bool _shipToBillingAddress = true;
10
11    public bool ShipToBillingAddress
12    {
13        get
14        {
15            return _shipToBillingAddress;
16        }
17
18        set
19        {
20            if (_shipToBillingAddress != value)
21            {
22                _shipToBillingAddress = value;
23                OnPropertyChanged("DeliveryAddress");
24                OnPropertyChanged("DeliveryCity");
25                OnPropertyChanged("ShipToBillingAddress");
26            }
27        }
28    }
29
30    private Address _billingAddress = new();
31
32    public Address BillingAddress
33    {
34        get
35        {
36            return _billingAddress;
37        }
38
39        set
40        {
41            if (!object.ReferenceEquals(value, _billingAddress))
42            {
43                var oldValue = _billingAddress;
44                if (oldValue != null)
45                {
46                    oldValue.PropertyChanged -= _handleBillingAddressPropertyChanged;
47                }
48
49                _billingAddress = value;
50                OnObservablePropertyChanged("BillingAddress", oldValue, (INotifyPropertyChanged?)value);
51                OnPropertyChanged("DeliveryAddress");
52                OnPropertyChanged("DeliveryCity");
53                OnPropertyChanged("BillingAddress");
54                SubscribeToBillingAddress(value);
55            }
56        }
57    }
58
59    private Address _shippingAddress = new();
60
61    public Address ShippingAddress
62    {
63        get
64        {
65            return _shippingAddress;
66        }
67
68        set
69        {
70            if (!object.ReferenceEquals(value, _shippingAddress))
71            {
72                var oldValue = _shippingAddress;
73                if (oldValue != null)
74                {
75                    oldValue.PropertyChanged -= _handleShippingAddressPropertyChanged;
76                }
77
78                _shippingAddress = value;
79                OnObservablePropertyChanged("ShippingAddress", oldValue, (INotifyPropertyChanged?)value);
80                OnPropertyChanged("DeliveryAddress");
81                OnPropertyChanged("DeliveryCity");
82                OnPropertyChanged("ShippingAddress");
83                SubscribeToShippingAddress(value);
84            }
85        }
86    }
87    public Address DeliveryAddress => this.ShipToBillingAddress ? this.BillingAddress : this.ShippingAddress;
88
89    // Fixed: City is accessed through the auto-properties.
90    public string DeliveryCity => this.ShipToBillingAddress ? this.BillingAddress.City : this.ShippingAddress.City;
91
92    private PropertyChangedEventHandler? _handleBillingAddressPropertyChanged;
93    private PropertyChangedEventHandler? _handleShippingAddressPropertyChanged;
94
95    public Customer()
96    {
97        SubscribeToBillingAddress(BillingAddress);
98        SubscribeToShippingAddress(ShippingAddress);
99    }
100
101    [ObservedExpressions("BillingAddress", "ShippingAddress")]
102    protected virtual void OnChildPropertyChanged(string parentPropertyPath, string propertyName)
103    {
104    }
105
106    [ObservedExpressions("BillingAddress", "ShippingAddress")]
107    protected virtual void OnObservablePropertyChanged(string propertyPath, INotifyPropertyChanged? oldValue, INotifyPropertyChanged? newValue)
108    {
109    }
110
111    protected virtual void OnPropertyChanged(string propertyName)
112    {
113        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
114    }
115
116    private void SubscribeToBillingAddress(Address value)
117    {
118        if (value != null)
119        {
120            _handleBillingAddressPropertyChanged ??= HandlePropertyChanged;
121            value.PropertyChanged += _handleBillingAddressPropertyChanged;
122        }
123
124        void HandlePropertyChanged(object? sender, PropertyChangedEventArgs e)
125        {
126            {
127                var propertyName = e.PropertyName;
128                switch (propertyName)
129                {
130                    case "City":
131                        OnPropertyChanged("DeliveryCity");
132                        OnChildPropertyChanged("BillingAddress", "City");
133                        break;
134                    default:
135                        OnChildPropertyChanged("BillingAddress", propertyName);
136                        break;
137                }
138            }
139        }
140    }
141
142    private void SubscribeToShippingAddress(Address value)
143    {
144        if (value != null)
145        {
146            _handleShippingAddressPropertyChanged ??= HandlePropertyChanged;
147            value.PropertyChanged += _handleShippingAddressPropertyChanged;
148        }
149
150        void HandlePropertyChanged(object? sender, PropertyChangedEventArgs e)
151        {
152            {
153                var propertyName = e.PropertyName;
154                switch (propertyName)
155                {
156                    case "City":
157                        OnPropertyChanged("DeliveryCity");
158                        OnChildPropertyChanged("ShippingAddress", "City");
159                        break;
160                    default:
161                        OnChildPropertyChanged("ShippingAddress", propertyName);
162                        break;
163                }
164            }
165        }
166    }
167
168    public event PropertyChangedEventHandler? PropertyChanged;
169}
170
1using Metalama.Patterns.Observability;
2
3namespace Doc.LAMA5163.Fixed;
4
5[Observable]
6public partial class Address
7{
8    public string City { get; set; } = "";
9}
10

See also