mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
Merge pull request #163 from NativeScript/feature/list-view-weak-events
List view weak events
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
<IISExpressAnonymousAuthentication />
|
||||
<IISExpressWindowsAuthentication />
|
||||
<IISExpressUseClassicPipelineMode />
|
||||
<UseGlobalApplicationHostFile />
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Cross</Configuration>
|
||||
@@ -186,6 +187,7 @@
|
||||
<DependentUpon>main-page.xml</DependentUpon>
|
||||
</TypeScriptCompile>
|
||||
<TypeScriptCompile Include="apps\editable-text-demo\model.ts" />
|
||||
<TypeScriptCompile Include="apps\tests\weak-event-listener-tests.ts" />
|
||||
<TypeScriptCompile Include="apps\ui-tests-app\pages\i61.ts" />
|
||||
<TypeScriptCompile Include="apps\ui-tests-app\pages\i73.ts" />
|
||||
<TypeScriptCompile Include="apps\ui-tests-app\pages\gestures.ts" />
|
||||
@@ -1575,7 +1577,7 @@
|
||||
<SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
|
||||
</WebProjectProperties>
|
||||
</FlavorProperties>
|
||||
<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" />
|
||||
<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" />
|
||||
</VisualStudio>
|
||||
</ProjectExtensions>
|
||||
</Project>
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import TKUnit = require("./TKUnit");
|
||||
import platform = require("platform");
|
||||
var timer = require("timer/timer");
|
||||
|
||||
// <snippet module="timer" title="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; }
|
||||
|
||||
|
||||
@@ -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<view.View>) => 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<view.View>) => void, content: vi
|
||||
export function do_PageTest_WithButton(test: (views: Array<view.View>) => 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<view.V
|
||||
|
||||
export function do_PageTest_WithStackLayout_AndButton_NavigatedBack(test: (views: Array<view.View>) => void,
|
||||
assert: (views: Array<view.View>) => void) {
|
||||
|
||||
|
||||
var newPage: page.Page;
|
||||
var stackLayout;
|
||||
var btn;
|
||||
@@ -175,6 +177,7 @@ export function buildUIWithWeakRefAndInteract<T extends view.View>(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<T extends view.View>(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();
|
||||
}
|
||||
@@ -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<viewModule.View>) {
|
||||
listView.itemTemplate = "<Label id=\"testLabel\" text=\"{{ date, date | dateConverter('DD.MM.YYYY') }}\" />";
|
||||
listView.itemTemplate = "<Label id=\"testLabel\" text=\"{{ date, date | dateConverter('DD.MM.YYYY') }}\" />";
|
||||
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 (<android.widget.TextView>(<any>nativeElement.getChildAt(0))).getText();
|
||||
}
|
||||
if (listView.android) {
|
||||
var nativeElement = listView.android.getChildAt(index);
|
||||
if (nativeElement instanceof android.view.ViewGroup) {
|
||||
return (<android.widget.TextView>((<any>nativeElement).getChildAt(0))).getText();
|
||||
}
|
||||
return (<android.widget.TextView>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 {
|
||||
|
||||
162
apps/tests/weak-event-listener-tests.ts
Normal file
162
apps/tests/weak-event-listener-tests.ts
Normal file
@@ -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.
|
||||
}
|
||||
@@ -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
|
||||
(<proxy.PropertyMetadata>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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Object>;
|
||||
target: WeakRef<Bindable>;
|
||||
weakEventListenerOptions: weakEventListener.WeakEventListenerOptions;
|
||||
|
||||
weakEL = weakEventListener.WeakEventListener;
|
||||
|
||||
private sourceOptions: { instance: WeakRef<any>; property: any };
|
||||
private targetOptions: { instance: WeakRef<any>; property: any };
|
||||
@@ -145,11 +142,11 @@ export class Binding {
|
||||
if (typeof (obj) === "number") {
|
||||
obj = new Number(obj);
|
||||
}
|
||||
|
||||
|
||||
if (typeof (obj) === "boolean") {
|
||||
obj = new Boolean(obj);
|
||||
}
|
||||
|
||||
|
||||
if (typeof (obj) === "string") {
|
||||
obj = new String(obj);
|
||||
}
|
||||
@@ -164,15 +161,11 @@ export class Binding {
|
||||
if (this.sourceOptions) {
|
||||
var sourceOptionsInstance = this.sourceOptions.instance.get();
|
||||
if (sourceOptionsInstance instanceof observable.Observable) {
|
||||
this.weakEventListenerOptions = {
|
||||
targetWeakRef: this.target,
|
||||
sourceWeakRef: this.sourceOptions.instance,
|
||||
eventName: observable.Observable.propertyChangeEvent,
|
||||
handler: this.onSourcePropertyChanged,
|
||||
handlerContext: this,
|
||||
key: this.options.targetProperty
|
||||
}
|
||||
this.weakEL.addWeakEventListener(this.weakEventListenerOptions);
|
||||
weakEvents.addWeakEventListener(
|
||||
sourceOptionsInstance,
|
||||
observable.Observable.propertyChangeEvent,
|
||||
this.onSourcePropertyChanged,
|
||||
this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,24 +175,32 @@ export class Binding {
|
||||
return;
|
||||
}
|
||||
|
||||
this.weakEL.removeWeakEventListener(this.weakEventListenerOptions);
|
||||
this.weakEventListenerOptions = undefined;
|
||||
if (this.source) {
|
||||
this.source.clear();
|
||||
}
|
||||
if (this.sourceOptions) {
|
||||
this.sourceOptions.instance.clear();
|
||||
this.sourceOptions = undefined;
|
||||
}
|
||||
if (this.targetOptions) {
|
||||
this.targetOptions = undefined;
|
||||
}
|
||||
if (this.sourceOptions) {
|
||||
var sourceOptionsInstance = this.sourceOptions.instance.get();
|
||||
if (sourceOptionsInstance) {
|
||||
weakEvents.removeWeakEventListener(sourceOptionsInstance,
|
||||
observable.Observable.propertyChangeEvent,
|
||||
this.onSourcePropertyChanged,
|
||||
this);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.source) {
|
||||
this.source.clear();
|
||||
}
|
||||
if (this.sourceOptions) {
|
||||
this.sourceOptions.instance.clear();
|
||||
this.sourceOptions = undefined;
|
||||
}
|
||||
if (this.targetOptions) {
|
||||
this.targetOptions = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public updateTwoWay(value: any) {
|
||||
if (this.updating) {
|
||||
return;
|
||||
}
|
||||
if (this.updating) {
|
||||
return;
|
||||
}
|
||||
if (this.options.twoWay) {
|
||||
if (this._isExpression(this.options.expression)) {
|
||||
var changedModel = {};
|
||||
@@ -238,15 +239,15 @@ export class Binding {
|
||||
var exp = polymerExpressions.PolymerExpressions.getExpression(expression);
|
||||
if (exp) {
|
||||
var context = this.source && this.source.get && this.source.get() || global;
|
||||
var model = {};
|
||||
model[contextKey] = context;
|
||||
model[resourcesKey] = appModule.resources;
|
||||
return exp.getValue(model, isBackConvert, changedModel);
|
||||
var model = {};
|
||||
model[contextKey] = context;
|
||||
model[resourcesKey] = appModule.resources;
|
||||
return exp.getValue(model, isBackConvert, changedModel);
|
||||
}
|
||||
return new Error(expression + " is not a valid expression.");
|
||||
}
|
||||
catch (e) {
|
||||
var errorMessage = "Run-time error occured in file: " + e.sourceURL + " at line: " + e.line + " and column: " + e.column;
|
||||
var errorMessage = "Run-time error occured in file: " + e.sourceURL + " at line: " + e.line + " and column: " + e.column;
|
||||
return new Error(errorMessage);
|
||||
}
|
||||
}
|
||||
@@ -293,7 +294,7 @@ export class Binding {
|
||||
}
|
||||
else if (sourceOptionsInstance instanceof observable.Observable) {
|
||||
value = sourceOptionsInstance.get(this.sourceOptions.property);
|
||||
}
|
||||
}
|
||||
else if (sourceOptionsInstance && this.sourceOptions.property &&
|
||||
this.sourceOptions.property in sourceOptionsInstance) {
|
||||
value = sourceOptionsInstance[this.sourceOptions.property];
|
||||
|
||||
59
ui/core/weak-event-listener.d.ts
vendored
59
ui/core/weak-event-listener.d.ts
vendored
@@ -2,55 +2,20 @@ declare module "ui/core/weak-event-listener" {
|
||||
import observable = require("data/observable");
|
||||
|
||||
/**
|
||||
* An interface that defines all options needed for creating weak event listener.
|
||||
* Attaches a WeakEventListener.
|
||||
* @param source Observable class which emits the event.
|
||||
* @param eventName The event name.
|
||||
* @param handler The function which should be called when event occurs.
|
||||
* @param target Subscriber (target) of the event listener. It will be used as a thisArg in the handler function.
|
||||
*/
|
||||
export interface WeakEventListenerOptions {
|
||||
/**
|
||||
* Weak reference to the subscriber (target) of the event listener.
|
||||
*/
|
||||
targetWeakRef: WeakRef<any>;
|
||||
|
||||
/**
|
||||
* Weak reference to an instance of observable.Observable class which emits the event.
|
||||
*/
|
||||
sourceWeakRef: WeakRef<observable.Observable>;
|
||||
|
||||
/**
|
||||
* Name of the event.
|
||||
*/
|
||||
eventName: string;
|
||||
|
||||
/**
|
||||
* The function which should be called when event occurs.
|
||||
*/
|
||||
handler: (eventData: observable.EventData) => void;
|
||||
|
||||
/**
|
||||
* The context (thisArg) in which handler should be executed.
|
||||
*/
|
||||
handlerContext?: any;
|
||||
|
||||
/**
|
||||
* A string to use as key for key value pair instance.
|
||||
*/
|
||||
key?: string;
|
||||
}
|
||||
export function addWeakEventListener(source: observable.Observable, eventName: string, handler: (eventData: observable.EventData) => void, target: any) : void;
|
||||
|
||||
/**
|
||||
* Represents a class that utilize work with weak event listeners.
|
||||
* Removes a WeakEventListener.
|
||||
* @param source Observable class which emits the event.
|
||||
* @param eventName The event name.
|
||||
* @param handler The function which should be called when event occurs.
|
||||
* @param target Subscriber (target) of the event listener. It will be used as a thisArg in the handler function.
|
||||
*/
|
||||
export class WeakEventListener {
|
||||
/**
|
||||
* Creates and initialize WeakEventListener (if all required options are set).
|
||||
* @param options An instance of WeakEventListenerOptions needed to create WeakEventListener instance.
|
||||
* Returns true if a WeakEventListener instance is created successfully.
|
||||
*/
|
||||
static addWeakEventListener(options: WeakEventListenerOptions): boolean;
|
||||
|
||||
/**
|
||||
* Removes and clears all resources from WeakEventListener.
|
||||
* @param options An instance of WeakEventListenerOptions used to create the WeakEventListener instance.
|
||||
*/
|
||||
static removeWeakEventListener(options: WeakEventListenerOptions): void;
|
||||
}
|
||||
export function removeWeakEventListener(source: observable.Observable, eventName: string, handler: (eventData: observable.EventData) => void, target: any): void;
|
||||
}
|
||||
@@ -1,103 +1,148 @@
|
||||
import observable = require("data/observable");
|
||||
import definition = require("ui/core/weak-event-listener");
|
||||
import types = require("utils/types");
|
||||
|
||||
export class WeakEventListener implements definition.WeakEventListener {
|
||||
private listener: WeakRef<any>;
|
||||
private sender: WeakRef<observable.Observable>;
|
||||
private eventName: string;
|
||||
private handler: (eventData: observable.EventData) => void;
|
||||
private handlerContext: any;
|
||||
var handlersForEventName = new Map<string,(eventData: observable.EventData) => void>();
|
||||
var sourcesMap = new WeakMap<observable.Observable, Map<string, Array<TargetHandlerPair>>>();
|
||||
|
||||
static rootWeakEventListenersMap = new WeakMap();
|
||||
class TargetHandlerPair {
|
||||
tagetRef: WeakRef<Object>;
|
||||
handler: (eventData: observable.EventData) => void;
|
||||
|
||||
private handlerCallback(eventData) {
|
||||
if (this.handler) {
|
||||
if (this.handlerContext) {
|
||||
this.handler.call(this.handlerContext, eventData);
|
||||
constructor(target: Object, handler: (eventData: observable.EventData) => void) {
|
||||
this.tagetRef = new WeakRef(target);
|
||||
this.handler = handler;
|
||||
}
|
||||
}
|
||||
|
||||
function getHandlerForEventName(eventName: string): (eventData: observable.EventData) => void {
|
||||
var handler = handlersForEventName.get(eventName);
|
||||
if (!handler) {
|
||||
handler = function (eventData: observable.EventData) {
|
||||
var source = eventData.object;
|
||||
var sourceEventMap = sourcesMap.get(source);
|
||||
if (!sourceEventMap) {
|
||||
// There is no event map for this source - it is safe to detach the listener;
|
||||
source.removeEventListener(eventName, handlersForEventName.get(eventName));
|
||||
return;
|
||||
}
|
||||
else {
|
||||
this.handler(eventData);
|
||||
|
||||
var targetHandlerPairList = sourceEventMap.get(eventName);
|
||||
if (!targetHandlerPairList) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private init(options: definition.WeakEventListenerOptions) {
|
||||
this.listener = options.targetWeakRef;
|
||||
this.sender = options.sourceWeakRef;
|
||||
this.eventName = options.eventName;
|
||||
this.handler = options.handler;
|
||||
if (options.handlerContext) {
|
||||
this.handlerContext = options.handlerContext;
|
||||
}
|
||||
var sourceInstance = this.sender.get();
|
||||
if (sourceInstance) {
|
||||
sourceInstance.addEventListener(this.eventName, this.handlerCallback, this);
|
||||
}
|
||||
}
|
||||
var deadPairsIndexes = [];
|
||||
for (var i = 0; i < targetHandlerPairList.length; i++) {
|
||||
var pair = targetHandlerPairList[i];
|
||||
|
||||
static addWeakEventListener(options: definition.WeakEventListenerOptions) {
|
||||
if (options.targetWeakRef && options.sourceWeakRef && options.eventName && options.handler && options.key) {
|
||||
var weakEventListener = new WeakEventListener();
|
||||
weakEventListener.init(options);
|
||||
var targetWeakEventListenersMap = WeakEventListener.getWeakMapValueByKeys([options.sourceWeakRef, options.targetWeakRef]);
|
||||
targetWeakEventListenersMap[options.key] = weakEventListener;
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private clear() {
|
||||
var sourceInstance = this.sender.get();
|
||||
if (sourceInstance) {
|
||||
sourceInstance.removeEventListener(this.eventName, this.handlerCallback, this);
|
||||
this.sender.clear();
|
||||
}
|
||||
this.listener = undefined;
|
||||
this.eventName = undefined;
|
||||
this.handler = undefined;
|
||||
this.handlerContext = undefined;
|
||||
}
|
||||
|
||||
static getWeakMapValueByKeys(keys) {
|
||||
var result;
|
||||
if (!WeakEventListener.rootWeakEventListenersMap) {
|
||||
WeakEventListener.rootWeakEventListenersMap = new WeakMap();
|
||||
}
|
||||
var currentMap = WeakEventListener.rootWeakEventListenersMap;
|
||||
var i;
|
||||
for (i = 0; i < keys.length - 1; i++) {
|
||||
if (currentMap.has(keys[i])) {
|
||||
currentMap = <WeakMap<{}, {}>>currentMap.get(keys[i]);
|
||||
}
|
||||
else {
|
||||
var innerMap = new WeakMap();
|
||||
currentMap.set(keys[i], innerMap);
|
||||
currentMap = innerMap;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentMap.has(keys[keys.length - 1])) {
|
||||
result = currentMap.get(keys[keys.length - 1]);
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
result = {};
|
||||
currentMap.set(keys[keys.length - 1], result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static removeWeakEventListener(options: definition.WeakEventListenerOptions) {
|
||||
if (options && options.sourceWeakRef && options.targetWeakRef && options.key) {
|
||||
var weakMapValueForKey = WeakEventListener.getWeakMapValueByKeys([options.sourceWeakRef, options.targetWeakRef]);
|
||||
if (weakMapValueForKey && weakMapValueForKey[options.key]) {
|
||||
if (weakMapValueForKey[options.key] instanceof definition.WeakEventListener) {
|
||||
(<WeakEventListener>weakMapValueForKey[options.key]).clear();
|
||||
var target = pair.tagetRef.get();
|
||||
if (target) {
|
||||
pair.handler.call(target, eventData);
|
||||
}
|
||||
else {
|
||||
deadPairsIndexes.push(i);
|
||||
}
|
||||
delete weakMapValueForKey[options.key];
|
||||
}
|
||||
|
||||
if (deadPairsIndexes.length === targetHandlerPairList.length) {
|
||||
// There are no alive targets for this event - unsubscribe
|
||||
source.removeEventListener(eventName, handlersForEventName.get(eventName));
|
||||
sourceEventMap.delete(eventName);
|
||||
}
|
||||
else {
|
||||
for (var j = deadPairsIndexes.length - 1; j >= 0; j--) {
|
||||
targetHandlerPairList.splice(deadPairsIndexes[j], 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
handlersForEventName.set(eventName, handler);
|
||||
}
|
||||
|
||||
return handler;
|
||||
}
|
||||
|
||||
function validateArgs(source: observable.Observable, eventName: string, handler: (eventData: observable.EventData) => void, target: any) {
|
||||
if (types.isNullOrUndefined(source)) {
|
||||
throw new Error("source is null or undefined");
|
||||
}
|
||||
|
||||
if (types.isNullOrUndefined(target)) {
|
||||
throw new Error("target is null or undefined");
|
||||
}
|
||||
|
||||
if (!types.isString(eventName)) {
|
||||
throw new Error("eventName is not a string");
|
||||
}
|
||||
|
||||
if (!types.isFunction(handler)) {
|
||||
throw new Error("handler is not a function");
|
||||
}
|
||||
}
|
||||
|
||||
export function addWeakEventListener(source: observable.Observable, eventName: string, handler: (eventData: observable.EventData) => void, target: any) {
|
||||
validateArgs(source, eventName, handler, target);
|
||||
|
||||
var shouldAttach: boolean = false;
|
||||
|
||||
var sourceEventMap = sourcesMap.get(source);
|
||||
if (!sourceEventMap) {
|
||||
sourceEventMap = new Map<string, Array<TargetHandlerPair>>();
|
||||
sourcesMap.set(source, sourceEventMap);
|
||||
shouldAttach = true;
|
||||
}
|
||||
|
||||
var pairList = sourceEventMap.get(eventName);
|
||||
if (!pairList) {
|
||||
pairList = new Array<TargetHandlerPair>();
|
||||
sourceEventMap.set(eventName, pairList);
|
||||
shouldAttach = true;
|
||||
}
|
||||
|
||||
pairList.push(new TargetHandlerPair(target, handler));
|
||||
|
||||
if (shouldAttach) {
|
||||
source.addEventListener(eventName, getHandlerForEventName(eventName));
|
||||
}
|
||||
}
|
||||
|
||||
export function removeWeakEventListener(source: observable.Observable, eventName: string, handler: (eventData: observable.EventData) => void, target: any) {
|
||||
validateArgs(source, eventName, handler, target);
|
||||
|
||||
var handlerForEventWithName = handlersForEventName.get(eventName);
|
||||
if (!handlerForEventWithName) {
|
||||
// We have never created handler for event with this name;
|
||||
return;
|
||||
}
|
||||
|
||||
var sourceEventMap = sourcesMap.get(source);
|
||||
if (!sourceEventMap) {
|
||||
return;
|
||||
}
|
||||
|
||||
var targetHandlerPairList = sourceEventMap.get(eventName);
|
||||
if (!targetHandlerPairList) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove all pairs that match given target and handler or have a dead target
|
||||
var targetHandlerPairsToRemove = [];
|
||||
for (var i = 0; i < targetHandlerPairList.length; i++) {
|
||||
var pair = targetHandlerPairList[i];
|
||||
|
||||
var registeredTarget = pair.tagetRef.get();
|
||||
if (!registeredTarget || (registeredTarget === target && handler === pair.handler)) {
|
||||
targetHandlerPairsToRemove.push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (targetHandlerPairsToRemove.length === targetHandlerPairList.length) {
|
||||
// There are no alive targets for this event - unsubscribe
|
||||
source.removeEventListener(eventName, handlerForEventWithName);
|
||||
sourceEventMap.delete(eventName);
|
||||
}
|
||||
else {
|
||||
for (var j = targetHandlerPairsToRemove.length - 1; j >= 0; j--) {
|
||||
targetHandlerPairList.splice(targetHandlerPairsToRemove[j], 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import observable = require("data/observable");
|
||||
import observableArray = require("data/observable-array");
|
||||
import view = require("ui/core/view");
|
||||
import proxy = require("ui/core/proxy");
|
||||
import definition = require("ui/list-view");
|
||||
@@ -6,13 +7,12 @@ import dependencyObservable = require("ui/core/dependency-observable");
|
||||
import builder = require("ui/builder");
|
||||
import label = require("ui/label");
|
||||
import color = require("color");
|
||||
import weakEvents = require("ui/core/weak-event-listener");
|
||||
|
||||
var ITEMS = "items";
|
||||
var ITEMTEMPLATE = "itemTemplate";
|
||||
var ISSCROLLING = "isScrolling";
|
||||
var LISTVIEW = "ListView";
|
||||
var ITEMSCHANGED = "_itemsChanged";
|
||||
var CHANGE = "change";
|
||||
var SEPARATORCOLOR = "separatorColor";
|
||||
|
||||
export module knownTemplates {
|
||||
@@ -20,18 +20,8 @@ export module knownTemplates {
|
||||
}
|
||||
|
||||
function onItemsPropertyChanged(data: dependencyObservable.PropertyChangeData) {
|
||||
var listView = <definition.ListView>data.object;
|
||||
var itemsChanged = listView[ITEMSCHANGED];
|
||||
|
||||
if (data.oldValue instanceof observable.Observable) {
|
||||
(<observable.Observable>data.oldValue).off(CHANGE, itemsChanged);
|
||||
}
|
||||
|
||||
if (data.newValue instanceof observable.Observable) {
|
||||
(<observable.Observable>data.newValue).on(CHANGE, itemsChanged);
|
||||
}
|
||||
|
||||
listView.refresh();
|
||||
var listView = <ListView>data.object;
|
||||
listView._onItemsPropertyChanged(data);
|
||||
}
|
||||
|
||||
function onItemTemplatePropertyChanged(data: dependencyObservable.PropertyChangeData) {
|
||||
@@ -78,13 +68,6 @@ export class ListView extends view.View implements definition.ListView {
|
||||
)
|
||||
);
|
||||
|
||||
private _itemsChanged: (args: observable.EventData) => void;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._itemsChanged = (args: observable.EventData) => { this.refresh(); };
|
||||
}
|
||||
|
||||
get items(): any {
|
||||
return this._getValue(ListView.itemsProperty);
|
||||
}
|
||||
@@ -143,6 +126,22 @@ export class ListView extends view.View implements definition.ListView {
|
||||
lbl.text = this._getDataItem(index) + "";
|
||||
return lbl;
|
||||
}
|
||||
|
||||
public _onItemsPropertyChanged(data: dependencyObservable.PropertyChangeData) {
|
||||
if (data.oldValue instanceof observable.Observable) {
|
||||
weakEvents.removeWeakEventListener(data.oldValue, observableArray.ObservableArray.changeEvent, this._onItemsChanged, this);
|
||||
}
|
||||
|
||||
if (data.newValue instanceof observable.Observable) {
|
||||
weakEvents.addWeakEventListener(data.newValue, observableArray.ObservableArray.changeEvent, this._onItemsChanged, this);
|
||||
}
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
private _onItemsChanged(args: observable.EventData) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
function getExports(instance: view.View): any {
|
||||
|
||||
@@ -151,6 +151,7 @@ export class ListView extends common.ListView {
|
||||
private _delegate;
|
||||
private _heights: Array<number>;
|
||||
private _preparingCell: boolean = false;
|
||||
private _isDataDirty: boolean = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -172,6 +173,9 @@ export class ListView extends common.ListView {
|
||||
|
||||
public onLoaded() {
|
||||
super.onLoaded();
|
||||
if (this._isDataDirty) {
|
||||
this.refresh();
|
||||
}
|
||||
this._ios.delegate = this._delegate;
|
||||
}
|
||||
|
||||
@@ -185,8 +189,13 @@ export class ListView extends common.ListView {
|
||||
}
|
||||
|
||||
public refresh() {
|
||||
this._ios.reloadData();
|
||||
this.requestLayout();
|
||||
if (this.isLoaded) {
|
||||
this._ios.reloadData();
|
||||
this.requestLayout();
|
||||
this._isDataDirty = false;
|
||||
} else {
|
||||
this._isDataDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
public getHeight(index: number): number {
|
||||
@@ -242,7 +251,7 @@ export class ListView extends common.ListView {
|
||||
cell.contentView.addSubview(view.ios);
|
||||
this._addView(view);
|
||||
}
|
||||
|
||||
|
||||
this._prepareItem(view, indexPath.row);
|
||||
cellHeight = this._layoutCell(view, indexPath);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import observable = require("data/observable");
|
||||
import dependencyObservable = require("ui/core/dependency-observable");
|
||||
import proxy = require("ui/core/proxy");
|
||||
import formattedString = require("text/formatted-string");
|
||||
import weakEventListener = require("ui/core/weak-event-listener");
|
||||
import weakEvents = require("ui/core/weak-event-listener");
|
||||
import utils = require("utils/utils");
|
||||
import trace = require("trace");
|
||||
|
||||
@@ -77,20 +77,12 @@ export class TextBase extends view.View implements definition.TextBase {
|
||||
|
||||
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(TextBase.formattedTextProperty, value);
|
||||
if (value) {
|
||||
weakEventListener.WeakEventListener.addWeakEventListener(weakEventOptions);
|
||||
weakEvents.addWeakEventListener(value, observable.Observable.propertyChangeEvent, this.onFormattedTextChanged, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user