Added support for property change with same object instance (via WrappedValue).

This commit is contained in:
Nedyalko Nikolov
2016-01-22 08:27:31 +02:00
parent 8159929ff7
commit 5c35f33441
4 changed files with 139 additions and 18 deletions

View File

@@ -491,3 +491,51 @@ export function test_ObservablesCreatedWithJSON_shouldNotEmitTwoTimesPropertyCha
TKUnit.assertEqual(propertyChangeCounter, 1, "PropertyChange event should be fired only once for a single change."); 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.");
}

View File

@@ -30,6 +30,37 @@ declare module "data/observable" {
value: any; 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. * Observable is used when you want to be notified when a change occurs. Use on/off methods to add/remove listener.
*/ */

View File

@@ -6,6 +6,45 @@ interface ListenerEntry {
thisArg: any; 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 { export class Observable implements definition.Observable {
public static propertyChangeEvent = "propertyChange"; public static propertyChangeEvent = "propertyChange";
private _map: Map<string, Object>; private _map: Map<string, Object>;
@@ -126,7 +165,8 @@ export class Observable implements definition.Observable {
public _setCore(data: definition.PropertyChangeData) { public _setCore(data: definition.PropertyChangeData) {
this.disableNotifications[data.propertyName] = true; 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]; delete this.disableNotifications[data.propertyName];
} }

View File

@@ -1,5 +1,5 @@
import definition = require("ui/core/dependency-observable"); import definition = require("ui/core/dependency-observable");
import observable = require("data/observable"); import {Observable, WrappedValue} from "data/observable";
import types = require("utils/types"); 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. // 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<string, any> = new Map<string, any>(); var defaultValueForPropertyPerType: Map<string, any> = new Map<string, any>();
export class DependencyObservable extends observable.Observable { export class DependencyObservable extends Observable {
private _propertyEntries = {}; private _propertyEntries = {};
public set(name: string, value: any) { public set(name: string, value: any) {
@@ -285,8 +285,9 @@ export class DependencyObservable extends observable.Observable {
} }
public _setValue(property: Property, value: any, source?: number) { public _setValue(property: Property, value: any, source?: number) {
if (!property.isValidValue(value)) { let realValue = WrappedValue.unwrap(value);
throw new Error("Invalid value " + value + " for property " + property.name); if (!property.isValidValue(realValue)) {
throw new Error("Invalid value " + realValue + " for property " + property.name);
} }
if (types.isUndefined(source)) { if (types.isUndefined(source)) {
@@ -350,17 +351,18 @@ export class DependencyObservable extends observable.Observable {
} }
public _onPropertyChanged(property: Property, oldValue: any, newValue: any) { public _onPropertyChanged(property: Property, oldValue: any, newValue: any) {
let realNewValue = WrappedValue.unwrap(newValue);
if (property.metadata.onValueChanged) { if (property.metadata.onValueChanged) {
property.metadata.onValueChanged({ property.metadata.onValueChanged({
object: this, object: this,
property: property, property: property,
eventName: observable.Observable.propertyChangeEvent, eventName: Observable.propertyChangeEvent,
newValue: newValue, newValue: realNewValue,
oldValue: oldValue oldValue: oldValue
}); });
} }
if (this.hasListeners(observable.Observable.propertyChangeEvent)) { if (this.hasListeners(Observable.propertyChangeEvent)) {
var changeData = super._createPropertyChangeData(property.name, newValue); var changeData = super._createPropertyChangeData(property.name, newValue);
this.notify(changeData); this.notify(changeData);
} }
@@ -371,7 +373,7 @@ export class DependencyObservable extends observable.Observable {
eventName: eventName, eventName: eventName,
propertyName: property.name, propertyName: property.name,
object: this, object: this,
value: newValue value: realNewValue
} }
this.notify(ngChangedData); this.notify(ngChangedData);
} }
@@ -399,10 +401,10 @@ export class DependencyObservable extends observable.Observable {
} }
private _setValueInternal(property: Property, value: any, source: number) { 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. // 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) { if (types.isString(realValue) && property.valueConverter) {
value = property.valueConverter(value); realValue = property.valueConverter(realValue);
} }
var entry: PropertyEntry = this._propertyEntries[property.id]; var entry: PropertyEntry = this._propertyEntries[property.id];
@@ -415,21 +417,21 @@ export class DependencyObservable extends observable.Observable {
switch (source) { switch (source) {
case ValueSource.Css: case ValueSource.Css:
entry.cssValue = value; entry.cssValue = realValue;
break; break;
case ValueSource.Inherited: case ValueSource.Inherited:
entry.inheritedValue = value; entry.inheritedValue = realValue;
break; break;
case ValueSource.Local: case ValueSource.Local:
entry.localValue = value; entry.localValue = realValue;
break; break;
case ValueSource.VisualState: case ValueSource.VisualState:
entry.visualStateValue = value; entry.visualStateValue = realValue;
break; break;
} }
var comparer: (x: any, y: any) => boolean = property.metadata.equalityComparer || this._defaultComparer; 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); this._onPropertyChanged(property, currentValue, entry.effectiveValue);
} }
} }