Flex에서 ArrayCollection, XMLListCollection은 List 계열 비주얼 컴포넌트(Visual Component)인 Tree, DataGrid등에 구현된 dataProvider() 메소드를 통해 값이 셋팅한다. 굳이 이런 Collection 계열을 dataProvider() 메소드로 받는 이유가 있다. 이 글 목표는 그 이유를 아는 것이다.

먼저 Collection 계열 클래스인 ArrayCollection과 XMLListCollection에 대해서 알아본 다음, List 계열 컴포넌트와의 관계를 살펴보면서 앞서 지적한 이유를 알아보자.


1. Collection 계열 클래스의 구조(ArrayCollection, XMLListCollection 분석)

먼저 Collection 계열 클래스의 구조를 보자. 아래 그림과 같이 ArrayCollectionXMLListCollectionListCollectionView를 확장해서 만들었다. 그리고 ListCollectionViewIEventDispatcher를 확장한 ICollectionIList, 그리고 IMXMLObject 등의 인터페이스를 구현했다.
 
사용자 삽입 이미지

Collection 계열 클래스의 구조


ICollection은 데이터의 정렬 및 필터링을 담당하고 IList는 데이터 추가,삭제,수정 기능 담당한다. 이 부분에 대한 설명은 검쉰님의 "ArrayCollection에 대한 이해" 를 보시면 충분히 공부할 수 있을 것이다.

여기서 주로 다루고 싶은 내용은 Collection계열의 클래스가 IEventDispatcher를 구현했다는 것이다. 이것이 뜻하는 것은 Collection계열의 데이터가 변경되면 이벤트를 송출해준다는 것을 의미한다. 데이터 변경시 CollectionEvent를 COLLECTION_CHANGE라는 type으로 이벤트를 송출한다.

public function set list(value:IList):void

{

    if (_list != value)

    {

        var oldHasItems:Boolean;

        var newHasItems:Boolean;

        if (_list)

        {

            _list.removeEventListener(CollectionEvent.COLLECTION_CHANGE,

                                      listChangeHandler);

            oldHasItems = _list.length > 0;

        }

 

        _list = value;

 

        if (_list)

        {

            // weak listeners to collections and dataproviders

            _list.addEventListener(CollectionEvent.COLLECTION_CHANGE,

                                  listChangeHandler, false, 0, true);

            newHasItems = _list.length > 0;

        }

 

        if (oldHasItems || newHasItems)

            reset();

        dispatchEvent(new Event("listChanged"));

    }

}



위 메소드는 ListCollectionView의 setter메소드인 list의 구현부다. IList 인터페이스를 구현한 데이터가 들어오면 _list에 값을 참고하고 그 데이터가 변경될때 listChangeHandler 메소드를 호출한다.

ArrayCollection이나 XMLListCollection을 사용하신 분은 알겠지만 둘다 source getter/setter 메소드가 정의되어 있다.  ArrayCollection은 source로 Array를 받고 XMLListCollection은 XMLList를 받는다.

아래는 ArrayCollection의 source setter이다.

public function set source(s:Array):void

{

    list = new ArrayList(s);

}

다음 아래는 XMLListCollection의 source setter이다.

public function set source(s:XMLList):void

{

    if (list)

        XMLListAdapter(list).source = null;

   

    list = new XMLListAdapter(s);

}

ArrayCollection과 XMLListCollection은 둘다 ListCollectionView 내부에 구현한 list setter 메소드에 IList를 구현한 ArrayList와 XMLListAdapter를 생성하여 대입하고 있다.

사용자 삽입 이미지

ArrayList와 XMLListAdapter 구조



위 그림처럼 ArrayList와 XMLListAdapter는 IList를 구현했기 때문에 내부적으로 addItem(), addItemAt()등이 구현되어 있고 IList는 IEventDispather를 확장함에 따라 addItem(), removeItem()등과 같은 메소드를 통해 데이터가 변경되면CollectionEvent를 송출하게 된다.

대입절차를 살펴보자면 Array값이 ArrayCollection의 source()에서 ArrayList값으로 변경되어 ListCollectionView의 setter list()를 통해_list:IList값으로 등록된다. 거의 같은 방법으로 XMLList는 XMLListCollection의 source()에서 XMLListAdapter값으로 변경되어 ListCollectionview의 setter list()를 통해 _list:IList값으로 등록된다.

이해를 돕기 위해 ArrayList의 구조를 조금 보자. IList를 구현한 ArrayList는 아래와 같이 addItemAt() 메소드를 구현했다.

public function addItemAt(item:Object, index:int):void

{

    if (index < 0 || index > length)

        {

               var message:String = resourceManager.getString(

                       "collections", "outOfBounds", [ index ]);

        throw new RangeError(message);

        }

       

    source.splice(index, 0, item);

 

    startTrackUpdates(item);

    internalDispatchEvent(CollectionEventKind.ADD, item, index);

}


위 함수는 데이터를 원하는 위치에 추가할때 사용하는 ArrayList의 메소드이다. 데이터를 추가하게 되면 아래에 있는 internalDispatchEvent()를 호출한다.

