diff --git a/CrossPlatformModules.csproj b/CrossPlatformModules.csproj
index d4cfc8abb..d8cab45cf 100644
--- a/CrossPlatformModules.csproj
+++ b/CrossPlatformModules.csproj
@@ -13,6 +13,7 @@
+
Cross
@@ -186,6 +187,7 @@
main-page.xml
+
@@ -1575,7 +1577,7 @@
False
-
+
\ No newline at end of file
diff --git a/apps/tests/testRunner.ts b/apps/tests/testRunner.ts
index edff81769..3f522c313 100644
--- a/apps/tests/testRunner.ts
+++ b/apps/tests/testRunner.ts
@@ -68,6 +68,8 @@ allTests["LIST-PICKER"] = require("./ui/list-picker/list-picker-tests");
allTests["DATE-PICKER"] = require("./ui/date-picker/date-picker-tests");
allTests["TIME-PICKER"] = require("./ui/time-picker/time-picker-tests");
allTests["WEB-VIEW"] = require("./ui/web-view/web-view-tests");
+allTests["WEAK-EVENTS"] = require("./weak-event-listener-tests");
+
if (!isRunningOnEmulator()) {
allTests["LOCATION"] = require("./location-tests");
}
diff --git a/apps/tests/ui/helper.ts b/apps/tests/ui/helper.ts
index bfda9c74e..b2ad8b48e 100644
--- a/apps/tests/ui/helper.ts
+++ b/apps/tests/ui/helper.ts
@@ -7,6 +7,7 @@ import TKUnit = require("../TKUnit");
import utils = require("utils/utils");
import types = require("utils/types");
import styling = require("ui/styling");
+import platform = require("platform");
var DELTA = 0.1;
@@ -174,11 +175,7 @@ export function buildUIWithWeakRefAndInteract(createFunc: (
}
sp.removeChild(weakRef.get());
- if (newPage.ios) {
- // Could cause GC on the next call.
- new ArrayBuffer(4 * 1024 * 1024);
- }
- utils.GC();
+ forceGC();
TKUnit.assert(!weakRef.get(), weakRef.get() + " leaked!");
testFinished = true;
@@ -222,4 +219,12 @@ export function assertAreClose(actual: number, expected: number, message: string
var delta = Math.floor(density) !== density ? 1.1 : DELTA;
TKUnit.assertAreClose(actual, expected, delta, message);
+}
+
+export function forceGC() {
+ if (platform.device.os === platform.platformNames.ios) {
+ // Could cause GC on the next call.
+ new ArrayBuffer(4 * 1024 * 1024);
+ }
+ utils.GC();
}
\ No newline at end of file
diff --git a/apps/tests/weak-event-listener-tests.ts b/apps/tests/weak-event-listener-tests.ts
new file mode 100644
index 000000000..96eb01f34
--- /dev/null
+++ b/apps/tests/weak-event-listener-tests.ts
@@ -0,0 +1,204 @@
+import TKUnit = require("./TKUnit");
+import types = require("utils/types");
+import observable = require("data/observable");
+import weakEvents = require("ui/core/weak-event-listener");
+import helper = require("./ui/helper");
+
+export function test_addWeakEventListener_throwsWhenCalledwitnInvalid_source() {
+ TKUnit.assertThrows(() => {
+ weakEvents.WeakEventListener.addWeakEventListener({
+ source: undefined,
+ target: {},
+ handler: emptyHandler,
+ eventName: observable.Observable.propertyChangeEvent
+ });
+ });
+}
+
+export function test_addWeakEventListener_throwsWhenCalledwitnInvalid_target() {
+ TKUnit.assertThrows(() => {
+ weakEvents.WeakEventListener.addWeakEventListener({
+ source: new observable.Observable(),
+ target: undefined,
+ handler: emptyHandler,
+ eventName: observable.Observable.propertyChangeEvent
+ });
+ });
+}
+
+export function test_addWeakEventListener_throwsWhenCalledwitnInvalid_handler() {
+ TKUnit.assertThrows(() => {
+ weakEvents.WeakEventListener.addWeakEventListener({
+ source: new observable.Observable(),
+ target: {},
+ handler: undefined,
+ eventName: observable.Observable.propertyChangeEvent
+ });
+ });
+}
+
+export function test_addWeakEventListener_throwsWhenCalledwitnInvalid_name() {
+ TKUnit.assertThrows(() => {
+ weakEvents.WeakEventListener.addWeakEventListener({
+ source: new observable.Observable(),
+ target: {},
+ handler: emptyHandler,
+ eventName: undefined
+ });
+ });
+}
+
+export function test_addWeakEventListener_listensForEvent() {
+ var source = new observable.Observable();
+ var target = new Object;
+ var callbackCalled = false;
+ var handler = function (args: observable.EventData) {
+ callbackCalled = true;
+ }
+
+ weakEvents.WeakEventListener.addWeakEventListener({
+ source: source,
+ target: target,
+ handler: handler,
+ eventName: observable.Observable.propertyChangeEvent
+ })
+
+ source.set("testProp", "some value");
+
+ TKUnit.assert(callbackCalled, "Handler not called.");
+}
+
+export function test_removeWeakEventListener_StopsListeningForEvet() {
+ var source = new observable.Observable();
+ var target = new Object;
+ var callbackCalled = false;
+ var handler = function (args: observable.EventData) {
+ callbackCalled = true;
+ }
+
+ var listenerID = weakEvents.WeakEventListener.addWeakEventListener({
+ source: source,
+ target: target,
+ handler: handler,
+ eventName: observable.Observable.propertyChangeEvent
+ })
+
+ weakEvents.WeakEventListener.removeWeakEventListener(listenerID);
+
+ source.set("testProp", "some value");
+ TKUnit.assert(!callbackCalled, "Handler should not be called.");
+}
+
+export function test_handlerIsCalled_WithTargetAsThis() {
+ var source = new observable.Observable();
+ var target = new Object;
+ var callbackCalled = false;
+ var handler = function (args: observable.EventData) {
+ TKUnit.assertEqual(this, target, "this should be the target");
+ callbackCalled = true;
+ }
+
+ weakEvents.WeakEventListener.addWeakEventListener({
+ source: source,
+ target: target,
+ handler: handler,
+ eventName: observable.Observable.propertyChangeEvent
+ })
+
+ source.set("testProp", "some value");
+ TKUnit.assert(callbackCalled, "Handler not called.");
+}
+
+export function test_listnerDoesNotRetainTarget() {
+ var source = new observable.Observable();
+ var target = new Object;
+ var callbackCalled = false;
+ var handler = function (args: observable.EventData) {
+ TKUnit.assertEqual(this, target, "this should be the target");
+ callbackCalled = true;
+ }
+
+ weakEvents.WeakEventListener.addWeakEventListener({
+ source: source,
+ target: target,
+ handler: handler,
+ eventName: observable.Observable.propertyChangeEvent
+ })
+
+ var targetRef = new WeakRef(target);
+ target = undefined;
+ helper.forceGC();
+
+ TKUnit.assert(!targetRef.get(), "Target should be released after GC");
+}
+
+export function test_listnerDoesNotRetainSource() {
+ var source = new observable.Observable();
+ var target = new Object();
+ var callbackCalled = false;
+ var handler = function (args: observable.EventData) {
+ TKUnit.assertEqual(this, target, "this should be the target");
+ callbackCalled = true;
+ }
+
+ weakEvents.WeakEventListener.addWeakEventListener({
+ source: source,
+ target: target,
+ handler: handler,
+ eventName: observable.Observable.propertyChangeEvent
+ })
+
+ var sourceRef = new WeakRef(source);
+ source = undefined;
+ helper.forceGC();
+
+ TKUnit.assert(!sourceRef.get(), "Source should be released after GC");
+}
+
+export function test_listnerIsCleared_WhenTargetIsDead() {
+ var source = new observable.Observable();
+
+ var listenerID = addListenerWithSource(source);
+ helper.forceGC();
+
+ for (var i = 0; i < weakEvents.WeakEventListener.cleanDeadReferencesCountTrigger; i++) {
+ addListenerWithSource(source);
+ }
+
+ TKUnit.assert(types.isUndefined(weakEvents.WeakEventListener._weakEventListeners[listenerID]), "The first listener should be dead by now");
+}
+
+export function test_listnerIsCleared_WhenSourceIsDead() {
+ var target = {};
+
+ var listenerID = addListenerWithTarget(target);
+ helper.forceGC();
+
+ for (var i = 0; i < weakEvents.WeakEventListener.cleanDeadReferencesCountTrigger; i++) {
+ addListenerWithTarget(target);
+ }
+
+ TKUnit.assert(types.isUndefined(weakEvents.WeakEventListener._weakEventListeners[listenerID]), "The first listener should be dead by now");
+}
+
+function addListenerWithSource(source: observable.Observable): number {
+ return weakEvents.WeakEventListener.addWeakEventListener({
+ source: source,
+ target: {},
+ handler: emptyHandler,
+ eventName: observable.Observable.propertyChangeEvent
+ })
+}
+
+function addListenerWithTarget(target: any): number {
+ return weakEvents.WeakEventListener.addWeakEventListener({
+ source: new observable.Observable(),
+ target: target,
+ handler: emptyHandler,
+ eventName: observable.Observable.propertyChangeEvent
+ })
+}
+
+function emptyHandler(data: observable.EventData) {
+ // Do nothing.
+}
\ No newline at end of file
diff --git a/ui/button/button-common.ts b/ui/button/button-common.ts
index 222ba14ed..e1d8cfdc6 100644
--- a/ui/button/button-common.ts
+++ b/ui/button/button-common.ts
@@ -33,12 +33,12 @@ function onFormattedTextPropertyChanged(data: dependencyObservable.PropertyChang
(formattedTextProperty.metadata).onSetNativeValue = onFormattedTextPropertyChanged;
export class Button extends view.View implements definition.Button {
-
public static tapEvent = "tap";
-
public static textProperty = textProperty;
public static formattedTextProperty = formattedTextProperty;
+ private _formattedTextWeakListenerId: number;
+
public _onBindingContextChanged(oldValue: any, newValue: any) {
super._onBindingContextChanged(oldValue, newValue);
if (this.formattedText) {
@@ -60,20 +60,17 @@ export class Button extends view.View implements definition.Button {
set formattedText(value: formattedString.FormattedString) {
if (this.formattedText !== value) {
- var weakEventOptions: weakEventListener.WeakEventListenerOptions = {
- targetWeakRef: new WeakRef(this),
- eventName: observable.Observable.propertyChangeEvent,
- sourceWeakRef: new WeakRef(value),
- handler: this.onFormattedTextChanged,
- handlerContext: this,
- key: "formattedText"
- };
if (this.formattedText) {
- weakEventListener.WeakEventListener.removeWeakEventListener(weakEventOptions);
+ weakEventListener.WeakEventListener.removeWeakEventListener(this._formattedTextWeakListenerId);
}
this._setValue(Button.formattedTextProperty, value);
if (value) {
- weakEventListener.WeakEventListener.addWeakEventListener(weakEventOptions);
+ this._formattedTextWeakListenerId = weakEventListener.WeakEventListener.addWeakEventListener({
+ target: this,
+ source: value,
+ eventName: observable.Observable.propertyChangeEvent,
+ handler: this.onFormattedTextChanged
+ });
}
}
}
diff --git a/ui/core/bindable.ts b/ui/core/bindable.ts
index 6a6bf0762..09e728877 100644
--- a/ui/core/bindable.ts
+++ b/ui/core/bindable.ts
@@ -1,7 +1,7 @@
import observable = require("data/observable");
import definition = require("ui/core/bindable");
import dependencyObservable = require("ui/core/dependency-observable");
-import weakEventListener = require("ui/core/weak-event-listener");
+import weakEvents = require("ui/core/weak-event-listener");
import appModule = require("application");
import types = require("utils/types");
import trace = require("trace");
@@ -108,7 +108,7 @@ export class Bindable extends dependencyObservable.DependencyObservable implemen
}
trace.write(
- "Binding target: " + binding.target.get() +
+ "Binding target: " + binding.target.get() +
" targetProperty: " + binding.options.targetProperty +
" to the changed context: " + newValue, trace.categories.Binding);
binding.unbind();
@@ -124,9 +124,7 @@ export class Binding {
updating = false;
source: WeakRef