| 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.
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
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.
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
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