Skip to content

Commit 3ac5281

Browse files
author
Alexander Vakrilov
committed
Merge pull request NativeScript#163 from NativeScript/feature/list-view-weak-events
List view weak events
2 parents c582f83 + 06c30f5 commit 3ac5281

File tree

13 files changed

+453
-240
lines changed

13 files changed

+453
-240
lines changed

CrossPlatformModules.csproj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<IISExpressAnonymousAuthentication />
1414
<IISExpressWindowsAuthentication />
1515
<IISExpressUseClassicPipelineMode />
16+
<UseGlobalApplicationHostFile />
1617
</PropertyGroup>
1718
<PropertyGroup>
1819
<Configuration Condition=" '$(Configuration)' == '' ">Cross</Configuration>
@@ -186,6 +187,7 @@
186187
<DependentUpon>main-page.xml</DependentUpon>
187188
</TypeScriptCompile>
188189
<TypeScriptCompile Include="apps\editable-text-demo\model.ts" />
190+
<TypeScriptCompile Include="apps\tests\weak-event-listener-tests.ts" />
189191
<TypeScriptCompile Include="apps\ui-tests-app\pages\i61.ts" />
190192
<TypeScriptCompile Include="apps\ui-tests-app\pages\i73.ts" />
191193
<TypeScriptCompile Include="apps\ui-tests-app\pages\gestures.ts" />
@@ -1575,7 +1577,7 @@
15751577
<SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
15761578
</WebProjectProperties>
15771579
</FlavorProperties>
1578-
<UserProperties ui_2scroll-view_2package_1json__JSONSchema="http://json.schemastore.org/package" apps_2editable-text-demo_2package_1json__JSONSchema="http://json.schemastore.org/package" apps_2absolute-layout-demo_2package_1json__JSONSchema="http://json.schemastore.org/package" apps_2gallery-app_2package_1json__JSONSchema="http://json.schemastore.org/package" ui_2content-view_2package_1json__JSONSchema="http://json.schemastore.org/package" ui_2web-view_2package_1json__JSONSchema="http://json.schemastore.org/package" ui_2layouts_2linear-layout_2package_1json__JSONSchema="http://json.schemastore.org/package" ui_2layouts_2absolute-layout_2package_1json__JSONSchema="http://json.schemastore.org/package" ui_2layouts_2dock-layout_2package_1json__JSONSchema="" ui_2layouts_2grid-layout_2package_1json__JSONSchema="" ui_2layouts_2wrap-layout_2package_1json__JSONSchema="http://json.schemastore.org/package" />
1580+
<UserProperties ui_2layouts_2wrap-layout_2package_1json__JSONSchema="http://json.schemastore.org/package" ui_2layouts_2grid-layout_2package_1json__JSONSchema="" ui_2layouts_2dock-layout_2package_1json__JSONSchema="" ui_2layouts_2absolute-layout_2package_1json__JSONSchema="http://json.schemastore.org/package" ui_2layouts_2linear-layout_2package_1json__JSONSchema="http://json.schemastore.org/package" ui_2web-view_2package_1json__JSONSchema="http://json.schemastore.org/package" ui_2content-view_2package_1json__JSONSchema="http://json.schemastore.org/package" apps_2gallery-app_2package_1json__JSONSchema="http://json.schemastore.org/package" apps_2absolute-layout-demo_2package_1json__JSONSchema="http://json.schemastore.org/package" apps_2editable-text-demo_2package_1json__JSONSchema="http://json.schemastore.org/package" ui_2scroll-view_2package_1json__JSONSchema="http://json.schemastore.org/package" />
15791581
</VisualStudio>
15801582
</ProjectExtensions>
15811583
</Project>

apps/tests/testRunner.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ allTests["LIST-PICKER"] = require("./ui/list-picker/list-picker-tests");
6868
allTests["DATE-PICKER"] = require("./ui/date-picker/date-picker-tests");
6969
allTests["TIME-PICKER"] = require("./ui/time-picker/time-picker-tests");
7070
allTests["WEB-VIEW"] = require("./ui/web-view/web-view-tests");
71+
allTests["WEAK-EVENTS"] = require("./weak-event-listener-tests");
72+
7173
if (!isRunningOnEmulator()) {
7274
allTests["LOCATION"] = require("./location-tests");
7375
}

apps/tests/timer-tests.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import TKUnit = require("./TKUnit");
2+
import platform = require("platform");
23
var timer = require("timer/timer");
34

45
// <snippet module="timer" title="timer">
@@ -85,6 +86,12 @@ export var test_setTimeout_shouldReturnNumber = function () {
8586
};
8687

