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 // This code is inspired by ChangeSummary: | 7 // This code is inspired by ChangeSummary: |
8 // https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js | 8 // https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js |
9 // ...which underlies MDV. Since we don't need the functionality of | 9 // ...which underlies MDV. Since we don't need the functionality of |
10 // ChangeSummary, we just implement what we need for data bindings. | 10 // ChangeSummary, we just implement what we need for data bindings. |
11 // This allows our implementation to be much simpler. | 11 // This allows our implementation to be much simpler. |
12 | 12 |
13 /** | 13 /** |
14 * A data-bound path starting from a view-model or model object, for example | 14 * A data-bound path starting from a view-model or model object, for example |
15 * `foo.bar.baz`. | 15 * `foo.bar.baz`. |
16 * | 16 * |
17 * When the [values] stream is being listened to, this will observe changes to | 17 * When the [values] stream is being listened to, this will observe changes to |
18 * the object and any intermediate object along the path, and send [values] | 18 * the object and any intermediate object along the path, and send [values] |
19 * accordingly. When all listeners are unregistered it will stop observing | 19 * accordingly. When all listeners are unregistered it will stop observing |
20 * the objects. | 20 * the objects. |
21 * | 21 * |
22 * This class is used to implement [Node.bind] and similar functionality. | 22 * This class is used to implement [Node.bind] and similar functionality. |
23 */ | 23 */ |
24 // TODO(jmesserly): consider specialized subclasses for: | |
25 // * single token in path, e.g. "foo" | |
26 // * specifically: "value" | |
Siggi Cherem (dart-lang)
2013/10/29 21:00:07
+1
Jennifer Messerly
2013/10/29 22:35:07
Done.
| |
24 class PathObserver extends ChangeNotifier { | 27 class PathObserver extends ChangeNotifier { |
25 /** The path string. */ | 28 /** The path string. */ |
26 final String path; | 29 final String path; |
27 | 30 |
28 /** True if the path is valid, otherwise false. */ | 31 /** True if the path is valid, otherwise false. */ |
29 final bool _isValid; | 32 final bool _isValid; |
30 | 33 |
31 final List<Object> _segments; | 34 final List<Object> _segments; |
32 List<Object> _values; | 35 List<Object> _values; |
33 List<StreamSubscription> _subs; | 36 List<StreamSubscription> _subs; |
34 | 37 |
38 // TODO(jmesserly): I'm not sure about these APIs. They do increase | |
39 // performance but at the cost of complexity and perhaps having too much | |
40 // functionality inside PathObserver. | |
Siggi Cherem (dart-lang)
2013/10/29 21:00:07
I think it's worth it. IIUC, if we have a compile-
Jennifer Messerly
2013/10/29 22:35:07
agree. removed the TODO. original it was getValue/
| |
41 final Function _getValue; | |
42 | |
35 /** | 43 /** |
36 * Observes [path] on [object] for changes. This returns an object that can be | 44 * Observes [path] on [object] for changes. This returns an object that can be |
37 * used to get the changes and get/set the value at this path. | 45 * used to get the changes and get/set the value at this path. |
38 * See [PathObserver.bindSync] and [PathObserver.value]. | 46 * See [PathObserver.bindSync] and [PathObserver.value]. |
Siggi Cherem (dart-lang)
2013/10/29 21:00:07
add short description for getValue?
Jennifer Messerly
2013/10/29 22:35:07
Done.
| |
39 */ | 47 */ |
40 PathObserver(Object object, String path) | 48 PathObserver(Object object, String path, {getValue(x)}) |
41 : path = path, | 49 : path = path, |
50 _getValue = getValue, | |
42 _isValid = _isPathValid(path), | 51 _isValid = _isPathValid(path), |
43 _segments = <Object>[] { | 52 _segments = <Object>[] { |
44 | 53 |
45 if (_isValid) { | 54 if (_isValid) { |
46 for (var segment in path.trim().split('.')) { | 55 for (var segment in path.trim().split('.')) { |
47 if (segment == '') continue; | 56 if (segment == '') continue; |
48 var index = int.parse(segment, radix: 10, onError: (_) => null); | 57 var index = int.parse(segment, radix: 10, onError: (_) => null); |
49 _segments.add(index != null ? index : new Symbol(segment)); | 58 _segments.add(index != null ? index : new Symbol(segment)); |
50 } | 59 } |
51 } | 60 } |
52 | 61 |
53 // Initialize arrays. | 62 // Initialize arrays. |
54 // Note that the path itself can't change after it is initially | 63 // Note that the path itself can't change after it is initially |
55 // constructed, even though the objects along the path can change. | 64 // constructed, even though the objects along the path can change. |
56 _values = new List<Object>(_segments.length + 1); | 65 _values = new List<Object>(_segments.length + 1); |
66 | |
67 if (_segments.isEmpty && getValue != null) object = getValue(object); | |
Siggi Cherem (dart-lang)
2013/10/29 21:00:07
reading this and the lines below it's a bit confus
Jennifer Messerly
2013/10/29 22:35:07
tried to clarify in a comment. changed argument na
| |
57 _values[0] = object; | 68 _values[0] = object; |
58 _subs = new List<StreamSubscription>(_segments.length); | 69 _subs = new List<StreamSubscription>(_segments.length); |
59 } | 70 } |
60 | 71 |
61 /** The object being observed. */ | 72 /** The object being observed. */ |
62 get object => _values[0]; | 73 get object => _values[0]; |
63 | 74 |
64 /** Gets the last reported value at this path. */ | 75 /** Gets the last reported value at this path. */ |
65 @reflectable get value { | 76 @reflectable get value { |
66 if (!_isValid) return null; | 77 if (!_isValid) return null; |
67 if (!hasObservers) _updateValues(); | 78 if (!hasObservers) _updateValues(); |
68 return _values.last; | 79 return _values.last; |
69 } | 80 } |
70 | 81 |
71 /** Sets the value at this path. */ | 82 /** Sets the value at this path. */ |
72 @reflectable void set value(Object value) { | 83 @reflectable void set value(Object newValue) { |
73 int len = _segments.length; | 84 int len = _segments.length; |
74 | 85 |
75 // TODO(jmesserly): throw if property cannot be set? | 86 // TODO(jmesserly): throw if property cannot be set? |
76 // MDV seems tolerant of these errors. | 87 // MDV seems tolerant of these errors. |
77 if (len == 0) return; | 88 if (len == 0) return; |
78 if (!hasObservers) _updateValues(); | 89 if (!hasObservers) _updateValues(); |
79 | 90 |
80 if (_setObjectProperty(_values[len - 1], _segments[len - 1], value)) { | 91 if (_setObjectProperty(_values[len - 1], _segments[len - 1], newValue)) { |
81 // Technically, this would get updated asynchronously via a change record. | 92 // Technically, this would get updated asynchronously via a change record. |
82 // However, it is nice if calling the getter will yield the same value | 93 // However, it is nice if calling the getter will yield the same value |
83 // that was just set. So we use this opportunity to update our cache. | 94 // that was just set. So we use this opportunity to update our cache. |
84 _values[len] = value; | 95 _values[len] = newValue; |
85 } | 96 } |
86 } | 97 } |
87 | 98 |
88 /** | 99 /** |
89 * Invokes the [callback] immediately with the current [value], and every time | 100 * Invokes the [callback] immediately with the current [value], and every time |
90 * the value changes. This is useful for bindings, which want to be up-to-date | 101 * the value changes. This is useful for bindings, which want to be up-to-date |
91 * immediately and stay bound to the value of the path. | 102 * immediately and stay bound to the value of the path. |
92 */ | 103 */ |
93 StreamSubscription bindSync(void callback(value)) { | 104 StreamSubscription bindSync(void callback(value)) { |
94 var result = changes.listen((records) { callback(value); }); | 105 var result = changes.listen((records) { callback(value); }); |
(...skipping 11 matching lines...) Expand all Loading... | |
106 for (int i = 0; i < _subs.length; i++) { | 117 for (int i = 0; i < _subs.length; i++) { |
107 if (_subs[i] != null) { | 118 if (_subs[i] != null) { |
108 _subs[i].cancel(); | 119 _subs[i].cancel(); |
109 _subs[i] = null; | 120 _subs[i] = null; |
110 } | 121 } |
111 } | 122 } |
112 } | 123 } |
113 | 124 |
114 // TODO(jmesserly): should we be caching these values if not observing? | 125 // TODO(jmesserly): should we be caching these values if not observing? |
115 void _updateValues() { | 126 void _updateValues() { |
116 for (int i = 0; i < _segments.length; i++) { | 127 for (int i = 0, last = _segments.length - 1; i <= last; i++) { |
117 _values[i + 1] = _getObjectProperty(_values[i], _segments[i]); | 128 var newValue = _getObjectProperty(_values[i], _segments[i]); |
129 if (i == last && _getValue != null) newValue = _getValue(newValue); | |
130 _values[i + 1] = newValue; | |
118 } | 131 } |
119 } | 132 } |
120 | 133 |
121 void _updateObservedValues([int start = 0]) { | 134 void _updateObservedValues([int start = 0]) { |
122 var oldValue, newValue; | 135 var oldValue, newValue; |
123 for (int i = start; i < _segments.length; i++) { | 136 for (int i = start, last = _segments.length - 1; i <= last; i++) { |
124 oldValue = _values[i + 1]; | 137 oldValue = _values[i + 1]; |
125 newValue = _getObjectProperty(_values[i], _segments[i]); | 138 newValue = _getObjectProperty(_values[i], _segments[i]); |
139 if (i == last && _getValue != null) newValue = _getValue(newValue); | |
126 if (identical(oldValue, newValue)) { | 140 if (identical(oldValue, newValue)) { |
127 _observePath(start, i); | 141 _observePath(start, i); |
128 return; | 142 return; |
129 } | 143 } |
130 _values[i + 1] = newValue; | 144 _values[i + 1] = newValue; |
131 } | 145 } |
132 | 146 |
133 _observePath(start); | 147 _observePath(start); |
134 notifyPropertyChange(#value, oldValue, newValue); | 148 notifyPropertyChange(#value, oldValue, newValue); |
135 } | 149 } |
136 | 150 |
137 void _observePath([int start = 0, int end]) { | 151 void _observePath([int start = 0, int end]) { |
138 if (end == null) end = _segments.length; | 152 if (end == null) end = _segments.length; |
139 | 153 |
140 for (int i = start; i < end; i++) { | 154 for (int i = start; i < end; i++) { |
141 if (_subs[i] != null) _subs[i].cancel(); | 155 if (_subs[i] != null) _subs[i].cancel(); |
142 _observeIndex(i); | 156 _observeIndex(i); |
143 } | 157 } |
144 } | 158 } |
145 | 159 |
146 void _observeIndex(int i) { | 160 void _observeIndex(int i) { |
147 final object = _values[i]; | 161 final object = _values[i]; |
148 if (object is Observable) { | 162 if (object is Observable) { |
149 // TODO(jmesserly): rather than allocating a new closure for each | 163 // TODO(jmesserly): rather than allocating a new closure for each |
150 // property, we could try and have one for the entire path. In that case, | 164 // property, we could try and have one for the entire path. However we'd |
151 // we would lose information about which object changed (note: unless | 165 // need to do a linear scan to find the index as soon as we got a change. |
152 // PropertyChangeRecord is modified to includes the sender object), so | 166 // Also we need to fix ListChangeRecord and MapChangeRecord to contain |
153 // we would need to re-evaluate the entire path. Need to evaluate perf. | 167 // the target. Not sure if it's worth it. |
154 _subs[i] = object.changes.listen((List<ChangeRecord> records) { | 168 _subs[i] = object.changes.listen((List<ChangeRecord> records) { |
155 if (!identical(_values[i], object)) { | |
156 // Ignore this object if we're now tracking something else. | |
157 return; | |
158 } | |
159 | |
160 for (var record in records) { | 169 for (var record in records) { |
161 if (_changeRecordMatches(record, _segments[i])) { | 170 if (_changeRecordMatches(record, _segments[i])) { |
162 _updateObservedValues(i); | 171 _updateObservedValues(i); |
163 return; | 172 return; |
164 } | 173 } |
165 } | 174 } |
166 }); | 175 }); |
167 } | 176 } |
168 } | 177 } |
169 } | 178 } |
(...skipping 24 matching lines...) Expand all Loading... | |
194 return null; | 203 return null; |
195 } | 204 } |
196 } | 205 } |
197 | 206 |
198 if (property is Symbol) { | 207 if (property is Symbol) { |
199 var mirror = reflect(object); | 208 var mirror = reflect(object); |
200 var result = _tryGetField(mirror, property); | 209 var result = _tryGetField(mirror, property); |
201 if (result != null) return result.reflectee; | 210 if (result != null) return result.reflectee; |
202 } | 211 } |
203 | 212 |
213 // TODO(jmesserly): need to fix NoSuchMethodErrors from _tryGetField; right | |
214 // now it's painful to debug (and probably really slow) to use maps. | |
204 if (object is Map) { | 215 if (object is Map) { |
205 if (property is Symbol) property = MirrorSystem.getName(property); | 216 if (property is Symbol) property = MirrorSystem.getName(property); |
206 return object[property]; | 217 return object[property]; |
207 } | 218 } |
208 | 219 |
220 // TODO(jmesserly): log a binding that fails? | |
209 return null; | 221 return null; |
210 } | 222 } |
211 | 223 |
212 bool _setObjectProperty(object, property, value) { | 224 bool _setObjectProperty(object, property, value) { |
213 if (object is List && property is int) { | 225 if (object is List && property is int) { |
214 if (property >= 0 && property < object.length) { | 226 if (property >= 0 && property < object.length) { |
215 object[property] = value; | 227 object[property] = value; |
216 return true; | 228 return true; |
217 } else { | 229 } else { |
218 return false; | 230 return false; |
(...skipping 81 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
300 | 312 |
301 final _spacesRegExp = new RegExp(r'\s'); | 313 final _spacesRegExp = new RegExp(r'\s'); |
302 | 314 |
303 bool _isPathValid(String s) { | 315 bool _isPathValid(String s) { |
304 s = s.replaceAll(_spacesRegExp, ''); | 316 s = s.replaceAll(_spacesRegExp, ''); |
305 | 317 |
306 if (s == '') return true; | 318 if (s == '') return true; |
307 if (s[0] == '.') return false; | 319 if (s[0] == '.') return false; |
308 return _pathRegExp.hasMatch(s); | 320 return _pathRegExp.hasMatch(s); |
309 } | 321 } |
OLD | NEW |