Avg. Rating 5.0

Problem

You want to create a container that holds multiple child containers that are lazily instantiated upon request.

Solution

Create a custom GroupBase-based container and assign an Array-based property to the [DefaultProperty] metatag for the container that represents the declared MXML children. Expose selectedIndex and selectedChild properties to represent the currently displayed child container, and override the protected commitProperties() method to add the appropriate child to the display list of the view stack.

Detailed explanation

The Spark container set does not provide equal parity to the navigational containers in the MX container set (such as Accordion and ViewStack). You can create Spark equivalents to these MX navigational containers, however, using the content API, as well as state management and the new skinning capabilities of the Spark architecture.

The ViewStack container from the MX component set acts as a navigation container for multiple child containers within a single display. As the selected container is changed, the current container is removed from the display list of the ViewStack and replaced with the requested container. Optionally, child containers can be lazily created using what is referred to as deferred instantiation. Although the Spark container set does not offer such a container, you can create a similar one, as shown in the following example:

package com.oreilly.f4cb

{

    import mx.core.IVisualElement;


    import spark.components.BorderContainer;

    import spark.events.IndexChangeEvent;


    [Event(name="change", type="spark.events.IndexChangeEvent")]


    [DefaultProperty("content")]

    public class CustomViewStack extends BorderContainer

    {

        [ArrayElementType("mx.core.IVisualElement")]

        protected var _content:Array;

        protected var _selectedIndex:int = −1;

        protected var _selectedChild:IVisualElement

        protected var _pendingSelectedIndex:int = −1;


        override protected function commitProperties() : void

        {

            super.commitProperties();

            // if pending change to selectedIndex property

            if( _pendingSelectedIndex != −1 )

            {

                // commit the change

                updateSelectedIndex( _pendingSelectedIndex );

                // set pending back to default

                _pendingSelectedIndex = −1;

            }

        }


        protected function updateSelectedIndex( index:int ):void

        {

            // store old for event

            var oldIndex:int = _selectedIndex;

            // set new

            _selectedIndex = index;


            // remove old element

            if( numElements > 0 )

                removeElementAt( 0 );


            // add new element

            selectedChild = _content[_selectedIndex];

            addElement( _selectedChild );


            // dispatch index change

            var event:IndexChangeEvent = new IndexChangeEvent(

                                         IndexChangeEvent.CHANGE,

                                         false, false,

                                         oldIndex, _selectedIndex );

            dispatchEvent( event );

        }


        private function getElementIndexFromContent( element:IVisualElement ):int

        {

            if( _content == null ) return −1;


            var i:int = _content.length;

            var contentElement:IVisualElement;

            while( --i > −1 )

            {

                contentElement = _content[i] as IVisualElement;

                if( contentElement == element )

                {

                    break;

                }

            }

            return i;

        }


        [Bindable]

        [ArrayElementType("mx.core.IVisualElement")]

        public function get content():Array /*IVisualElement*/

        {

            return _content;

        }

        public function set content( value:Array /*IVisualElement*/ ):void

        {

            _content = value;

            // update selected index based on pending operations

            selectedIndex = _pendingSelectedIndex == −1 ? 0 :

                            _pendingSelectedIndex;

        }


        [Bindable]

        public function get selectedIndex():int

        {

            return selectedIndex = _pendingIndex == -1"

                         "? 0"

                         ": _pendingIndex

        }

        public function set selectedIndex( value:int ):void

        {

            if( _selectedIndex == value ) return;


            _pendingSelectedIndex = value;

            invalidateProperties();

        }


        [Bindable]

        public function get selectedChild():IVisualElement

        {

            return _selectedChild;

        }

        public function set selectedChild( value:IVisualElement ):void

        {

            if( _selectedChild == value ) return;


            // if not pending operation on selectedIndex, induce

            if( _pendingSelectedIndex == −1 )

            {

                var proposedIndex:int = getElementIndexFromContent( value );

                selectedIndex = proposedIndex;

            }            // else just hold a reference for binding update

            else _selectedChild = value;

        }

    }

}

The content property of the CustomViewStack in this example is an array of IVisualElement-based objects and is declared as the [DefaultProperty] value for the class. Consequently, any child elements declared within the MXML markup for a CustomViewStack instance are considered elements of the array, and the view stack manages how those child elements are instantiated.

The selectedIndex and selectedChild properties are publicly exposed to represent the requested child to display within the custom view stack. Lazy creation of the child containers is accomplished by deferring instantiation of children to the first request to add a child to the display list using the addElement() method of the content API.

