Idealism is what precedes experience; cynicism is what follows...

Saturday, July 19, 2008

Flex : Pageable ArrayCollection with support for active paging

Yesterday one of the developers in my company had the need for a PagedArrayCollection. A quick search on Google revealed only this implementation, which turned out to be buggy so I decided to implement one myself.

I designed an interface called IPagedCollection which in combination with an extension of the existing mx.collections.ArrayCollection implementation would do the job.

Only hurdle was the need to override addItemAt and removeItemAt as the autoUpdate doesn't seem to work when a filterFunction is employed. I will look in to this phenomena, but as for now a call to refresh after inserting or removing does the job nicely.

The code itself is quite simple, I have also created a small demo-application which illustrates the use of the collection.

Live Demo

Download of Sources

/**
* Copyright(c) 2008 HelloGroup A/S, some rights reserved.
* Your reuse is governed by the Creative Commons Attribution 3.0 Denmark License
**/
package com.hello.collections
{
public interface IPagedArrayCollection
{
function get currentPage() : Number
function set currentPage( value:Number ) : void;

function get numberOfPages() : Number;

function get pageSize() : Number;
function set pageSize( value:Number ) : void;

function get lengthTotal() : Number;
}
}



/**
* Copyright(c) 2008 HelloGroup A/S, some rights reserved.
* Your reuse is governed by the Creative Commons Attribution 3.0 Denmark License
*
* Known Issues:
* - When the collection changes in size or pagesize, the currentPage is not updated. This is a problem if currentPage is set to a higher value than in the new collection.
**/
package com.hello.collections
{
import mx.collections.ArrayCollection;
import mx.events.CollectionEvent;

public class PagedArrayCollection extends ArrayCollection implements IPagedArrayCollection
{
private var _currentPage:Number = 1;
private var _numberOfPages:Number = 1;
private var _pageSize:Number = 10;

public function PagedArrayCollection(source:Array=null)
{
super( source );
filterFunction = filterData;
addEventListener( CollectionEvent.COLLECTION_CHANGE, onChange );
}

/**
* Adds an item to the collection at the specified index.
*
* @param item Item to be added
* @param index Index of the item to be added
*
* Note: Needs to be overridden in order to trigger refresh. AddItem eventually calls this function so its not needed to override addItem
*/
override public function addItemAt( item:Object, index:int ) : void
{
super.addItemAt( item, index );
refresh();
}

/**
* Removes the item from the collection at the specified index
*
* @param index Index of the item to be removed
* @return The item removed
*
* Note: Needs to be overridden in order to trigger refresh
*/
override public function removeItemAt( index:int ) : Object
{
var removedItem:Object = super.removeItemAt( index );
refresh();
return removedItem;
}

protected function onChange( event:CollectionEvent ) : void
{
if( _numberOfPages != numberOfPages )
{
_numberOfPages = numberOfPages;
onPagingChange( PagedCollectionEventKind.NUMBEROFPAGES_CHANGE );
}
}

protected function onPagingChange( kind:String ) : void
{
dispatchEvent( new CollectionEvent( CollectionEvent.COLLECTION_CHANGE, false, false, kind ) );
}

[ChangeEvent("collectionChange")]
public function get currentPage() : Number
{
return _currentPage;
}
public function set currentPage( value:Number ) : void
{
_currentPage = value;
refresh();
onPagingChange( PagedCollectionEventKind.CURRENTPAGE_CHANGE );
}

[ChangeEvent("collectionChange")]
public function get numberOfPages() : Number
{
var result:Number = source.length / pageSize;
result = Math.ceil( result );
return result;
}

[ChangeEvent("collectionChange")]
public function get pageSize() : Number
{
return _pageSize;
}
public function set pageSize( value:Number ) : void
{
_pageSize = value;
refresh();
onPagingChange( PagedCollectionEventKind.PAGESIZE_CHANGE );
}

[ChangeEvent("collectionChange")]
public function get lengthTotal() : Number
{
return source.length;
}

private function filterData( item:Object ) : Boolean
{
var dataWindowCeiling:Number = pageSize * currentPage;
var dataWindowFloor:Number = dataWindowCeiling - pageSize;

var itemIndex:Number = getItemIndex( item );

var result:Boolean = dataWindowFloor <= itemIndex && itemIndex < dataWindowCeiling;
return result;
}
}
}