private function internalDispatchEvent(kind:String, item:Object = null, location:int = -1):void

{

        if (_dispatchEvents == 0)

        {

               if (hasEventListener(CollectionEvent.COLLECTION_CHANGE))

               {

                var event:CollectionEvent =

                new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);

                event.kind = kind;

                event.items.push(item);

                event.location = location;

                dispatchEvent(event);

            }

        …(생략)

}

}

이로써 IList를 구현한 ArrayList나 XMLListAdapter는 데이터가 추가, 수정, 삭제, 변경등이 일어날때 CollectionEvent를 송출하는 것을 확인할 수 있다.

결국 ListCollectionView의 list setter 메소드로부터 IList를 구현한 ArrayList와 XMLListAdapter의 값이 _list에 대입되고 _list.addEventHandler()를 구현함으로써 _list의 데이터가 변경되면 자동적으로 CollectionEvent가 송출된다.

정말 그런지 살펴보자. IList를 구현한 ListCollectionView내부에는 setItem(), addItem(), addItemAt() 등의 메소드가 정의되어 있다. 이들 함수로부터 _list:IList의 _list.setItem(), _list.addItem(), _list.addItemAt()이 호출된다. 그러므로 ArrayList나 XMLListAdapter의 setItem(), addItem(), addItemAt()을 호출하는 것과 동일하다. 결국 ListCollectionView의 setter인 list 메소드안에 _list.addEventHandler()로 등록한 핸들러 메소드 listChangeHandler()를 호출하게 된다. 다음 코드는 listChangedHandler() 핸들러 메소드이다.

private function listChangeHandler(event:CollectionEvent):void

{

    if (autoUpdateCounter > 0)

    {

        if (!pendingUpdates)

        {

            pendingUpdates = [];

        }

        pendingUpdates.push(event);

    }

    else

    {

        switch (event.kind)

        {

            case CollectionEventKind.ADD:

                addItemsToView(event.items, event.location);

            break;

 

            case CollectionEventKind.RESET:

                reset();

            break;

 

            case CollectionEventKind.REMOVE:

                removeItemsFromView(event.items, event.location);

            break;

 

            case CollectionEventKind.REPLACE:

                 replaceItemsInView(event.items, event.location);

            break;

 

            case CollectionEventKind.UPDATE:

                 handlePropertyChangeEvents(event.items);

            break;

 

            default:

                dispatchEvent(event);

        } // switch

    }

}


위 이벤트 핸들러는 ArrayList또는 XMLListAdapter의 데이터가 변경될 때 발생하는 CollectionEvent를 처리하는 메소드이다. 어떤 종류의 데이터 처리를 했느냐에 따라 다음 처리를 위해 switch문에서 이벤트 kind값을 참고하여 관련 메소드를 호출한다. 예를 들어 데이터를 추가한 경우를 살펴보자. 그럼 CollectionEventKind.ADD 부분에서 다음과 같은 addItemsToView() 메소드가 호출된다.

private function addItemsToView(items:Array, sourceLocation:int,

                                  dispatch:Boolean = true):int

{

        …(생략)

    if (dispatch && addedItems.length > 0)

    {

        var event:CollectionEvent =

            new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);

        event.kind = CollectionEventKind.ADD;

        event.location = addLocation;

        event.items = addedItems;

        dispatchEvent(event);

    }

        …(생략)

}

위와 같이 데이터가 추가되면 CollectionEventKind.ADD를 이벤트 kind로 하여 CollectionEvent가 송출되는 것을 확인할 수 있다. 또한 event.location, event.items에 의해 데이터가 변동된 위치와 어떤 데이터가 변동되었는지도 알 수 있다.

결국 ArrayCollection, XMLListCollection의 addItem(), addItemAt(), setItemAt()등을 호출하면 변경된 데이터에 대한 이벤트가 송출되고 데이터 변경에 따른 다른 로직 구현을 마련할 수 있게된다.


이것만은 꼭 알아두자!
Array와 XMLList은 데이터일 뿐이다. 데이터 변동이 있더라도 외부에 알릴 수 있는 통로가 없다. 그의 단점을 보완한것이 ArrayList와 XMLListAdapter이다. 이는 IList를 구현했으며 원하는 위치에 데이터를 바꿀 수 있고 그에 따라 이벤트를 송출할 수 있다. ArrayCollection과 XMLListCollection은 IList, ICollection등을 구현해서 IList기능 뿐이 아니라 ICollection의 데이터 정렬/필터링 기능을 가진다.



2. List 계열 컴포넌트의 구조

Flex의 List 계열의 컴포넌트들은 데이터를 리스트화하여 화면에 보여주기 위한 비주얼 컴포넌트(visual component)이다. Collection 계열의 클래스들이 데이터를 처리하기 위한 것이라면 List 계열 컴포넌트의 가장 큰 목적은 Collection 계열의 클래스의 데이터를 가시화하는 것이다. 가시화를 위해 List계열 컴포넌트는 UIComponent를 기반으로 만들어 졌으며 이들의 핵심 클래스는 ListBase로써 이를 확장하여 목적에 맞게 제작되었다. 자주 사용하는 List, DataGrid가 모두 ListBase를 확장해서 만들어진 것이다. 아래 그림에서 그 관계를 알 수 있다.

