//////////////////////////////////////////////////////////////////////////////// // // Licensed to the Apache Software Foundation (ASF) under one or more // contributor license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright ownership. // The ASF licenses this file to You under the Apache License, Version 2.0 // (the "License"); you may not use this file except in compliance with // the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // //////////////////////////////////////////////////////////////////////////////// package mx.collections { import flash.events.Event; import flash.utils.getQualifiedClassName; import mx.collections.errors.ItemPendingError; import mx.core.mx_internal; import mx.events.CollectionEvent; import mx.events.CollectionEventKind; import mx.events.PropertyChangeEvent; import mx.events.PropertyChangeEventKind; import mx.utils.OnDemandEventDispatcher; use namespace mx_internal; // for mx_internal functions pendingItemSucceeded,Failed() /** * Dispatched when the list's length has changed or when a list * element is replaced. * * @eventType mx.events.CollectionEvent.COLLECTION_CHANGE * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ [Event(name="collectionChange", type="mx.events.CollectionEvent")] /** * The AsyncListView class is an implementation of the IList interface * that handles ItemPendingErrors errors * thrown by the getItemAt(), removeItemAt(), * and toArray() methods. * *

The getItemAt() method handles ItemPendingErrors by returning a provisional * "pending" item until the underlying request succeeds or fails. The provisional * item is produced by calling the function specified by the createPendingItemFunction * property. . If the request * succeeds, the actual item replaces the provisional one. * If it fails, the provisional item is replaced with the item returned by calling * the function specified by the createFailedItemFunction property.

* *

This class delegates the IList methods and properties to its list. * If a list isn't specified, methods that mutate the collection are no-ops, * and methods that query the collection return an empty value, such as null or zero * as appropriate.

* *

This class is intended to be used with Spark components based on DataGroup, * such as List and ComboBox. The Spark classes do not provide intrinsic support for * ItemPendingError handling.

* *

AsyncListView does not support re-insertion of pending or failed items. Once * a failed or pending item is removed, its connection to a pending request for data * is lost. Using drag and drop to move a pending item in an ASyncListView, or sorting * an ASyncListView that contains pending or failed items, is not supported because * these operations remove and then re-insert list items.

* * @mxml * *

The <mx:AsyncListView> tag inherits all the attributes of its * superclass, and adds the following attributes:

* *
 *  <mx:AsyncListView
 *  Properties
 *    createFailedItemFunction="null"
 *    createPendingItemFunction="null"
 *    list="null"
 *  />
 *  
* * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public class AsyncListView extends OnDemandEventDispatcher implements IList { /** * Constructor. * * @param list Initial value of the list property, the IList we're delegating to. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public function AsyncListView(list:IList = null) { super(); this.list = list; } //-------------------------------------------------------------------------- // // Properties // //-------------------------------------------------------------------------- //---------------------------------- // list //---------------------------------- private var _list:IList; [Inspectable(category="General")] [Bindable("listChanged")] /** * The IList object that this collection wraps. That means the object to which all of * the IList methods are delegated. * *

If this property is null, the IList mutation methods, such as setItemAt(), * are no-ops. The IList query methods, such getItemAt(), return null * or zero (-1 for getItemIndex()), as appropriate.

* * @default null * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public function get list():IList { return _list; } /** * @private */ public function set list(value:IList):void { if (_list == value) return; deleteAllPendingResponders(); oldLength = -1; if (_list) _list.removeEventListener(CollectionEvent.COLLECTION_CHANGE, handleCollectionChangeEvent); _list = value; if (_list) { _list.addEventListener(CollectionEvent.COLLECTION_CHANGE, handleCollectionChangeEvent, false, 0, true); oldLength = _list.length; } dispatchEvent(new Event("listChanged")); dispatchEvent(new CollectionEvent(CollectionEvent.COLLECTION_CHANGE, false, false, CollectionEventKind.RESET)); } /** * @private */ private function deleteAllPendingResponders():void { for each (var responder:ListItemResponder in pendingResponders) { if (responder) responder.index = -1; } pendingResponders.length = 0; failedItems.length = 0; } /** * The previous known length of the list before handling a CollectionEvent. * oldLength is updated by the list setter and isValidCollectionEvent(). */ private var oldLength:int = -1; /** * This method checks the validity of incoming CollectionEvents. * In some cases, a CollectionEvent from the underlying list may have already * been received once, or have been erroneously dispatched (See SDK-30594). * Thus, we check the incoming event's location against the last known length' * of the list (oldLength). * *

Returns false if the index is less than 0 or greater than the previous * length of the list. * This only applies to ADD, REMOVE, REPLACE, and MOVE CollectionEvents. * It also updates oldLength to be the current length of the list, so it * should not be called twice.

*/ private function isValidCollectionEvent(ce:CollectionEvent):Boolean { if (oldLength < 0) return true; const location:int = ce.location; switch (ce.kind) { case CollectionEventKind.ADD: { if (location < 0 || location > oldLength) return false; break; } case CollectionEventKind.REMOVE: case CollectionEventKind.REPLACE: case CollectionEventKind.MOVE: { if (location < 0 || location >= oldLength) return false; break; } } oldLength = length; return true; } /** * @private * Fixup the pendingResponders and failedItems arrays after a change to the list. * Generally speaking, if a list[index] item changes, the pending responder for * that index is no longer needed. * * All "collectionChange" events are redispatched to the AsyncListView listeners. */ private function handleCollectionChangeEvent(ce:CollectionEvent):void { if (!isValidCollectionEvent(ce)) return; switch (ce.kind) { case CollectionEventKind.REPLACE: case CollectionEventKind.UPDATE: deletePendingResponders(ce); break; case CollectionEventKind.MOVE: movePendingResponders(ce); break; case CollectionEventKind.ADD: shiftPendingRespondersRight(ce); break; case CollectionEventKind.REMOVE: shiftPendingRespondersLeft(ce); break; case CollectionEventKind.RESET: case CollectionEventKind.REFRESH: deleteAllPendingResponders(); break; } dispatchEvent(ce); // redispatch to CollectionEvent listeners on this } /** * @private * Delete the ListItemResponder at the specified index, if any. * If a pending responder exists, return its item. * * This method assumes that the responder hasn't run yet, it sets * the ListItemResponder index to -1 to prevent it from updating * this AsyncListView later. */ private function deletePendingResponder(index:int):Object { if ((index < 0) || (index >= pendingResponders.length)) return null; const pendingResponder:ListItemResponder = pendingResponders[index]; if (pendingResponder) { delete pendingResponders[index]; ListItemResponder(pendingResponder).index = -1; return pendingResponder.item; } return null; } /** * @private * Handler for a CollectionEventKind.UPDATE or REPLACE event. In either * case a contiguous block of items (ce.items) beginning with index=ce.location * has been changed. If there are any pending requests for these indices, we * assume they're no longer valid, i.e. we assume that getItemAt() should no longer * return the pending item. Likewise for failed items. */ private function deletePendingResponders(ce:CollectionEvent):void { var index:int = ce.location; for each (var item:Object in ce.items) { deletePendingResponder(index); delete failedItems[index]; index += 1; } } /** * @private * Handler for a CollectionEventKind.MOVE event. The event indicates that a * contiguous block of items (ce.items), beginning with index=ce.oldLocation, * has been moved to ce.location. If pendingRequests already exist at ce.location, * they're deleted first. */ private function movePendingResponders(ce:CollectionEvent):void { var fromIndex:int = ce.oldLocation; var toIndex:int = ce.location; for each (var item:Object in ce.items) { var pendingResponder:ListItemResponder = pendingResponders[fromIndex]; if (pendingResponder) { delete pendingResponders[fromIndex]; ListItemResponder(pendingResponder).index = toIndex; deletePendingResponder(toIndex); // in case we're copying over a pending request pendingResponders[toIndex] = pendingResponder; } var failedItem:* = failedItems[fromIndex]; if (failedItem !== undefined) { delete failedItems[fromIndex]; failedItems[toIndex] = failedItem; } fromIndex += 1; toIndex += 1; } } /** * @private * Handler for a CollectionEventKind.ADD. The event indicates * that a block of ce.items.length items starting at ce.location was inserted, * which implies that all of the pendingResponders whose index is greater than or * equal to ce.location, must be shifted right by ce.items.length. The failedItems * array is handled similarly. */ private function shiftPendingRespondersRight(ce:CollectionEvent):void { const delta:int = ce.items.length; const startIndex:int = ce.location; const pendingRespondersCopy:Array = sparseCopy(pendingResponders); pendingResponders.length = 0; for each (var responder:ListItemResponder in pendingRespondersCopy) { if (responder.index >= startIndex) responder.index += delta; pendingResponders[responder.index] = responder; } for (var index:int = failedItems.length - 1; index >= startIndex; index--) { var failedItem:* = failedItems[index]; if (failedItem !== undefined) { delete failedItems[index]; failedItems[index + delta] = failedItem; } } } /** * @private * Handler for a CollectionEventKind.REMOVE. The event indicates * that a block of ce.items.length items starting at ce.location was removed, * which implies that all of the pendingResponders whose index is greater than or * equal to ce.location, must be shifted left by ce.items.length. The failedItems * array is handled similarly. */ private function shiftPendingRespondersLeft(ce:CollectionEvent):void { const delta:int = ce.items.length; const startIndex:int = ce.location + delta; const pendingRespondersCopy:Array = sparseCopy(pendingResponders); pendingResponders.length = 0; for each (var responder:ListItemResponder in pendingRespondersCopy) { if (responder.index >= startIndex) responder.index -= delta; pendingResponders[responder.index] = responder; } const failedItemsLength:int = failedItems.length; for (var index:int = startIndex; index < failedItemsLength; index++) { var failedItem:* = failedItems[index]; if (failedItem !== undefined) { delete failedItems[index]; failedItems[index - delta] = failedItem; } } } /** * Applying concat() to a sparse array produces a new array that's * not sparse, nulls replace items that were undefined. Although the * result of this method is not sparse, it only includes items that * were in the original array. */ private function sparseCopy(a:Array):Array { const r:Array = new Array(); var index:int = 0; for each (var item:* in a) { if (item !== undefined) r[index++] = item; } return r; } //---------------------------------- // createPendingItemFunction //---------------------------------- private var _createPendingItemFunction:Function = defaultCreatePendingItemFunction; /** * @private */ private function defaultCreatePendingItemFunction(index:int, ipe:ItemPendingError):Object { return null; } /** * A callback function used to create a provisional item when * the initial request causes an ItemPendingError to be thrown. * If the request eventually succeeds, the provisional item is automatically * replaced by the actual item. If the request fails, then the item is replaced * with one created with the callback function specified by the * createFailedItemFunction property. * *

The value of this property must be a function with two parameters: the index * of the requested data provider item, and the ItemPendingError itself. In most * cases, the second parameter can be ignored. * The following example shows an implementation of the callback function: * *

     * function createPendingItem(index:int, ipe:ItemPendingError):Object
     * {
     *     return "[" + index + "request is pending...]";        
     * }
     *   
*

* *

Setting this property does not affect provisional pending items that were already * created. Setting this property to null prevents provisional pending items * from being created.

* * @default A function that unconditionally returns null. * @see #getItemAt() * @see #createFailedItemFunction * @see mx.collections.errors.ItemPendingError * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public function get createPendingItemFunction():Function { return _createPendingItemFunction; } /** * @private */ public function set createPendingItemFunction(value:Function):void { _createPendingItemFunction = value; } //---------------------------------- // createFailedItemFunction //---------------------------------- private var _createFailedItemFunction:Function = defaultCreateFailedItemFunction; /** * @private */ private function defaultCreateFailedItemFunction(index:int, info:Object):Object { return null; } /** * A callback function used to create a substitute item when * a request which had caused an ItemPendingError to be thrown, * subsequently fails. The existing item, typically a pending item created * by the callback function specified by the createPendingItemFunction() property, * is replaced with the failed item. * *

The value of this property must be a function with two parameters: the index * of the requested item, and the failure "info" object, which is * passed along from the IResponder fault() method. * In most cases you can ignore the second parameter. * Shown below is an example implementation of the callback function:

* *
     * function createFailedItem(index:int, info:Object):Object
     * {
     *     return "[" + index + "request failed]";        
     * }
     *   
* * *

Setting this property does not affect failed items that were already * created. Setting this property to null prevents failed items from being created. *

* * @default A function that unconditionally returns null. * @see #getItemAt() * @see #createPendingItemFunction * @see mx.rpc.IResponder#fault * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public function get createFailedItemFunction():Function { return _createFailedItemFunction; } /** * @private */ public function set createFailedItemFunction(value:Function):void { _createFailedItemFunction = value; } //-------------------------------------------------------------------------- // // Methods // //-------------------------------------------------------------------------- private const pendingResponders:Array = new Array(); private const failedItems:Array = new Array(); /** * @private * Called by the ListItemProvider/result() method when a pending request * completes successfully. * * @param index The item's index. * @param info The informational object passed to IResponder/result(). * @see mx.rpc.IResponder#result */ mx_internal function pendingRequestSucceeded(index:int, info:Object):void { delete pendingResponders[index]; } /** * @private * Called by the ListItemProvider/fault() method when a pending request * fails. * * @param index The item's index. * @param info The informational object passed to IResponder/fault(). * @see mx.rpc.IResponder#fault */ mx_internal function pendingRequestFailed(index:int, info:Object):void { delete pendingResponders[index]; if (createFailedItemFunction === null) return; const item:Object = createFailedItemFunction(index, info); failedItems[index] = item; // dispatch collection and property change events const hasCollectionListener:Boolean = hasEventListener(CollectionEvent.COLLECTION_CHANGE); const hasPropertyListener:Boolean = hasEventListener(PropertyChangeEvent.PROPERTY_CHANGE); var pce:PropertyChangeEvent; if (hasCollectionListener || hasPropertyListener) { pce = new PropertyChangeEvent(PropertyChangeEvent.PROPERTY_CHANGE); pce.kind = PropertyChangeEventKind.UPDATE; pce.oldValue = null; pce.newValue = item; pce.property = index; } if (hasCollectionListener) { var ce:CollectionEvent = new CollectionEvent(CollectionEvent.COLLECTION_CHANGE); ce.kind = CollectionEventKind.REPLACE; ce.location = index; ce.items.push(pce); dispatchEvent(ce); } if (hasPropertyListener) dispatchEvent(pce); } //-------------------------------------------------------------------------- // // IList Implementation // //-------------------------------------------------------------------------- [Bindable("collectionChange")] /** * @inheritDoc * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public function get length():int { try { return (list) ? list.length : 0; } catch (ignore:ItemPendingError) { // The mx.data DataList class can throw an IPE here. We ignore it because // when the length is determined, a CollectionChanged event will be // be dispatched. See handleCollectionChangeEvent(). } return 0; } /** * @inheritDoc * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public function addItem(item:Object):void { if (list) { try { list.addItem(item); } catch (ignore:ItemPendingError) { // The mx.data DataList class can throw an IPE here. We ignore it because // when the item is actually added, a CollectionChanged event will be // be dispatched. See handleCollectionChangeEvent(). } } } /** * @inheritDoc * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public function addItemAt(item:Object, index:int):void { if (list) { try { list.addItemAt(item, index); } catch (ignore:ItemPendingError) { // The mx.data DataList class can throw an IPE here. We ignore it because // when the item is actually added, a CollectionChanged event will be // be dispatched. See handleCollectionChangeEvent(). } } } /** * Returns the value of list.getItemAt(index). * *

This method catches ItemPendingErrors (IPEs) generated as a consequence of * calling getItemAt(). If an IPE is thrown, an IResponder is added to * the IPE and a provisional "pending" item, created with the * createPendingItemFunction is returned. If the underlying request * eventually succeeds, the pending item is replaced with the real item. If it fails, * the pending item is replaced with a value produced by calling * createFailedItemFunction.

* * @param index The list index from which to retrieve the item. * * @param prefetch An int indicating both the direction * and number of items to fetch during the request if the item is not local. * * @throws RangeError if index < 0 or index >= length. * * @return The list item at the specified index. * * @see #createPendingItemFunction * @see #createFailedItemFunction * @see mx.collections.errors.ItemPendingError * @see mx.rpc.IResponder * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public function getItemAt(index:int, prefetch:int=0):Object { if (!list) return null; const failedItem:* = failedItems[index]; if (failedItem !== undefined) return failedItem; const pendingResponder:ListItemResponder = pendingResponders[index]; if (pendingResponder) return pendingResponder.item; var item:Object = null; try { return list.getItemAt(index, prefetch); } catch (ipe:ItemPendingError) { const createPendingItem:Function = createPendingItemFunction; if (createPendingItem !== null) item = createPendingItem(index, ipe); var responder:ListItemResponder = new ListItemResponder(this, index, item); pendingResponders[index] = responder; ipe.addResponder(responder); } return item; } /** * @inheritDoc * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public function getItemIndex(item:Object):int { const failedItemIndex:int = failedItems.indexOf(item); if (failedItemIndex != -1) return failedItemIndex; for each (var responder:ListItemResponder in pendingResponders) if (responder && responder.item === item) return responder.index; return (list) ? list.getItemIndex(item) : -1; } /** * @inheritDoc * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public function itemUpdated(item:Object, property:Object=null, oldValue:Object=null, newValue:Object=null):void { if (list) list.itemUpdated(item, property, oldValue, newValue); } /** * @inheritDoc * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public function removeAll():void { if (list) list.removeAll(); } /** * Removes the actual or pending item at the specified index and returns it. * All items whose index is greater than the specified index * have their index reduced by 1. * *

If there is no actual or pending item at the specified index, for * example because a call to getItemAt(index) hasn't caused the data to be * paged in, then the underlying list may throw an ItemPendingError. * The implementation ignores the ItemPendingError and returns null.

* * @param index The list index from which to retrieve the item. * * @throws RangeError if index < 0 or index >= length. * * @return The item that was removed or null. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public function removeItemAt(index:int):Object { if (!list) return null; const failedItem:* = failedItems[index]; delete failedItems[index]; const pendingItem:Object = deletePendingResponder(index); try { const actualItem:Object = list.removeItemAt(index); if (failedItem !== undefined) return failedItem; return (pendingItem) ? pendingItem : actualItem; } catch (ipe:ItemPendingError) { // If list[index] doesn't exist yet, an IPE will be thrown. There's nothing // we can do about that, so ignore it. } return (failedItem !== undefined) ? failedItem : pendingItem; } /** * @inheritDoc * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public function setItemAt(item:Object, index:int):Object { if (!list) return null; const failedItem:* = failedItems[index]; const pendingResponder:ListItemResponder = pendingResponders[index]; var setItemValue:Object = null; // return null if IPE try { setItemValue = list.setItemAt(item, index); } catch (ignore:ItemPendingError) { // The mx.data DataList class can throw an IPE here. We ignore it because // when the item is actually changed, a CollectionChanged event will be // be dispatched. See handleCollectionChangeEvent(). } if (failedItem !== undefined) return failedItem; else return (pendingResponder) ? pendingResponder.item : setItemValue; } /** * Returns an array with the same elements as this AsyncListView. The array is initialized * by retrieving each item with getItemAt(), so pending items are substituted where actual * values aren't available yet. The array will not be updated when the AsyncListView replaces * the pending items with actual (or failed) values. * * @return an array with the same elements as this AsyncListView. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public function toArray():Array { if (!list) return []; const a:Array = new Array(list.length); for(var i:int = 0; i < a.length; i++) a[i] = getItemAt(i); return a; } /** * Returns a string that contains the list's length and the number of pending item requests. * It does not trigger pending requests. * * @return A brief description of the list. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public function toString():String { var s:String = getQualifiedClassName(this); if (list) { var nRequests:int = 0; for each (var responder:ListItemResponder in pendingResponders) { if (responder) nRequests += 1; } s += " length=" + length + ", " + nRequests + " pending requests"; } else s += " no list"; return s; } } } import mx.rpc.IResponder; import mx.collections.AsyncListView; import mx.core.mx_internal; use namespace mx_internal; // for mx_internal functions pendingItemSucceeded,Failed() class ListItemResponder implements IResponder { private var asyncListView:AsyncListView; public var index:int = -1; public var item:Object = null; public function ListItemResponder(asyncListView:AsyncListView, index:int, item:Object) { super(); this.asyncListView = asyncListView; this.index = index; this.item = item; } public function result(info:Object):void { if (index != -1) asyncListView.pendingRequestSucceeded(index, info); } public function fault(info:Object):void { if (index != -1) asyncListView.pendingRequestFailed(index, info); } }