The CustomViewStack container can be added to an application in MXML markup just like any other container, as long as the namespace for the package in which it resides is defined:

<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"

               xmlns:s="library://ns.adobe.com/flex/spark"

               xmlns:mx="library://ns.adobe.com/flex/mx"

               xmlns:f4cb="com.oreilly.f4cb.*">


    <fx:Declarations>

        <fx:String id="lorem">

            Lorem ipsum dolor sit amet consectetur adipisicing elit.

        </fx:String>

    </fx:Declarations>


    <fx:Script>

        <![CDATA[

            private function changeIndex():void

            {

                var index:int = viewstack.selectedIndex;

                index = ( index + 1 > viewstack.content.length - 1 )

                          ? 0 :

                          index + 1;

                viewstack.selectedIndex = index;

            }

        ]]>

    </fx:Script>


    <s:layout>

        <s:VerticalLayout />

    </s:layout>


    <f4cb:CustomViewStack id="viewstack" width="300" height="300"

                          skinClass="com.oreilly.f4cb.CustomBorderSkin">

        <s:Group id="child1"

                 width="800" height="100%"

                 clipAndEnableScrolling="true">

            <s:layout>

                <s:VerticalLayout horizontalAlign="justify" />

            </s:layout>

            <s:Button label="top" />

            <s:Button label="bottom" bottom="0" />

        </s:Group>

        <s:Panel id="child2"

                 width="100%" height="200"

                 title="Child 2">

            <s:Scroller>

                <s:Group width="100%" height="100%">

                    <s:layout>

                        <s:VerticalLayout horizontalAlign="center" />

                    </s:layout>

                    <s:Button label="panel button 1" />

                    <s:Button label="panel button 2" />

                </s:Group>

            </s:Scroller>

        </s:Panel>

        <s:DataGroup id="child3"

                     width="100%" height="100%"

                     itemRenderer="spark.skins.spark.DefaultItemRenderer">

            <s:layout>

                <s:VerticalLayout />

            </s:layout>

            <s:dataProvider>

                <s:ArrayCollection source="{lorem.split(' ')}" />

            </s:dataProvider>

        </s:DataGroup>

    </f4cb:CustomViewStack>


    <s:Button label="switch index" click="changeIndex();" />


    <s:HGroup>

        <s:Button label="select child 1"

                  enabled="{viewstack.selectedChild != child1}"

                  click="{viewstack.selectedChild = child1}" />

        <s:Button label="select child 2"

                  enabled="{viewstack.selectedChild != child2}"

                  click="{viewstack.selectedChild = child2}" />

        <s:Button label="select child 3"

                  enabled="{viewstack.selectedChild != child3}"

                  click="{viewstack.selectedChild = child3}" />

    </s:HGroup>


</s:Application>

Children of the CustomViewStack are declared in markup, but they are added to the defined [DefaultProperty] metatag and are not initially added to the display list of the view stack. Instead, it is deferred to the container to create children as they are requested using the selectedIndex and selectedChild properties. The selectedIndex and selectedChild properties are bindable and allow for visual and functional updates to the s:Button controls in the Application container for this example.

To enable scrolling within the view stack, a custom skin is applied that fulfills the contract for a BorderContainer-based container. A Group container with a reference id of contentGroup is declared and wrapped within a Scroller component, as in the following example:

<s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009"

        xmlns:s="library://ns.adobe.com/flex/spark">


    <fx:Metadata>

        <![CDATA[

            [HostComponent("spark.components.BorderContainer")]

        ]]>

    </fx:Metadata>


    <s:states>

        <s:State name="normal" />

        <s:State name="disabled" />

    </s:states>


    <s:Rect width="100%" height="100%">

        <s:stroke>

            <s:SolidColorStroke color="#000000" />

        </s:stroke>

        <s:fill>

            <s:SolidColor color="#FFFFFF" />

        </s:fill>

    </s:Rect>


    <s:Scroller width="100%" height="100%"

                left="2" right="2" top="2" bottom="2">

        <s:Group id="contentGroup"

                 left="0" right="0" top="0" bottom="0"

                 minWidth="0" minHeight="0" />

    </s:Scroller>


</s:Skin>

This example demonstrates a technique for accomplishing deferred instantiation of child elements of a Spark-based navigation container that can be applied to creating equivalents of navigation containers from the MX set within the Flex 4 SDK.

This recipe was originally contributed by  Todd Anderson as part of O'Reilly's Flex 4 Cookbook.


+
This work is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 Unported License. Permissions beyond the scope of this license, pertaining to the examples of code included within this work are available at Adobe.

Report abuse

Related recipes