8788
export var test_setTimeout_callbackShouldBeCleared = function () {
89+
// This test is very unstable in iOS, because the platform does not guarantee the
90+
// callback will be cleared on time. Better skip it for iOS.
91+
if (platform.device.os === platform.platformNames.ios) {
92+
return;
93+
}
94+
8895
var completed: boolean;
8996
var isReady = function () { return completed; }
9097

apps/tests/ui/helper.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import TKUnit = require("../TKUnit");
77
import utils = require("utils/utils");
88
import types = require("utils/types");
99
import styling = require("ui/styling");
10+
import platform = require("platform");
1011

1112
var DELTA = 0.1;
1213

1314
export var ASYNC = 0.2;
15+
export var MEMORY_ASYNC = 2;
1416

1517
export function do_PageTest(test: (views: Array<view.View>) => void, content: view.View, secondView: view.View, thirdView: view.View) {
1618
var newPage: page.Page;
@@ -33,7 +35,7 @@ export function do_PageTest(test: (views: Array<view.View>) => void, content: vi
3335
export function do_PageTest_WithButton(test: (views: Array<view.View>) => void) {
3436
var newPage: page.Page;
3537
var btn: button.Button;
36-
var pageFactory = function(): page.Page {
38+
var pageFactory = function (): page.Page {
3739
newPage = new page.Page();
3840
btn = new button.Button();
3941
newPage.content = btn;
@@ -76,7 +78,7 @@ export function do_PageTest_WithStackLayout_AndButton(test: (views: Array<view.V
7678

7779
export function do_PageTest_WithStackLayout_AndButton_NavigatedBack(test: (views: Array<view.View>) => void,
7880
assert: (views: Array<view.View>) => void) {
79-
81+
8082
var newPage: page.Page;
8183
var stackLayout;
8284
var btn;
@@ -175,6 +177,7 @@ export function buildUIWithWeakRefAndInteract<T extends view.View>(createFunc: (
175177
sp.removeChild(weakRef.get());
176178
if (newPage.ios) {
177179
// Could cause GC on the next call.
180+
// NOTE: Don't replace this with forceGC();
178181
new ArrayBuffer(4 * 1024 * 1024);
179182
}
180183
utils.GC();
@@ -188,7 +191,7 @@ export function buildUIWithWeakRefAndInteract<T extends view.View>(createFunc: (
188191

189192
try {
190193
navigate(pageFactory);
191-
TKUnit.waitUntilReady(() => { return testFinished; });
194+
TKUnit.waitUntilReady(() => { return testFinished; }, MEMORY_ASYNC);
192195
}
193196
finally {
194197
goBack();
@@ -221,4 +224,13 @@ export function assertAreClose(actual: number, expected: number, message: string
221224
var delta = Math.floor(density) !== density ? 1.1 : DELTA;
222225

223226
TKUnit.assertAreClose(actual, expected, delta, message);
227+
}
228+
229+
export function forceGC() {
230+
if (platform.device.os === platform.platformNames.ios) {
231+
// Could cause GC on the next call.
232+
new ArrayBuffer(4 * 1024 * 1024);
233+
TKUnit.wait(ASYNC);
234+
}
235+
utils.GC();
224236
}

apps/tests/ui/list-view/list-view-tests.ts

Lines changed: 50 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -430,28 +430,28 @@ export function test_loadMoreItems_not_raised_when_showing_many_items() {
430430
}
431431

432432
export function test_usingAppLevelConvertersInListViewItems() {
433-
var listView = new listViewModule.ListView();
433+
var listView = new listViewModule.ListView();
434434

435-
var dateConverter = function (value, format) {
436-
var result = format;
437-
var day = value.getDate();
438-
result = result.replace("DD", month < 10 ? "0" + day : day);
439-
var month = value.getMonth() + 1;
440-
result = result.replace("MM", month < 10 ? "0" + month : month);
441-
result = result.replace("YYYY", value.getFullYear());
442-
return result;
443-
};
435+
var dateConverter = function (value, format) {
436+
var result = format;
437+
var day = value.getDate();
438+
result = result.replace("DD", month < 10 ? "0" + day : day);
439+
var month = value.getMonth() + 1;
440+
result = result.replace("MM", month < 10 ? "0" + month : month);
441+
result = result.replace("YYYY", value.getFullYear());
442+
return result;
443+
};
444444

445-
app.resources["dateConverter"] = dateConverter;
445+
app.resources["dateConverter"] = dateConverter;
446446

447447
var data = new observableArray.ObservableArray();
448448

449-
data.push({date: new Date()});
449+
data.push({ date: new Date() });
450450

451451
function testAction(views: Array<viewModule.View>) {
452-
listView.itemTemplate = "<Label id=\"testLabel\" text=\"{{ date, date | dateConverter('DD.MM.YYYY') }}\" />";
452+
listView.itemTemplate = "<Label id=\"testLabel\" text=\"{{ date, date | dateConverter('DD.MM.YYYY') }}\" />";
453453
listView.items = data;
454-
454+
455455
TKUnit.wait(ASYNC);
456456
var nativeElementText = getTextFromNativeElementAt(listView, 0);
457457

@@ -501,6 +501,33 @@ export function test_BindingListViewToASimpleArrayWithExpression() {
501501
helper.buildUIAndRunTest(listView, testAction);
502502
}
503503

504+
export function test_no_memory_leak_when_items_is_regular_array() {
505+
var createFunc = function (): listViewModule.ListView {
506+
var listView = new listViewModule.ListView();
507+
listView.items = FEW_ITEMS;
508+
return listView;
509+
};
510+
511+
helper.buildUIWithWeakRefAndInteract(createFunc, (list) => {
512+
TKUnit.assert(list.isLoaded, "ListView should be loaded here");
513+
});
514+
}
515+
516+
export function test_no_memory_leak_when_items_is_observable_array() {
517+
// Keep the reference to the observable array to test the weakEventListener
518+
var colors = new observableArray.ObservableArray(["red", "green", "blue"]);
519+
520+
var createFunc = function (): listViewModule.ListView {
521+
var listView = new listViewModule.ListView();
522+
listView.items = colors;
523+
return listView;
524+
};
525+
526+
helper.buildUIWithWeakRefAndInteract(createFunc, (list) => {
527+
TKUnit.assert(list.isLoaded, "ListView should be loaded here");
528+
});
529+
}
530+
504531
function loadViewWithItemNumber(args: listViewModule.ItemEventData) {
505532
if (!args.view) {
506533
args.view = new labelModule.Label();
@@ -509,16 +536,16 @@ function loadViewWithItemNumber(args: listViewModule.ItemEventData) {
509536
}
510537

511538
function getTextFromNativeElementAt(listView: listViewModule.ListView, index: number): any {
512-
if (listView.android) {
513-
var nativeElement = listView.android.getChildAt(index);
514-
if (nativeElement instanceof android.view.ViewGroup) {
515-
return (<android.widget.TextView>(<any>nativeElement.getChildAt(0))).getText();
516-
}
539+
if (listView.android) {
540+
var nativeElement = listView.android.getChildAt(index);
541+
if (nativeElement instanceof android.view.ViewGroup) {
542+
return (<android.widget.TextView>((<any>nativeElement).getChildAt(0))).getText();
543+
}
517544
return (<android.widget.TextView>nativeElement).getText();
518-
}
519-
else if (listView.ios) {
520-
return listView.ios.visibleCells()[index].contentView.subviews[0].text;
521-
}
545+
}
546+
else if (listView.ios) {
547+
return listView.ios.visibleCells()[index].contentView.subviews[0].text;
548+
}
522549
}
523550

524551
function getNativeViewCount(listView: listViewModule.ListView): number {
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import TKUnit = require("./TKUnit");
2+
import observable = require("data/observable");
3+
import weakEvents = require("ui/core/weak-event-listener");
4+
import helper = require("./ui/helper");
5+
6+
class Target {
7+
public counter: number = 0;
8+
public onEvent(data: observable.EventData) {
9+
this.counter++;
10+
}
11+
12+
}
13+
14+
export function test_addWeakEventListener_throwsWhenCalledwitnInvalid_source() {
15+
TKUnit.assertThrows(() => {
16+
weakEvents.addWeakEventListener(undefined, "eventName", emptyHandler, {});
17+
});
18+
}
19+
20+
export function test_addWeakEventListener_throwsWhenCalledwitnInvalid_target() {
21+
TKUnit.assertThrows(() => {
22+
weakEvents.addWeakEventListener(new observable.Observable(), "eventName", emptyHandler, undefined);
23+
});
24+
}
25+
26+
export function test_addWeakEventListener_throwsWhenCalledwitnInvalid_handler() {
27+
TKUnit.assertThrows(() => {
28+
weakEvents.addWeakEventListener(new observable.Observable(), "eventName", undefined, {});
29+
});
30+
}
31+
32+
export function test_addWeakEventListener_throwsWhenCalledwitnInvalid_name() {
33+
TKUnit.assertThrows(() => {
34+
weakEvents.addWeakEventListener(new observable.Observable(), undefined, emptyHandler, {});
35+
});
36+
}
37+
38+
export function test_addWeakEventListener_listensForEvent() {
39+
var source = new observable.Observable();
40+
var target = new Target();
41+
42+
weakEvents.addWeakEventListener(
43+
source,
44+
observable.Observable.propertyChangeEvent,
45+
target.onEvent,
46+
target);
47+
48+
helper.forceGC();
49+
50+
source.set("testProp", "some value");
51+
52+
TKUnit.assertEqual(target.counter, 1, "Handler not called.");
53+
}
54+
55+
export function test_addWeakEventListener_listensForEven_multipleTargetst() {
56+
var source = new observable.Observable();
57+
var target1 = new Target();
58+
var target2 = new Target();
59+
60+
weakEvents.addWeakEventListener(source, observable.Observable.propertyChangeEvent, target1.onEvent, target1);
61+
weakEvents.addWeakEventListener(source, observable.Observable.propertyChangeEvent, target2.onEvent, target2);
62+
63+
helper.forceGC();
64+
65+
source.set("testProp", "some value");
66+
67+
TKUnit.assertEqual(target1.counter, 1, "Handler not called.");
68+
TKUnit.assertEqual(target2.counter, 1, "Handler not called.");
69+
}
70+
71+
export function test_removeWeakEventListener_StopsListeningForEvet() {
72+
var source = new observable.Observable();
73+
var target = new Target();
74+
75+
weakEvents.addWeakEventListener(source, observable.Observable.propertyChangeEvent, target.onEvent, target);
76+
weakEvents.removeWeakEventListener(source, observable.Observable.propertyChangeEvent, target.onEvent, target)
77+
78+
source.set("testProp", "some value");
79+
TKUnit.assertEqual(target.counter, 0, "Handler should not be called.");
80+
}
81+
82+
export function test_handlerIsCalled_WithTargetAsThis() {
83+
var source = new observable.Observable();
84+
var target = new Object();
85+
var callbackCalled = false;
86+
var handler = function (args: observable.EventData) {
87+
TKUnit.assertEqual(this, target, "this should be the target");
88+
callbackCalled = true;
89+
}
90+
91+
weakEvents.addWeakEventListener(source, observable.Observable.propertyChangeEvent, handler, target);
92+
93+
source.set("testProp", "some value");
94+
TKUnit.assert(callbackCalled, "Handler not called.");
95+
}
96+
97+
export function test_listnerDoesNotRetainTarget() {
98+
var source = new observable.Observable();
99+
var target = new Target();
100+
101+
weakEvents.addWeakEventListener(source, observable.Observable.propertyChangeEvent, target.onEvent, target);
102+
103+
var targetRef = new WeakRef(target);
104+
target = undefined;
105+
helper.forceGC();
106+
107+
TKUnit.assert(!targetRef.get(), "Target should be released after GC");
108+
}
109+
110+
export function test_listnerDoesNotRetainSource() {
111+
var source = new observable.Observable();
112+
var target = new Target();
113+
114+
weakEvents.addWeakEventListener(source, observable.Observable.propertyChangeEvent, target.onEvent, target);
115+
116+
var sourceRef = new WeakRef(source);
117+
source = undefined;
118+
helper.forceGC();
119+
120+
TKUnit.assert(!sourceRef.get(), "Source should be released after GC");
121+
}
122+
123+
export function test_handlerIsDetached_WhenAllListenersAreRemoved() {
124+
var source = new observable.Observable();
125+
126+
var target1 = new Target();
127+
var target2 = new Target();
128+
129+
weakEvents.addWeakEventListener(source, observable.Observable.propertyChangeEvent, target1.onEvent, target1);
130+
weakEvents.addWeakEventListener(source, observable.Observable.propertyChangeEvent, target2.onEvent, target2);
131+
132+
weakEvents.removeWeakEventListener(source, observable.Observable.propertyChangeEvent, target1.onEvent, target1)
133+
weakEvents.removeWeakEventListener(source, observable.Observable.propertyChangeEvent, target2.onEvent, target2)
134+
135+
TKUnit.assert(!source.hasListeners(observable.Observable.propertyChangeEvent), "All events should be detached");
136+
}
137+
138+
export function test_autoDetachingOfDeadReferences() {
139+
var source = new observable.Observable();
140+
141+
for (var i = 0; i < 100; i++) {
142+
addListenerWithSource(source);
143+
}
144+
145+
helper.forceGC();
146+
147+
var target = new Target();
148+
149+
weakEvents.addWeakEventListener(source, observable.Observable.propertyChangeEvent, target.onEvent, target);
150+
weakEvents.removeWeakEventListener(source, observable.Observable.propertyChangeEvent, target.onEvent, target)
151+
152+
TKUnit.assert(!source.hasListeners(observable.Observable.propertyChangeEvent), "All events should be detached");
153+
}
154+
155+
function addListenerWithSource(source: observable.Observable) {
156+
var target = new Target();
157+
weakEvents.addWeakEventListener(source, observable.Observable.propertyChangeEvent, target.onEvent, target)
158+
}
159+
160+
function emptyHandler(data: observable.EventData) {
161+
// Do nothing.
162+
}

0 commit comments

Comments
 (0)