diff --git a/apps/tests/observable-tests.ts b/apps/tests/observable-tests.ts index 1cbb6e702..8a02dd570 100644 --- a/apps/tests/observable-tests.ts +++ b/apps/tests/observable-tests.ts @@ -490,4 +490,52 @@ export function test_ObservablesCreatedWithJSON_shouldNotEmitTwoTimesPropertyCha testObservable.set("property1", 2); TKUnit.assertEqual(propertyChangeCounter, 1, "PropertyChange event should be fired only once for a single change."); +} + +export function test_ObservableShouldEmitPropertyChangeWithSameObjectUsingWrappedValue() { + var testArray = [1]; + var testObservable = new observable.Observable({ "property1": testArray}); + var propertyChangeCounter = 0; + var propertyChangeHandler = function (args) { + propertyChangeCounter++; + } + testObservable.on(observable.Observable.propertyChangeEvent, propertyChangeHandler); + testArray.push(2); + + testObservable.set("property1", testArray); + + TKUnit.assertEqual(propertyChangeCounter, 0, "PropertyChange event should not be fired when the same object instance is passed."); + + testObservable.set("property1", observable.WrappedValue.wrap(testArray)); + + TKUnit.assertEqual(propertyChangeCounter, 1, "PropertyChange event should be fired only once for a single change."); +} + +export function test_CorrectEventArgsWhenWrappedValueIsUsed() { + let testArray = [1]; + let testObservable = new observable.Observable({ "property1": testArray}); + let actualArgsValue = 0; + let propertyChangeHandler = function (args) { + actualArgsValue = args.value; + + } + testObservable.on(observable.Observable.propertyChangeEvent, propertyChangeHandler); + testArray.push(2); + + let wrappedArray = observable.WrappedValue.wrap(testArray); + + testObservable.set("property1", wrappedArray); + + TKUnit.assertEqual(actualArgsValue, wrappedArray, "PropertyChange event should be fired with correct value in arguments."); +} + +export function test_CorrectPropertyValueAfterUsingWrappedValue() { + let testArray = [1]; + let testObservable = new observable.Observable({ "property1": testArray}); + + let wrappedArray = observable.WrappedValue.wrap(testArray); + + testObservable.set("property1", wrappedArray); + + TKUnit.assertEqual(testObservable.get("property1"), testArray, "WrappedValue is used only to execute property change logic and unwrapped value should be used as proeprty value."); } \ No newline at end of file diff --git a/data/observable/observable.d.ts b/data/observable/observable.d.ts index 939e762c2..5e0d99df2 100644 --- a/data/observable/observable.d.ts +++ b/data/observable/observable.d.ts @@ -29,6 +29,37 @@ declare module "data/observable" { */ value: any; } + + /** + * Helper class that is used to fire property change even when real object is the same. + * By default property change will not be fired for a same object. + * By wrapping object into a WrappedValue instance `same object restriction` will be passed. + */ + class WrappedValue { + /** + * Property which holds the real value. + */ + wrapped: any; + + /** + * Creates an instance of WrappedValue object. + * @param value - the real value which should be wrapped. + */ + constructor(value: any); + + /** + * Gets the real value of previously wrappedValue. + * @param value - Value that should be unwraped. If there is no wrappedValue property of the value object then value will be returned. + */ + static unwrap(value: any): any; + + /** + * Returns an instance of WrappedValue. The actual instance is get from a WrappedValues pool. + * @param value - Value that should be wrapped. + */ + static wrap(value: any): WrappedValue + + } /** * Observable is used when you want to be notified when a change occurs. Use on/off methods to add/remove listener. diff --git a/data/observable/observable.ts b/data/observable/observable.ts index aac6f0e8d..1a144680c 100644 --- a/data/observable/observable.ts +++ b/data/observable/observable.ts @@ -6,6 +6,45 @@ interface ListenerEntry { thisArg: any; } +var _wrappedIndex = 0; + +export class WrappedValue implements definition.WrappedValue { + private _wrapped: any; + + public get wrapped(): any { + return this._wrapped; + } + + public set wrapped(value) { + this._wrapped = value; + } + + constructor(value: any) { + this._wrapped = value; + } + + public static unwrap(value: any) { + if (value && value.wrapped) { + return value.wrapped; + } + return value; + } + + public static wrap(value: any) { + var w = _wrappedValues[_wrappedIndex++ % 5]; + w.wrapped = value; + return w; + } +} + +var _wrappedValues = [ + new WrappedValue(null), + new WrappedValue(null), + new WrappedValue(null), + new WrappedValue(null), + new WrappedValue(null) +] + export class Observable implements definition.Observable { public static propertyChangeEvent = "propertyChange"; private _map: Map; @@ -126,7 +165,8 @@ export class Observable implements definition.Observable { public _setCore(data: definition.PropertyChangeData) { this.disableNotifications[data.propertyName] = true; - this[data.propertyName] = data.value; + let newValue = WrappedValue.unwrap(data.value); + this[data.propertyName] = newValue; delete this.disableNotifications[data.propertyName]; } diff --git a/ui/core/dependency-observable.ts b/ui/core/dependency-observable.ts index 9d58cc507..7716b2348 100644 --- a/ui/core/dependency-observable.ts +++ b/ui/core/dependency-observable.ts @@ -1,5 +1,5 @@ import definition = require("ui/core/dependency-observable"); -import observable = require("data/observable"); +import {Observable, WrappedValue} from "data/observable"; import types = require("utils/types"); // use private variables in the scope of the module rather than static members of the class since a member is still accessible through JavaScript and may be changed. @@ -263,7 +263,7 @@ export class PropertyEntry implements definition.PropertyEntry { var defaultValueForPropertyPerType: Map = new Map(); -export class DependencyObservable extends observable.Observable { +export class DependencyObservable extends Observable { private _propertyEntries = {}; public set(name: string, value: any) { @@ -285,8 +285,9 @@ export class DependencyObservable extends observable.Observable { } public _setValue(property: Property, value: any, source?: number) { - if (!property.isValidValue(value)) { - throw new Error("Invalid value " + value + " for property " + property.name); + let realValue = WrappedValue.unwrap(value); + if (!property.isValidValue(realValue)) { + throw new Error("Invalid value " + realValue + " for property " + property.name); } if (types.isUndefined(source)) { @@ -350,17 +351,18 @@ export class DependencyObservable extends observable.Observable { } public _onPropertyChanged(property: Property, oldValue: any, newValue: any) { + let realNewValue = WrappedValue.unwrap(newValue); if (property.metadata.onValueChanged) { property.metadata.onValueChanged({ object: this, property: property, - eventName: observable.Observable.propertyChangeEvent, - newValue: newValue, + eventName: Observable.propertyChangeEvent, + newValue: realNewValue, oldValue: oldValue }); } - if (this.hasListeners(observable.Observable.propertyChangeEvent)) { + if (this.hasListeners(Observable.propertyChangeEvent)) { var changeData = super._createPropertyChangeData(property.name, newValue); this.notify(changeData); } @@ -371,7 +373,7 @@ export class DependencyObservable extends observable.Observable { eventName: eventName, propertyName: property.name, object: this, - value: newValue + value: realNewValue } this.notify(ngChangedData); } @@ -399,10 +401,10 @@ export class DependencyObservable extends observable.Observable { } private _setValueInternal(property: Property, value: any, source: number) { - + let realValue = WrappedValue.unwrap(value); // Convert the value to the real property type in case it is coming as a string from CSS or XML. - if (types.isString(value) && property.valueConverter) { - value = property.valueConverter(value); + if (types.isString(realValue) && property.valueConverter) { + realValue = property.valueConverter(realValue); } var entry: PropertyEntry = this._propertyEntries[property.id]; @@ -415,21 +417,21 @@ export class DependencyObservable extends observable.Observable { switch (source) { case ValueSource.Css: - entry.cssValue = value; + entry.cssValue = realValue; break; case ValueSource.Inherited: - entry.inheritedValue = value; + entry.inheritedValue = realValue; break; case ValueSource.Local: - entry.localValue = value; + entry.localValue = realValue; break; case ValueSource.VisualState: - entry.visualStateValue = value; + entry.visualStateValue = realValue; break; } var comparer: (x: any, y: any) => boolean = property.metadata.equalityComparer || this._defaultComparer; - if (!comparer(currentValue, entry.effectiveValue)) { + if ((value && value.wrapped) || !comparer(currentValue, entry.effectiveValue)) { this._onPropertyChanged(property, currentValue, entry.effectiveValue); } } @@ -437,4 +439,4 @@ export class DependencyObservable extends observable.Observable { private _defaultComparer(x: any, y: any): boolean { return x === y; } -} +} \ No newline at end of file