사용자 삽입 이미지

List 계열 컴포넌트의 구조



그러므로 List 계열 컴포넌트는 ListBase를 이해하는 것이 순서이다.

여기서 언급할 것은 앞서 설명한 Collection계열의 클래스와 ListBase의 관계이다.

Collection 계열인 ArrayCollection의 인스턴스값이 ListBase의 dataProvider() 메소드에 인자로 넘어가게 되면 그때부터는 ArrayCollection의 데이터가 변경에 대응하여 ListBase에 변경된 데이터를 가시화한다. 여기서 중요하게 생각할 것은 ListBase의 데이터를 수정하는 것이 아니라 ArrayCollection의 데이터가 수정된다는 점과 ArrayCollection 데이터가 수정되면 자동으로 ListBase에 알려준다는 것이다. ListBase의 dataProvider() 메소드는 이런 관계를 만들어주는 역할을 하는 것이다.


그럼, 상세한 이해를 위해 ListBase를 분석해보겠다.
위 그림에서 ListBase는 ICollectionView 형태의 collection이라는 이름의 변수를 사용하고 있다. 아래 코드를 보면 확인할 수 있겠다.

public class ListBase extends ScrollControlBase

                      implements IDataRenderer, IFocusManagerComponent,

                      IListItemRenderer, IDropInListItemRenderer,

                      IEffectTargetHost

{

        …(생략)

 

        protected var collection:ICollectionView;

 

 

        …(생략)

 

}


ListBase의 collection 변수가 protected인 것에 주목하자. ListBase를 확장하는 모든 클래스(List, DataGrid등)은 이 변수를 사용할 수 있음을 알 수 있다.

ListBase의 dataProvider()에서 이 collection 변수를 설정한다. 아래 코드를 보자.

public function set dataProvider(value:Object):void

    {

        if (collection)

        {

            collection.removeEventListener(CollectionEvent.COLLECTION_CHANGE, collectionChangeHandler);

        }

 

        if (value is Array)

        {

            collection = new ArrayCollection(value as Array);

        }

        else if (value is ICollectionView)

        {

            collection = ICollectionView(value);

        }

        else if (value is IList)

        {

            collection = new ListCollectionView(IList(value));

        }

        else if (value is XMLList)

        {

            collection = new XMLListCollection(value as XMLList);

        }

        else if (value is XML)

        {

            var xl:XMLList = new XMLList();

            xl += value;

            collection = new XMLListCollection(xl);

        }

        else

        {

            // convert it to an array containing this one item

            var tmp:Array = [];

            if (value != null)

                tmp.push(value);

            collection = new ArrayCollection(tmp);

        }

 

        …(생략)

 

collection.addEventListener(CollectionEvent.COLLECTION_CHANGE, collectionChangeHandler, false, 0, true);

 

…(생략)

 

}



value는 Object가 들어오지만 어떤 형태의 값을 가지냐에 따라서 그에 맞게 대응하여 ICollectionView 계열 클래스 형태로 바뀌는 것을 볼 수 있다. 어떤 값이 들어오든지 ICollectionList 형태로 데이터가 만들어지도록 구현된 것에 주목하길 바란다. value가 ArrayCollection 형태로 들어온다면  "else if (value is ICollectionView)" 조건에 의해  collection = ICollectionView(value) 로 대입된다.

만약 value가 ArrayList나 XMLListAdapter라면 이 클래스는 IList를 구현했기 때문에 "else if (value is IList)" 조건에서 collection = new ListCollectionView(IList(value)); 로 대입되는 것을 볼 수 있다. 참고로 ArrayList와 XMLListAdapter는 데이터가 변경되면 CollectionEvent를 송출한다고 했다. 하지만 이들 클래스는 ICollectionList를 구현하지 않아 정렬/필터링 기능이 없다. 이 기능이 필요 없을때만 사용하자.


언급하고 싶은 중요한 부분은 그 다음이다.
dataProvider()의 아래부분에 collection.addEventListener(CollectionEvent.COLLECTION_CHANGE, collectionChangeHandler) 부분이 있다. 즉, ICollection 형태의 데이터가 변경되면 collectionChangedHandler 메소드에서 변경된 데이터를 반영하여 가시화 시키겠다는 것을 유추할 수 있다.

그럼 데이터가 변경될 때 호출되는 collectionChangedHandler() 메소드가 어떻게 구현되었나 보도록 하자.

protected function collectionChangeHandler(event:Event):void

{

    var len:int;

    var index:int;

    var i:int;

    var data:ListBaseSelectionData;

    var p:String;

    var selectedUID:String;

 

    if (event is CollectionEvent)

    {

        var ce:CollectionEvent = CollectionEvent(event);

 

if (ce.kind == CollectionEventKind.ADD)

        {

            prepareDataEffect(ce);                       

// special case when we have less than a screen full of stuff

            if