OLD | NEW |
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 part of observe; | 5 part of observe; |
6 | 6 |
7 /** | 7 /** |
8 * Interface representing an observable object. This is used by data in | 8 * Represents an object with observable properties. This is used by data in |
9 * model-view architectures to notify interested parties of [changes]. | 9 * model-view architectures to notify interested parties of [changes] to the |
| 10 * object's properties (fields or getter/setter pairs). |
10 * | 11 * |
11 * This object does not require any specific technique to implement | 12 * The interface does not require any specific technique to implement |
12 * observability. If you mixin [ObservableMixin], [dirtyCheck] will know to | 13 * observability. You can implement it in the following ways: |
13 * check for changes on the object. You may also implement change notification | |
14 * yourself, by calling [notifyChange]. | |
15 * | 14 * |
16 * You can use [ObservableBase] or [ObservableMixin] to implement this. | 15 * - extend or mixin this class, and let the application call [dirtyCheck] |
| 16 * periodically to check for changes to your object. |
| 17 * - extend or mixin [ChangeNotifier], and implement change notifications |
| 18 * manually by calling [notifyPropertyChange] from your setters. |
| 19 * - implement this interface and provide your own implementation. |
17 */ | 20 */ |
18 abstract class Observable { | 21 abstract class Observable { |
19 /** | 22 /** |
20 * The stream of change records to this object. Records will be delivered | 23 * Performs dirty checking of objects that inherit from [Observable]. |
21 * asynchronously. | |
22 * | |
23 * [deliverChanges] can be called to force synchronous delivery. | |
24 */ | |
25 Stream<List<ChangeRecord>> get changes; | |
26 | |
27 /** | |
28 * Synchronously deliver pending [changes]. Returns true if any records were | |
29 * delivered, otherwise false. | |
30 */ | |
31 // TODO(jmesserly): this is a bit different from the ES Harmony version, which | |
32 // allows delivery of changes to a particular observer: | |
33 // http://wiki.ecmascript.org/doku.php?id=harmony:observe#object.deliverchange
records | |
34 // | |
35 // The rationale for that, and for async delivery in general, is the principal | |
36 // that you shouldn't run code (observers) when it doesn't expect to be run. | |
37 // If you do that, you risk violating invariants that the code assumes. | |
38 // | |
39 // For this reason, we need to match the ES Harmony version. The way we can do | |
40 // this in Dart is to add a method on StreamSubscription (possibly by | |
41 // subclassing Stream* types) that immediately delivers records for only | |
42 // that subscription. Alternatively, we could consider using something other | |
43 // than Stream to deliver the multicast change records, and provide an | |
44 // Observable->Stream adapter. | |
45 // | |
46 // Also: we should be delivering changes to the observer (subscription) based | |
47 // on the birth order of the observer. This is for compatibility with ES | |
48 // Harmony as well as predictability for app developers. | |
49 bool deliverChanges(); | |
50 | |
51 /** | |
52 * Notify observers of a change. | |
53 * | |
54 * For most objects [ObservableMixin.notifyPropertyChange] is more | |
55 * convenient, but collections sometimes deliver other types of changes such | |
56 * as a [ListChangeRecord]. | |
57 */ | |
58 void notifyChange(ChangeRecord record); | |
59 | |
60 /** | |
61 * True if this object has any observers, and should call | |
62 * [notifyChange] for changes. | |
63 */ | |
64 bool get hasObservers; | |
65 | |
66 /** | |
67 * Performs dirty checking of objects that inherit from [ObservableMixin]. | |
68 * This scans all observed objects using mirrors and determines if any fields | 24 * This scans all observed objects using mirrors and determines if any fields |
69 * have changed. If they have, it delivers the changes for the object. | 25 * have changed. If they have, it delivers the changes for the object. |
70 */ | 26 */ |
71 static void dirtyCheck() => dirtyCheckObservables(); | 27 static void dirtyCheck() => dirtyCheckObservables(); |
72 } | |
73 | 28 |
74 /** | |
75 * Base class implementing [Observable]. | |
76 * | |
77 * When a field, property, or indexable item is changed, the change record | |
78 * will be sent to [changes]. | |
79 */ | |
80 class ObservableBase = Object with ObservableMixin; | |
81 | |
82 /** | |
83 * Mixin for implementing [Observable] objects. | |
84 * | |
85 * When a field, property, or indexable item is changed, the change record | |
86 * will be sent to [changes]. | |
87 */ | |
88 abstract class ObservableMixin implements Observable { | |
89 StreamController _changes; | 29 StreamController _changes; |
90 InstanceMirror _mirror; | 30 InstanceMirror _mirror; |
91 | 31 |
92 Map<Symbol, Object> _values; | 32 Map<Symbol, Object> _values; |
93 List<ChangeRecord> _records; | 33 List<ChangeRecord> _records; |
94 | 34 |
95 static final _objectType = reflectClass(Object); | 35 static final _objectType = reflectClass(Object); |
96 | 36 |
| 37 /** |
| 38 * The stream of change records to this object. Records will be delivered |
| 39 * asynchronously. |
| 40 * |
| 41 * [deliverChanges] can be called to force synchronous delivery. |
| 42 */ |
97 Stream<List<ChangeRecord>> get changes { | 43 Stream<List<ChangeRecord>> get changes { |
98 if (_changes == null) { | 44 if (_changes == null) { |
99 _changes = new StreamController.broadcast(sync: true, | 45 _changes = new StreamController.broadcast(sync: true, |
100 onListen: _observed, onCancel: _unobserved); | 46 onListen: _observed, onCancel: _unobserved); |
101 } | 47 } |
102 return _changes.stream; | 48 return _changes.stream; |
103 } | 49 } |
104 | 50 |
| 51 /** |
| 52 * True if this object has any observers, and should call |
| 53 * [notifyChange] for changes. |
| 54 */ |
105 bool get hasObservers => _changes != null && _changes.hasListener; | 55 bool get hasObservers => _changes != null && _changes.hasListener; |
106 | 56 |
107 void _observed() { | 57 void _observed() { |
108 // Register this object for dirty checking purposes. | 58 // Register this object for dirty checking purposes. |
109 registerObservable(this); | 59 registerObservable(this); |
110 | 60 |
111 var mirror = reflect(this); | 61 var mirror = reflect(this); |
112 var values = new Map<Symbol, Object>(); | 62 var values = new Map<Symbol, Object>(); |
113 | 63 |
114 // Note: we scan for @observable regardless of whether the base type | 64 // Note: we scan for @observable regardless of whether the base type |
(...skipping 22 matching lines...) Expand all Loading... |
137 /** Release data associated with observation. */ | 87 /** Release data associated with observation. */ |
138 void _unobserved() { | 88 void _unobserved() { |
139 // Note: we don't need to explicitly unregister from the dirty check list. | 89 // Note: we don't need to explicitly unregister from the dirty check list. |
140 // This will happen automatically at the next call to dirtyCheck. | 90 // This will happen automatically at the next call to dirtyCheck. |
141 if (_values != null) { | 91 if (_values != null) { |
142 _mirror = null; | 92 _mirror = null; |
143 _values = null; | 93 _values = null; |
144 } | 94 } |
145 } | 95 } |
146 | 96 |
| 97 /** |
| 98 * Synchronously deliver pending [changes]. Returns true if any records were |
| 99 * delivered, otherwise false. |
| 100 */ |
| 101 // TODO(jmesserly): this is a bit different from the ES Harmony version, which |
| 102 // allows delivery of changes to a particular observer: |
| 103 // http://wiki.ecmascript.org/doku.php?id=harmony:observe#object.deliverchange
records |
| 104 // |
| 105 // The rationale for that, and for async delivery in general, is the principal |
| 106 // that you shouldn't run code (observers) when it doesn't expect to be run. |
| 107 // If you do that, you risk violating invariants that the code assumes. |
| 108 // |
| 109 // For this reason, we need to match the ES Harmony version. The way we can do |
| 110 // this in Dart is to add a method on StreamSubscription (possibly by |
| 111 // subclassing Stream* types) that immediately delivers records for only |
| 112 // that subscription. Alternatively, we could consider using something other |
| 113 // than Stream to deliver the multicast change records, and provide an |
| 114 // Observable->Stream adapter. |
| 115 // |
| 116 // Also: we should be delivering changes to the observer (subscription) based |
| 117 // on the birth order of the observer. This is for compatibility with ES |
| 118 // Harmony as well as predictability for app developers. |
147 bool deliverChanges() { | 119 bool deliverChanges() { |
148 if (_values == null || !hasObservers) return false; | 120 if (_values == null || !hasObservers) return false; |
149 | 121 |
150 // Start with manually notified records (computed properties, etc), | 122 // Start with manually notified records (computed properties, etc), |
151 // then scan all fields for additional changes. | 123 // then scan all fields for additional changes. |
152 List records = _records; | 124 List records = _records; |
153 _records = null; | 125 _records = null; |
154 | 126 |
155 _values.forEach((name, oldValue) { | 127 _values.forEach((name, oldValue) { |
156 var newValue = _mirror.getField(name).reflectee; | 128 var newValue = _mirror.getField(name).reflectee; |
157 if (!identical(oldValue, newValue)) { | 129 if (oldValue != newValue) { |
158 if (records == null) records = []; | 130 if (records == null) records = []; |
159 records.add(new PropertyChangeRecord(name)); | 131 records.add(new PropertyChangeRecord(this, name, oldValue, newValue)); |
160 _values[name] = newValue; | 132 _values[name] = newValue; |
161 } | 133 } |
162 }); | 134 }); |
163 | 135 |
164 if (records == null) return false; | 136 if (records == null) return false; |
165 | 137 |
166 _changes.add(new UnmodifiableListView<ChangeRecord>(records)); | 138 _changes.add(new UnmodifiableListView<ChangeRecord>(records)); |
167 return true; | 139 return true; |
168 } | 140 } |
169 | 141 |
170 /** | 142 /** |
171 * Notify that the field [name] of this object has been changed. | 143 * Notify that the field [name] of this object has been changed. |
172 * | 144 * |
173 * The [oldValue] and [newValue] are also recorded. If the two values are | 145 * The [oldValue] and [newValue] are also recorded. If the two values are |
174 * identical, no change will be recorded. | 146 * equal, no change will be recorded. |
175 * | 147 * |
176 * For convenience this returns [newValue]. | 148 * For convenience this returns [newValue]. |
177 */ | 149 */ |
178 notifyPropertyChange(Symbol field, Object oldValue, Object newValue) | 150 notifyPropertyChange(Symbol field, Object oldValue, Object newValue) |
179 => _notifyPropertyChange(this, field, oldValue, newValue); | 151 => _notifyPropertyChange(this, field, oldValue, newValue); |
180 | 152 |
181 /** | 153 /** |
182 * Notify a change manually. This is *not* required for fields, but can be | 154 * Notify observers of a change. |
183 * used for computed properties. *Note*: unlike [ChangeNotifierMixin] this | 155 * |
184 * will not schedule [deliverChanges]; use [Observable.dirtyCheck] instead. | 156 * For most objects [Observable.notifyPropertyChange] is more convenient, but |
| 157 * collections sometimes deliver other types of changes such as a |
| 158 * [ListChangeRecord]. |
| 159 * |
| 160 * Notes: |
| 161 * - This is *not* required for fields if you mixin or extend [Observable], |
| 162 * but you can use it for computed properties. |
| 163 * - Unlike [ChangeNotifier] this will not schedule [deliverChanges]; use |
| 164 * [Observable.dirtyCheck] instead. |
185 */ | 165 */ |
186 void notifyChange(ChangeRecord record) { | 166 void notifyChange(ChangeRecord record) { |
187 if (!hasObservers) return; | 167 if (!hasObservers) return; |
188 | 168 |
189 if (_records == null) _records = []; | 169 if (_records == null) _records = []; |
190 _records.add(record); | 170 _records.add(record); |
191 } | 171 } |
192 } | 172 } |
193 | 173 |
194 /** | 174 /** |
| 175 * *Deprecated* use [Observable.notifyPropertyChange] instead. |
| 176 * |
| 177 * This API should not be used as it creates a |
| 178 * [PropertyChangeRecord] without oldValue and newValue. |
| 179 * |
195 * Notify the property change. Shorthand for: | 180 * Notify the property change. Shorthand for: |
196 * | 181 * |
197 * target.notifyChange(new PropertyChangeRecord(targetName)); | 182 * target.notifyChange(new PropertyChangeRecord(target, name, null, null)); |
198 */ | 183 */ |
199 void notifyProperty(Observable target, Symbol targetName) { | 184 @deprecated |
200 target.notifyChange(new PropertyChangeRecord(targetName)); | 185 void notifyProperty(Observable target, Symbol name) { |
| 186 target.notifyChange(new PropertyChangeRecord(target, name, null, null)); |
201 } | 187 } |
202 | 188 |
203 // TODO(jmesserly): remove the instance method and make this top-level method | 189 // TODO(jmesserly): remove the instance method and make this top-level method |
204 // public instead? | 190 // public instead? |
205 _notifyPropertyChange(Observable obj, Symbol field, Object oldValue, | 191 _notifyPropertyChange(Observable obj, Symbol field, Object oldValue, |
206 Object newValue) { | 192 Object newValue) { |
207 | 193 |
208 // TODO(jmesserly): should this be == instead of identical, to prevent | 194 if (obj.hasObservers && oldValue != newValue) { |
209 // spurious loops? | 195 obj.notifyChange(new PropertyChangeRecord(obj, field, oldValue, newValue)); |
210 if (obj.hasObservers && !identical(oldValue, newValue)) { | |
211 obj.notifyChange(new PropertyChangeRecord(field)); | |
212 } | 196 } |
213 return newValue; | 197 return newValue; |
214 } | 198 } |
OLD | NEW |