/**
* Copyright(c) 2008 HelloGroup A/S, some rights reserved.
* Your reuse is governed by the Creative Commons Attribution 3.0 Denmark License
**/
package com.hello.collections
{
public class PagedCollectionEventKind
{
public static const CURRENTPAGE_CHANGE:String = "currentPageChange";
public static const PAGESIZE_CHANGE:String = "pageSizeChange";
public static const NUMBEROFPAGES_CHANGE:String = "numberOfPagesChange";
}
}


This is the small demo-application which illustrates and validates that the collection works.

<?xml version="1.0" encoding="utf-8"?>
<mx:Application
xmlns:mx="http://www.adobe.com/2006/mxml"
layout="vertical"
initialize="onInitialize(event)"
creationComplete="onCreationComplete(event)">

<mx:HBox width="100%">
<mx:Label text="Key:" />
<mx:TextInput id="keyInput" text="NEW KEY" />
<mx:Label text="Value:" />
<mx:TextInput id="valueInput" text="NEW VALUE" />
<mx:Button id="addButton" label="Add Item" />
<mx:Button id="removeButton" label="Remove Item" enabled="{ grid.selectedItem != null }" />
<mx:Label text="Set PageSize:" />
<mx:ComboBox id="pageSizeSelector" dataProvider="{ [ 5, 10, 25] }" selectedIndex="1" change="{ collection.pageSize = Number( pageSizeSelector.value ) }" />
</mx:HBox>
<mx:DataGrid id="grid" dataProvider="{ collection }" width="100%" height="100%" />
<mx:HBox>
<mx:Label text="Count: { collection.length } ({ collection.lengthTotal })" />
<mx:Label text="( { collection.currentPage }/{ collection.numberOfPages } )" />
<mx:Label text="PageSize: { collection.pageSize }" />
<mx:Button label="Previous" enabled="{ collection.currentPage > 1 }" click="{ collection.currentPage-- }" />
<mx:Button label="Next" enabled="{ collection.numberOfPages > collection.currentPage }" click="{ collection.currentPage++ }" />
</mx:HBox>

<mx:Script>
<![CDATA[
import mx.events.CollectionEvent;
import mx.events.FlexEvent;
import com.hello.collections.PagedArrayCollection;

[Bindable]
private var collection:PagedArrayCollection;

private function onInitialize( event:FlexEvent ) : void
{
var collection:Array = new Array();

for( var i:Number = 1; i <= 20; i++ )
{
collection.push( { key:"Item_Key_"+ i, value:"Item_Value_"+ i } );
}

this.collection = new PagedArrayCollection( collection );
this.collection.addEventListener( CollectionEvent.COLLECTION_CHANGE, onItemsChange );
this.collection.refresh();
}

private function onCreationComplete( event:FlexEvent ) : void
{
addButton.addEventListener( MouseEvent.CLICK, addButton_Click );
removeButton.addEventListener( MouseEvent.CLICK, removeButton_Click );
}

private function addButton_Click( event:MouseEvent ) : void
{
collection.addItem( { key:keyInput.text, value:valueInput.text } );
}

private function removeButton_Click( event:MouseEvent ) : void
{
collection.removeItemAt( collection.getItemIndex( grid.selectedItem ) );
}

private function onItemsChange( event:CollectionEvent ) : void
{
trace( event.kind +" collectionchanged" );
}

]]>
</mx:Script>

</mx:Application>

3 comments:

Anonymous said...

This is a great class, thanks for sharing - I'm noticing that it's having a hard time when you filter by headers or by a .sort call.

Do you have any insight into why this might be?

Thanks again for sharing your work.

Anonymous said...

I was wondering.... when using this collection, and using it as a data source for say something like a datagrid, does it load all of the data up front, or on demand?

Peter Andreas Molgaard said...

Thanks for your comment.

The collection doesn't actually handle loading of the data, in regards to getting data its just as simple and "stupid" as a normal arraycollection. I think however its an interesting point so I will look into it and create a posting about it in the near future.

Blog Archive

My Network