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/timer-tests.ts b/apps/tests/timer-tests.ts
index 18d309db2..26e418a35 100644
--- a/apps/tests/timer-tests.ts
+++ b/apps/tests/timer-tests.ts
@@ -1,4 +1,5 @@
import TKUnit = require("./TKUnit");
+import platform = require("platform");
var timer = require("timer/timer");
//
@@ -85,6 +86,12 @@ export var test_setTimeout_shouldReturnNumber = function () {
};
export var test_setTimeout_callbackShouldBeCleared = function () {
+ // This test is very unstable in iOS, because the platform does not guarantee the
+ // callback will be cleared on time. Better skip it for iOS.
+ if (platform.device.os === platform.platformNames.ios) {
+ return;
+ }
+
var completed: boolean;
var isReady = function () { return completed; }
diff --git a/apps/tests/ui/helper.ts b/apps/tests/ui/helper.ts
index 04cff1d1b..5b021bea4 100644
--- a/apps/tests/ui/helper.ts
+++ b/apps/tests/ui/helper.ts
@@ -7,10 +7,12 @@ 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;
export var ASYNC = 0.2;
+export var MEMORY_ASYNC = 2;
export function do_PageTest(test: (views: Array) => void, content: view.View, secondView: view.View, thirdView: view.View) {
var newPage: page.Page;
@@ -33,7 +35,7 @@ export function do_PageTest(test: (views: Array) => void, content: vi
export function do_PageTest_WithButton(test: (views: Array) => void) {
var newPage: page.Page;
var btn: button.Button;
- var pageFactory = function(): page.Page {
+ var pageFactory = function (): page.Page {
newPage = new page.Page();
btn = new button.Button();
newPage.content = btn;
@@ -76,7 +78,7 @@ export function do_PageTest_WithStackLayout_AndButton(test: (views: Array) => void,
assert: (views: Array) => void) {
-
+
var newPage: page.Page;
var stackLayout;
var btn;
@@ -175,6 +177,7 @@ export function buildUIWithWeakRefAndInteract(createFunc: (
sp.removeChild(weakRef.get());
if (newPage.ios) {
// Could cause GC on the next call.
+ // NOTE: Don't replace this with forceGC();
new ArrayBuffer(4 * 1024 * 1024);
}
utils.GC();
@@ -188,7 +191,7 @@ export function buildUIWithWeakRefAndInteract(createFunc: (
try {
navigate(pageFactory);
- TKUnit.waitUntilReady(() => { return testFinished; });
+ TKUnit.waitUntilReady(() => { return testFinished; }, MEMORY_ASYNC);
}
finally {
goBack();
@@ -221,4 +224,13 @@ 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);
+ TKUnit.wait(ASYNC);
+ }
+ utils.GC();
}
\ No newline at end of file
diff --git a/apps/tests/ui/list-view/list-view-tests.ts b/apps/tests/ui/list-view/list-view-tests.ts
index 5522104d9..c8562b228 100644
--- a/apps/tests/ui/list-view/list-view-tests.ts
+++ b/apps/tests/ui/list-view/list-view-tests.ts
@@ -430,28 +430,28 @@ export function test_loadMoreItems_not_raised_when_showing_many_items() {
}
export function test_usingAppLevelConvertersInListViewItems() {
- var listView = new listViewModule.ListView();
+ var listView = new listViewModule.ListView();
- var dateConverter = function (value, format) {
- var result = format;
- var day = value.getDate();
- result = result.replace("DD", month < 10 ? "0" + day : day);
- var month = value.getMonth() + 1;
- result = result.replace("MM", month < 10 ? "0" + month : month);
- result = result.replace("YYYY", value.getFullYear());
- return result;
- };
+ var dateConverter = function (value, format) {
+ var result = format;
+ var day = value.getDate();
+ result = result.replace("DD", month < 10 ? "0" + day : day);
+ var month = value.getMonth() + 1;
+ result = result.replace("MM", month < 10 ? "0" + month : month);
+ result = result.replace("YYYY", value.getFullYear());
+ return result;
+ };
- app.resources["dateConverter"] = dateConverter;
+ app.resources["dateConverter"] = dateConverter;
var data = new observableArray.ObservableArray();
- data.push({date: new Date()});
+ data.push({ date: new Date() });
function testAction(views: Array) {
- listView.itemTemplate = "";
+ listView.itemTemplate = "";
listView.items = data;
-
+
TKUnit.wait(ASYNC);
var nativeElementText = getTextFromNativeElementAt(listView, 0);
@@ -501,6 +501,33 @@ export function test_BindingListViewToASimpleArrayWithExpression() {
helper.buildUIAndRunTest(listView, testAction);
}
+export function test_no_memory_leak_when_items_is_regular_array() {
+ var createFunc = function (): listViewModule.ListView {
+ var listView = new listViewModule.ListView();
+ listView.items = FEW_ITEMS;
+ return listView;
+ };
+
+ helper.buildUIWithWeakRefAndInteract(createFunc, (list) => {
+ TKUnit.assert(list.isLoaded, "ListView should be loaded here");
+ });
+}
+
+export function test_no_memory_leak_when_items_is_observable_array() {
+ // Keep the reference to the observable array to test the weakEventListener
+ var colors = new observableArray.ObservableArray(["red", "green", "blue"]);
+
+ var createFunc = function (): listViewModule.ListView {
+ var listView = new listViewModule.ListView();
+ listView.items = colors;
+ return listView;
+ };
+
+ helper.buildUIWithWeakRefAndInteract(createFunc, (list) => {
+ TKUnit.assert(list.isLoaded, "ListView should be loaded here");
+ });
+}
+
function loadViewWithItemNumber(args: listViewModule.ItemEventData) {
if (!args.view) {
args.view = new labelModule.Label();
@@ -509,16 +536,16 @@ function loadViewWithItemNumber(args: listViewModule.ItemEventData) {
}
function getTextFromNativeElementAt(listView: listViewModule.ListView, index: number): any {
- if (listView.android) {
- var nativeElement = listView.android.getChildAt(index);
- if (nativeElement instanceof android.view.ViewGroup) {
- return ((nativeElement.getChildAt(0))).getText();
- }
+ if (listView.android) {
+ var nativeElement = listView.android.getChildAt(index);
+ if (nativeElement instanceof android.view.ViewGroup) {
+ return (((nativeElement).getChildAt(0))).getText();
+ }
return (nativeElement).getText();
- }
- else if (listView.ios) {
- return listView.ios.visibleCells()[index].contentView.subviews[0].text;
- }
+ }
+ else if (listView.ios) {
+ return listView.ios.visibleCells()[index].contentView.subviews[0].text;
+ }
}
function getNativeViewCount(listView: listViewModule.ListView): number {
diff --git a/apps/tests/weak-event-listener-tests.ts b/apps/tests/weak-event-listener-tests.ts
new file mode 100644
index 000000000..c4108468d
--- /dev/null
+++ b/apps/tests/weak-event-listener-tests.ts
@@ -0,0 +1,162 @@
+import TKUnit = require("./TKUnit");
+import observable = require("data/observable");
+import weakEvents = require("ui/core/weak-event-listener");
+import helper = require("./ui/helper");
+
+class Target {
+ public counter: number = 0;
+ public onEvent(data: observable.EventData) {
+ this.counter++;
+ }
+
+}
+
+export function test_addWeakEventListener_throwsWhenCalledwitnInvalid_source() {
+ TKUnit.assertThrows(() => {
+ weakEvents.addWeakEventListener(undefined, "eventName", emptyHandler, {});
+ });
+}
+
+export function test_addWeakEventListener_throwsWhenCalledwitnInvalid_target() {
+ TKUnit.assertThrows(() => {
+ weakEvents.addWeakEventListener(new observable.Observable(), "eventName", emptyHandler, undefined);
+ });
+}
+
+export function test_addWeakEventListener_throwsWhenCalledwitnInvalid_handler() {
+ TKUnit.assertThrows(() => {
+ weakEvents.addWeakEventListener(new observable.Observable(), "eventName", undefined, {});
+ });
+}
+
+export function test_addWeakEventListener_throwsWhenCalledwitnInvalid_name() {
+ TKUnit.assertThrows(() => {
+ weakEvents.addWeakEventListener(new observable.Observable(), undefined, emptyHandler, {});
+ });
+}
+
+export function test_addWeakEventListener_listensForEvent() {
+ var source = new observable.Observable();
+ var target = new Target();
+
+ weakEvents.addWeakEventListener(
+ source,
+ observable.Observable.propertyChangeEvent,
+ target.onEvent,
+ target);
+
+ helper.forceGC();
+
+ source.set("testProp", "some value");
+
+ TKUnit.assertEqual(target.counter, 1, "Handler not called.");
+}
+
+export function test_addWeakEventListener_listensForEven_multipleTargetst() {
+ var source = new observable.Observable();
+ var target1 = new Target();
+ var target2 = new Target();
+
+ weakEvents.addWeakEventListener(source, observable.Observable.propertyChangeEvent, target1.onEvent, target1);
+ weakEvents.addWeakEventListener(source, observable.Observable.propertyChangeEvent, target2.onEvent, target2);
+
+ helper.forceGC();
+
+ source.set("testProp", "some value");
+
+ TKUnit.assertEqual(target1.counter, 1, "Handler not called.");
+ TKUnit.assertEqual(target2.counter, 1, "Handler not called.");
+}
+
+export function test_removeWeakEventListener_StopsListeningForEvet() {
+ var source = new observable.Observable();
+ var target = new Target();
+
+ weakEvents.addWeakEventListener(source, observable.Observable.propertyChangeEvent, target.onEvent, target);
+ weakEvents.removeWeakEventListener(source, observable.Observable.propertyChangeEvent, target.onEvent, target)
+
+ source.set("testProp", "some value");
+ TKUnit.assertEqual(target.counter, 0, "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.addWeakEventListener(source, observable.Observable.propertyChangeEvent, handler, target);
+
+ source.set("testProp", "some value");
+ TKUnit.assert(callbackCalled, "Handler not called.");
+}
+
+export function test_listnerDoesNotRetainTarget() {
+ var source = new observable.Observable();
+ var target = new Target();
+
+ weakEvents.addWeakEventListener(source, observable.Observable.propertyChangeEvent, target.onEvent, target);
+
+ 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 Target();
+
+ weakEvents.addWeakEventListener(source, observable.Observable.propertyChangeEvent, target.onEvent, target);
+
+ var sourceRef = new WeakRef(source);
+ source = undefined;
+ helper.forceGC();
+
+ TKUnit.assert(!sourceRef.get(), "Source should be released after GC");
+}
+
+export function test_handlerIsDetached_WhenAllListenersAreRemoved() {
+ var source = new observable.Observable();
+
+ var target1 = new Target();
+ var target2 = new Target();
+
+ weakEvents.addWeakEventListener(source, observable.Observable.propertyChangeEvent, target1.onEvent, target1);
+ weakEvents.addWeakEventListener(source, observable.Observable.propertyChangeEvent, target2.onEvent, target2);
+
+ weakEvents.removeWeakEventListener(source, observable.Observable.propertyChangeEvent, target1.onEvent, target1)
+ weakEvents.removeWeakEventListener(source, observable.Observable.propertyChangeEvent, target2.onEvent, target2)
+
+ TKUnit.assert(!source.hasListeners(observable.Observable.propertyChangeEvent), "All events should be detached");
+}
+
+export function test_autoDetachingOfDeadReferences() {
+ var source = new observable.Observable();
+
+ for (var i = 0; i < 100; i++) {
+ addListenerWithSource(source);
+ }
+
+ helper.forceGC();
+
+ var target = new Target();
+
+ weakEvents.addWeakEventListener(source, observable.Observable.propertyChangeEvent, target.onEvent, target);
+ weakEvents.removeWeakEventListener(source, observable.Observable.propertyChangeEvent, target.onEvent, target)
+
+ TKUnit.assert(!source.hasListeners(observable.Observable.propertyChangeEvent), "All events should be detached");
+}
+
+function addListenerWithSource(source: observable.Observable) {
+ var target = new Target();
+ weakEvents.addWeakEventListener(source, observable.Observable.propertyChangeEvent, target.onEvent, target)
+}
+
+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..47922c008 100644
--- a/ui/button/button-common.ts
+++ b/ui/button/button-common.ts
@@ -4,7 +4,7 @@ import definition = require("ui/button");
import proxy = require("ui/core/proxy");
import formattedString = require("text/formatted-string");
import observable = require("data/observable");
-import weakEventListener = require("ui/core/weak-event-listener");
+import weakEvents = require("ui/core/weak-event-listener");
var textProperty = new dependencyObservable.Property(
"text",
@@ -33,9 +33,7 @@ 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;
@@ -60,20 +58,12 @@ 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);
+ weakEvents.removeWeakEventListener(this.formattedText, observable.Observable.propertyChangeEvent, this.onFormattedTextChanged, this);
}
this._setValue(Button.formattedTextProperty, value);
if (value) {
- weakEventListener.WeakEventListener.addWeakEventListener(weakEventOptions);
+ weakEvents.addWeakEventListener(value, observable.Observable.propertyChangeEvent, this.onFormattedTextChanged, this);
}
}
}
diff --git a/ui/core/bindable.ts b/ui/core/bindable.ts
index 6a6bf0762..13fdc8de8 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,6 @@ export class Binding {
updating = false;
source: WeakRef