Search code examples
apache-flexflex4arraylistflex4.5flex4.6

ArayList as dataProvider for a List: The index 0 is out of range 0


Does anybody please have an idea, why do I get the runtime error:

RangeError: Error #1125: The index 0 is out of range 0.
    ........
    at Popup/update()[Popup.mxml:80]
    at PopupTest/showPopup()[PopupTest.mxml:45]
    at PopupTest/___btn_click()[PopupTest.mxml:52]

when calling the function:

private function showPopup(event:MouseEvent):void {
    _popup.update(new Array('Pass' , 
        '6♠', '6♣', '6♦', '6♥', '6 x', 
        '7♠', '7♣', '7♦', '7♥', '7 x', 
        '8♠', '8♣', '8♦', '8♥', '8 x', 
        '9♠', '9♣', '9♦', '9♥', '9 x', 
        '10♠', '10♣', '10♦', '10♥', '10 x'), true, 80);
}

As if my _list would have no entries at all (but why? I do assign _data.source=args) and thus the _list.ensureIndexIsVisible(0) call would fail at the line 80:

<?xml version="1.0" encoding="utf-8"?>
<s:Panel xmlns:fx="http://ns.adobe.com/mxml/2009" 
    xmlns:s="library://ns.adobe.com/flex/spark" 
    xmlns:mx="library://ns.adobe.com/flex/mx"
    width="220" height="200"
    initialize="init(event)">   

    <fx:Script>
        <![CDATA[
            import mx.collections.ArrayList;
            import mx.events.FlexEvent;
            import mx.utils.ObjectUtil;

            private static const FORCE:uint = 20;

            [Bindable]
            private var _data:ArrayList = new ArrayList();

            private var _timer:Timer = new Timer(1000, 120);

            private function init(event:FlexEvent):void {
                _timer.addEventListener(TimerEvent.TIMER, timerUpdated);
                _timer.addEventListener(TimerEvent.TIMER_COMPLETE, timerCompleted);
            }

            public function close():void {
                _timer.reset();
                _data.source = null;
                visible = false;
            }

            private function timerUpdated(event:TimerEvent=null):void {
                var seconds:int = _timer.repeatCount - _timer.currentCount;
                title = 'Your turn! (' + seconds + ')';
                // show panel for cards too
                if (seconds < FORCE)
                    visible = true;
            }

            private function timerCompleted(event:TimerEvent=null):void {
                title = 'Your turn!';
                close();
            }

            public function update(args:Array, bidding:Boolean, seconds:int):void {
                if (seconds <= 0) {
                    close();
                    return;
                }

                // nothing has changed
                if (ObjectUtil.compare(_data.source, args, 0) == 0)
                    return;
                _data.source = args;

                if (args == null || args.length == 0) {
                    close();
                    return;
                }

                if (seconds < FORCE || bidding)
                    visible = true;

                _timer.reset();

                title = 'Your turn! (' + seconds + ')';
                _list.ensureIndexIsVisible(0); // the line 80
                _timer.repeatCount = seconds;
                _timer.start();
            }
        ]]>
    </fx:Script>

    <s:VGroup paddingLeft="10" paddingTop="10" paddingRight="10" paddingBottom="10" gap="10" width="100%" height="100%">
        <s:List id="_list" dataProvider="{_data}" width="100%" height="100%" fontSize="24" itemRenderer="RedBlack" />
    </s:VGroup>
</s:Panel>

Solution

  • the reason

    You are adding the new array allright, but then the List starts creating ItemRenderers based on the items that are in that array. This takes some time and happens asynchronously. In the meantime you're saying "show me item 1", but the ItemRenderer for item 1 doesn't exist yet. It will very soon, but not right now. That's why you get an indexoutofrange error.

    the solution

    You have to be sure the List is done creating ItemRenderers before you call that method. The easiest way to solve this situation - though definitely not the cleanest - is to just wait until the next render cycle by using the infamous callLater().

    callLater(_list.ensureIndexIsVisible, [0]);
    

    This essentially saying: wait for the next render cycle and then call ensureIndexIsVisible() on _list with parameter 0.

    (On a side note: if you really only want index 0 this whole thing is rather pointless, because I think a List scrolls back to the top when its dataprovider is changed anyway)

    a cleaner solution

    You can listen on the List for the RendererExistenceEvent#RENDERER_ADD event. This will be dispatched whenever a new ItemRenderer was added to the list and it holds a reference to the item's index in the List, the data and the ItemRenderer itself. However in your case we only need the 'index'. Whenever an ItemRenderer is added at index 0 we'll scroll back to the top:

    _list.addEventListener(RendererExistenceEvent.RENDERER_ADD, onRendererAdded);
    
    private function onRendererAdded(event:RendererExistenceEvent):void {
        if (event.index == 0) myList.ensureIndexIsVisible(0);
    }
    

    This will immediately scroll to the top when the first ItemRenderer is added and doesn't need to wait until all of them are ready.