From 71c9266be3bba60717f604de902dde578a974da1 Mon Sep 17 00:00:00 2001 From: vakrilov Date: Thu, 14 May 2015 13:45:28 +0300 Subject: [PATCH] WeakEvents refactoring and used in ListView --- CrossPlatformModules.csproj | 4 +- apps/tests/testRunner.ts | 2 + apps/tests/ui/helper.ts | 15 +- apps/tests/weak-event-listener-tests.ts | 204 ++++++++++++++++++++++++ ui/button/button-common.ts | 21 ++- ui/core/bindable.ts | 66 ++++---- ui/core/weak-event-listener.d.ts | 36 +++-- ui/core/weak-event-listener.ts | 156 ++++++++++-------- ui/list-view/list-view-common.ts | 34 ++-- ui/text-base/text-base.ts | 20 +-- 10 files changed, 389 insertions(+), 169 deletions(-) create mode 100644 apps/tests/weak-event-listener-tests.ts 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; target: WeakRef; - weakEventListenerOptions: weakEventListener.WeakEventListenerOptions; - - weakEL = weakEventListener.WeakEventListener; + weakEventId: number; private sourceOptions: { instance: WeakRef; property: any }; private targetOptions: { instance: WeakRef; property: any }; @@ -145,11 +143,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 +162,12 @@ 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, + this.weakEventId = weakEvents.WeakEventListener.addWeakEventListener({ + target: this, + source: this.sourceOptions.instance.get(), eventName: observable.Observable.propertyChangeEvent, handler: this.onSourcePropertyChanged, - handlerContext: this, - key: this.options.targetProperty - } - this.weakEL.addWeakEventListener(this.weakEventListenerOptions); + }); } } } @@ -182,24 +177,25 @@ 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; - } + weakEvents.WeakEventListener.removeWeakEventListener(this.weakEventId); + delete this.weakEventId; + + 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 +234,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 +289,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]; diff --git a/ui/core/weak-event-listener.d.ts b/ui/core/weak-event-listener.d.ts index 1637a062b..7424e850c 100644 --- a/ui/core/weak-event-listener.d.ts +++ b/ui/core/weak-event-listener.d.ts @@ -8,12 +8,12 @@ declare module "ui/core/weak-event-listener" { /** * Weak reference to the subscriber (target) of the event listener. */ - targetWeakRef: WeakRef; + target: any; /** - * Weak reference to an instance of observable.Observable class which emits the event. + * Weak reference to an instance of observable. Observable class which emits the event. */ - sourceWeakRef: WeakRef; + source: observable.Observable; /** * Name of the event. @@ -24,16 +24,6 @@ declare module "ui/core/weak-event-listener" { * 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; } /** @@ -43,14 +33,26 @@ declare module "ui/core/weak-event-listener" { /** * 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. + * Returns The id of the WeakEventListener object to be used in removeWeakEventListener method. */ - static addWeakEventListener(options: WeakEventListenerOptions): boolean; + static addWeakEventListener(options: WeakEventListenerOptions): number; /** * Removes and clears all resources from WeakEventListener. - * @param options An instance of WeakEventListenerOptions used to create the WeakEventListener instance. + * @param The id of the WeakEventListener object. */ - static removeWeakEventListener(options: WeakEventListenerOptions): void; + static removeWeakEventListener(listenerId: number): void; + + /** + * Specifies how often will internal clearing of dead references will occur. + * Clearing will be triggered on addWeakEventListener on every N times where N is + * the value of this property. Set 0 to disable clearing. + */ + static cleanDeadReferencesCountTrigger: number; + + //@private + static _cleanDeadReferences(); + static _weakEventListeners: Object; + //@endprivate } } \ No newline at end of file diff --git a/ui/core/weak-event-listener.ts b/ui/core/weak-event-listener.ts index 543673cc6..2bf5def4a 100644 --- a/ui/core/weak-event-listener.ts +++ b/ui/core/weak-event-listener.ts @@ -1,103 +1,121 @@ import observable = require("data/observable"); import definition = require("ui/core/weak-event-listener"); +import types = require("utils/types"); +var CLEAN_TRIGGER_DEFAULT = 1000; export class WeakEventListener implements definition.WeakEventListener { - private listener: WeakRef; - private sender: WeakRef; + private id: number; + private targetRef: WeakRef; + private senderRef: WeakRef; private eventName: string; private handler: (eventData: observable.EventData) => void; - private handlerContext: any; - static rootWeakEventListenersMap = new WeakMap(); + static _idCounter: number = 0; + static _weakEventListeners = {}; + static _cleanDeadReferencesCountTrigger = CLEAN_TRIGGER_DEFAULT; + static get cleanDeadReferencesCountTrigger(): number { + return WeakEventListener._cleanDeadReferencesCountTrigger; + } + static set cleanDeadReferencesCountTrigger(value: number) { + WeakEventListener._cleanDeadReferencesCountTrigger = value; + } private handlerCallback(eventData) { - if (this.handler) { - if (this.handlerContext) { - this.handler.call(this.handlerContext, eventData); - } - else { - this.handler(eventData); - } + var target = this.targetRef.get(); + if (target) { + this.handler.call(target, eventData); + } + else { + // The target is dead - we can unsubscribe; + WeakEventListener.removeWeakEventListener(this.id); } } - private init(options: definition.WeakEventListenerOptions) { - this.listener = options.targetWeakRef; - this.sender = options.sourceWeakRef; + private init(options: definition.WeakEventListenerOptions, listenerId: number) { + this.id = listenerId; + this.targetRef = new WeakRef(options.target); + this.senderRef = new WeakRef(options.source); this.eventName = options.eventName; this.handler = options.handler; - if (options.handlerContext) { - this.handlerContext = options.handlerContext; - } - var sourceInstance = this.sender.get(); + + var sourceInstance = this.senderRef.get(); if (sourceInstance) { sourceInstance.addEventListener(this.eventName, this.handlerCallback, this); } } - 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(); + var sender = this.senderRef.get(); + if (sender) { + sender.removeEventListener(this.eventName, this.handlerCallback, this); } - this.listener = undefined; + + this.targetRef.clear(); + this.targetRef = undefined; + this.senderRef.clear(); + this.senderRef = undefined; + this.eventName = undefined; this.handler = undefined; - this.handlerContext = undefined; } - static getWeakMapValueByKeys(keys) { - var result; - if (!WeakEventListener.rootWeakEventListenersMap) { - WeakEventListener.rootWeakEventListenersMap = new WeakMap(); + static addWeakEventListener(options: definition.WeakEventListenerOptions) { + if (types.isNullOrUndefined(options.target)) { + throw new Error("targetWeakRef is null or undefined"); } - var currentMap = WeakEventListener.rootWeakEventListenersMap; - var i; - for (i = 0; i < keys.length - 1; i++) { - if (currentMap.has(keys[i])) { - currentMap = >currentMap.get(keys[i]); - } - else { - var innerMap = new WeakMap(); - currentMap.set(keys[i], innerMap); - currentMap = innerMap; + + if (types.isNullOrUndefined(options.source)) { + throw new Error("sourceWeakRef is null or undefined"); + } + + if (!types.isString(options.eventName)) { + throw new Error("eventName is not a string"); + } + + if (!types.isFunction(options.handler)) { + throw new Error("handler is not a function"); + } + + var listenerId = WeakEventListener._idCounter++; + var weakEventListener = new WeakEventListener(); + weakEventListener.init(options, listenerId); + + WeakEventListener._weakEventListeners[listenerId] = new WeakRef(weakEventListener); + + if (WeakEventListener._cleanDeadReferencesCountTrigger && + (listenerId % WeakEventListener._cleanDeadReferencesCountTrigger) === 0) { + WeakEventListener._cleanDeadReferences(); + } + + return listenerId; + } + + static removeWeakEventListener(listenerId: number) { + var listenerRef = >WeakEventListener._weakEventListeners[listenerId]; + + if (listenerRef) { + var listener = listenerRef.get(); + if (listener) { + listener.clear(); } } - 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; + delete WeakEventListener._weakEventListeners[listenerId]; } - 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) { - (weakMapValueForKey[options.key]).clear(); - } - delete weakMapValueForKey[options.key]; + static _cleanDeadReferences() { + var deadListeners = new Array(); + for (var id in WeakEventListener._weakEventListeners) { + var listenerRef = >WeakEventListener._weakEventListeners[id]; + + var listener = listenerRef.get(); + if (!listener || !listener.targetRef.get() || !listener.senderRef.get()) { + deadListeners.push(id); } } + + for (var i = 0; i < deadListeners.length; i++) { + WeakEventListener.removeWeakEventListener(deadListeners[i]); + } } -} \ No newline at end of file +} diff --git a/ui/list-view/list-view-common.ts b/ui/list-view/list-view-common.ts index 5d36a53b7..a5cc1ece5 100644 --- a/ui/list-view/list-view-common.ts +++ b/ui/list-view/list-view-common.ts @@ -8,13 +8,13 @@ import builder = require("ui/builder"); import label = require("ui/label"); import color = require("color"); import weakEventListener = require("ui/core/weak-event-listener"); +import types = require("utils/types"); var ITEMS = "items"; var ITEMTEMPLATE = "itemTemplate"; var ISSCROLLING = "isScrolling"; var LISTVIEW = "ListView"; var SEPARATORCOLOR = "separatorColor"; -var WEAKEVENTKEY = "_observableArrayChanged"; export module knownTemplates { export var itemTemplate = "itemTemplate"; @@ -69,13 +69,7 @@ export class ListView extends view.View implements definition.ListView { ) ); - private _itemsChanged: (args: observable.EventData) => void; - private _weakEventListenerOptions: weakEventListener.WeakEventListenerOptions; - - constructor() { - super(); - this._itemsChanged = (args: observable.EventData) => { this.refresh(); }; - } + private _itemsChangedWeakListenerId: number; get items(): any { return this._getValue(ListView.itemsProperty); @@ -136,27 +130,27 @@ export class ListView extends view.View implements definition.ListView { return lbl; } - public _onItemsPropertyChanged(data: dependencyObservable.PropertyChangeData) { - if (data.oldValue instanceof observable.Observable && this._weakEventListenerOptions) { - weakEventListener.WeakEventListener.removeWeakEventListener(this._weakEventListenerOptions); - this._weakEventListenerOptions = null; + if (data.oldValue instanceof observable.Observable && types.isDefined(this._itemsChangedWeakListenerId)) { + weakEventListener.WeakEventListener.removeWeakEventListener(this._itemsChangedWeakListenerId); + delete this._itemsChangedWeakListenerId; } if (data.newValue instanceof observable.Observable) { - this._weakEventListenerOptions = { - targetWeakRef: new WeakRef(this), - sourceWeakRef: new WeakRef(data.newValue), + this._itemsChangedWeakListenerId = weakEventListener.WeakEventListener.addWeakEventListener({ + target: this, + source: data.newValue, eventName: observableArray.ObservableArray.changeEvent, - handler: this._itemsChanged, - handlerContext: this, - key: WEAKEVENTKEY - }; - weakEventListener.WeakEventListener.addWeakEventListener(this._weakEventListenerOptions); + handler: this._onItemsChanged, + }); } this.refresh(); } + + private _onItemsChanged(args: observable.EventData) { + this.refresh(); + } } function getExports(instance: view.View): any { diff --git a/ui/text-base/text-base.ts b/ui/text-base/text-base.ts index 92ab9a124..1bf124d7f 100644 --- a/ui/text-base/text-base.ts +++ b/ui/text-base/text-base.ts @@ -38,6 +38,8 @@ export class TextBase extends view.View implements definition.TextBase { public static textProperty = textProperty; public static formattedTextProperty = formattedTextProperty; + private _formattedTextChangedListenerId: number; + constructor(options?: definition.Options) { super(options); } @@ -77,20 +79,18 @@ 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); + weakEventListener.WeakEventListener.removeWeakEventListener(this._formattedTextChangedListenerId); + delete this._formattedTextChangedListenerId; } this._setValue(TextBase.formattedTextProperty, value); if (value) { - weakEventListener.WeakEventListener.addWeakEventListener(weakEventOptions); + this._formattedTextChangedListenerId = weakEventListener.WeakEventListener.addWeakEventListener({ + target: this, + source: value, + eventName: observable.Observable.propertyChangeEvent, + handler: this.onFormattedTextChanged, + }); } } }