Sunday, January 21, 2007

New version of RemoteList (IList implementation that support paging)

This is another post about implementation of remote list for Adobe Flex 2.

As I mentioned in previous post Flex framework provides ItemPendingError exception mechanism to use when collection data is not available. For example DataGrid component provides support for this exception. I didn't look too deeply in DataGrid code but my understanding that if ItemPendingError is thrown DataGrid would retry load the data by providing hooks into the ItemPendingError. I believe that all List based components should behave similary.

Given all these built-in support I realized it's much better to rely on ItemPendingError mechanism then event notification as I did in my previous post.

New version of RemoteList is based on old one with one exception - instead of dispatching CollectionEventKind.REPLACE to notifiy view (DataGrid), I throw ItemPendingException.

There are 3 main pieces:
IRemoteDataSet - interface that defines how to get remote data (RemoteDataSet a sample implementation)
RemoteList - class that uses IRemoteDataSet to get data and ItemPendingError mechanism to notify DataGrid when data is available
ItemPendingErrorEx - extends ItemPendingError to add dataReady/error methods.


I extended ItemPendingException by adding new method:


public function dataReady(data:Object):void
{
if (responders)
{
responders.forEach(
function(item:IResponder, index:int, arr:Array):void
{
item.result(data);
});
}
}




(I am somewhat surprised that similar method is not included by default)

This method iterates through all IResponders and notifies them when data is ready. IResponders would be added by DataGrid component so I don't need to do anything else here.

I also updated miss() method in the RemoteList class:

/**
* Called when we need to load a new page of data and throws a new exception for first request or
* rethrows already created exception if data loading already in progress.
*/
public function miss(index:int):void
{
var item:Object = localData[index];
if (item == null)
{
var page : Number = Math.floor(index / pageSize);

if (pagesPending[page] == null)
{
remoteDataSet.loadData(page, pageSize);

var loadingEvent:Event = new Event("Loading");
dispatchEvent(loadingEvent);

var error:ItemPendingErrorEx = new ItemPendingErrorEx("Loading Data ...");
pagesPending[page] = error;
throw error;
} else
{
throw pagesPending[page];
}
}
}



The method would throw an ItemPendingErrorEx to let notify DataGrid that data is loading.

When data is ready onDataReady() method is called by RemoteDataSet class:


public function onDataReady(loadedData:Object):void
{
var result:Array = loadedData.result[0]["value"].toArray();
var page:int = loadedData.result[1]["value"];

var error:ItemPendingErrorEx = pagesPending[page];

pagesLoaded[page] = true;
var beginIdx : Number = page * pageSize;

localData.splice.apply(localData, [beginIdx, result.length].concat(result));

if (cleanData)
{
cleanInvisiblePages(page);
}

trace("localData is updated: " + beginIdx + "-" + (beginIdx + result.length) );

error.dataReady(result);

if (isDataInit == false)
{
isDataInit = true;
dispatchResetEvent(); // make columns are initialized
}

var dataReadyEvent:Event = new Event("DataReady");
dispatchEvent(dataReadyEvent);
}



Here ItemPendingErrorEx.dataReady method is called that in turn would call DataGrid's supplied listener to notify that data is ready and DataGrid can redraw the rows.


You can download all code from here.
(Still need to add support for sorting)

Saturday, January 20, 2007

Remote Collection

Lately I was trying to implement remote collection that supports paging (or more of remote moving window of a data set). If you are lucky enough to use FDS2 than you don't to worry much about paging and can stop reading.

When I started to look into the problem I tried to use ItemPendingError but without much success (mostly because I could not find any decent doc or sample). I still believe that it's the way to go but I found the article on similar topic: http://weblogs.macromedia.com/mchotin/archives/2004/03/large_data_sets.cfm that used somewhat different approach (probably because it's not for Flex2 but Flex1.5) and decided to try it out with Flex2.

If you want to implement collection data provider in Flex2 you need to use either IList or ICollectionView. IList interface is somewhat easier so I decided to go with it.

Below is a sample implementation of IList. It's based on Matt Chotin's article I mentioned above. The main idea the same, when getItemAt() method is called by DataGrid, check if row is local, if not download it and send event notification. I also added some code to remove previously loaded item (we keep locally up to 3 pages and as current window of data moves, old data gets discarded)



package
{
import flash.events.*;
import flash.utils.Timer;

import mx.collections.ArrayCollection;
import mx.collections.ICollectionView;
import mx.collections.IList;
import mx.collections.IViewCursor;
import mx.collections.Sort;
import mx.collections.errors.ItemPendingError;
import mx.events.*;
import mx.controls.DataGrid;

public class RemoteList extends EventDispatcher implements IList
{
public var grid:DataGrid;

private var remoteDataSet:IRemoteDataSet;
private var localData : Array;
private var pagesLoaded : Array;
private var pagesPending : Array;
public var pageSize : int;
private var isDataInit:Boolean = false;

private var eventDispatcher:EventDispatcher;


public function RemoteList(ds:IRemoteDataSet)
{
localData = new Array();
pagesLoaded = new Array();
pagesPending = new Array();
pageSize = 10; //default

remoteDataSet = ds;
ds.registerListeners("getRows", onDataReady, onError);
ds.registerListeners("getLength", onLengthReady, onError);
remoteDataSet.getLength();

}

private function onLengthReady(result:Object):void
{
localData.length = uint(result.result);
trace("onLengthReady:" + localData.length)
dispatchRefreshEvent();
}

public function get length():int
{
// trace("length:" + localData.length);
return localData.length;
}

public function getItemAt(index:int, prefetch:int=0.0):Object
{
// trace("getItemAt:" + index);
var item:Object = localData[index];
if (item == null)
{
item = miss(index);
}
return item;
}

public function getItemIndex(item:Object):int
{
trace("getItemIndex:" + item);
return -1;
}

private function miss(index : Number) : Object
{
var page : Number = Math.floor(index / pageSize);

//it's possible that the page is already being loaded
if (pagesPending[page] == true)
{
//this miss event is useful for just monitoring what's going on
//dispatchEvent({type: "miss", index: index, alreadyPending: true});
return {id:"Loading:" + index};
}

//if the page is not loaded call for it
//var call = localDataService.getElements(page * pageSize, pageSize, this);
trace("remoteDataSet.loadData: " + page);
remoteDataSet.loadData(page, pageSize);

pagesPending[page] = true;
//dispatchEvent({type: "miss", index: index, alreadyPending: false});
return {id:"XXXLoading:" + index};;
}

public function onDataReady(loadedData:Object):void
{
var result:Array = loadedData.result[0]["value"].toArray();
var page:int = loadedData.result[1]["value"];

pagesPending[page] = false;
pagesLoaded[page] = true;
var beginIdx : Number = page * pageSize;

localData.splice.apply(localData, [beginIdx, result.length].concat(result));

cleanInvisiblePages(page);

trace("localData is updated: " + beginIdx + "-" + (beginIdx + result.length) );

if (isDataInit == false) {
isDataInit = true;
dispatchResetEvent(); // make columns are initialized
} else {
dispatchReplaceEvent(result, beginIdx);
//dispatchRefreshEvent();
}

//trace(localData[51]);
}

private function cleanInvisiblePages(page:int):void
{
trace("start cleaning");
for (var k:int = 0; k < pagesLoaded.length; k++)
{
if ( pagesLoaded[k] == true && (k > (page+1) k < (page-1)) )
{
var start:int = k*pageSize;
var end:int = start + pageSize;
end = (end > length) ? length : end;

for (var i:int = start; i < end; i++)
{
localData[i] = undefined;
}
trace("removed: " + start + "-" + end)

pagesLoaded[k] = false;
pagesPending[k] = false;
}
}
}

public function onError(error:Object):void
{
trace("error");
}

private function dispatchReplaceEvent(items:Array, startIndex:int):void
{
trace("dispatchReplaceEvent:" + startIndex );
var eventCollectionChange:CollectionEvent =
new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
eventCollectionChange.kind = CollectionEventKind.REPLACE;
eventCollectionChange.items = items;
eventCollectionChange.location = startIndex;
var result:Boolean = dispatchEvent(eventCollectionChange);

grid.rowCount = 100;
grid.validateNow();
}

private function dispatchRefreshEvent():void
{
trace("dispatchRefreshEvent");
var eventCollectionChange:CollectionEvent =
new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
eventCollectionChange.kind = CollectionEventKind.REFRESH;
var result:Boolean = dispatchEvent(eventCollectionChange);
}

private function dispatchResetEvent():void
{
trace("dispatchResetEvent");
var eventCollectionChange:CollectionEvent =
new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
eventCollectionChange.kind = CollectionEventKind.RESET;
var result:Boolean = dispatchEvent(eventCollectionChange);
}


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

public function toArray():Array
{
return null;
}

public function itemUpdated(item:Object, property:Object=null, oldValue:Object=null, newValue:Object=null):void
{
}

public function removeAll():void
{
}

public function setItemAt(item:Object, index:int):Object
{
return null;
}

public function removeItemAt(index:int):Object
{
return null;
}

public function addItem(item:Object):void
{
}
}
}



To get actual data we delegate request to IRemoteDataSet interface that has several methods to load data and data length:



package
{
public interface IRemoteDataSet
{
function registerListeners(method:String, dataHandler:Function, faultHandler:Function):void;

function getLength():void;

function loadData(startIndex:int, size:int):void;

function sort(param:Object):void;

}
}



Below is a sample of implementation of the interface (this is just a mock implementation that uses Timer to simulate data loading delay):



package
{
import flash.utils.Timer;
import flash.events.TimerEvent;
import mx.collections.ArrayCollection;
import mx.utils.ObjectProxy;

public class RemoteDataSet implements IRemoteDataSet
{
private var len:uint = 42;
private var handlerMap:Object = new Object();

public function loadData(page:int, size:int):void
{
// create data
var arr:Array = [];
var startIndex:int = page * size;
var endIndex:int = startIndex + size;
var max:int = (endIndex > len) ? len : endIndex;

for (var i:int=startIndex; i < max; i++)
{
var obj:Object = createObject();
obj["id"] = i;
obj["name"] = "name" + i;
arr.push(new ObjectProxy(obj));
}

// simulate remote connection with 2 sec delay
var timer:Timer = new Timer(2000, 1);
timer.addEventListener(TimerEvent.TIMER, function ():void
{
handlerMap["getRows"]["dataHandler"].call(this, {result:[{value:new ArrayCollection(arr)}, {value:page}]});
});
timer.start();
}

public function registerListeners(method:String, dataHandler:Function, faultHandler:Function):void
{
handlerMap[method] = new Object();
handlerMap[method]["dataHandler"] = dataHandler;
handlerMap[method]["faultHandler"] = faultHandler;
}

private function createObject():Object
{
var result:Object = new Object();
return result;
}

public function sort(param:Object):void
{
}

public function getLength():void
{
handlerMap["getLength"]["dataHandler"].call(this, {result:len});
}

}
}


So far so good, but unfortunatly I encountered an issue (maybe bug in DataGrid or my lack of Flex knowledge) - sometimes if you scroll up or down, the first row of the grid would not be redrawn (meaning it would display temp data like "Loading") despite the fact that actual data source is up to date. If you force refresh (by changing size of browser window) the correct data would appear.

If somebody knows what's going or if there are better ways to achieve paging please share the knowledge.

Here is the link to the whole project with demo and another sample of RemoteDataSet that uses web service (server side web services code are not included).

Monday, January 15, 2007

Minor update of the Grid Component

I moved my blog. Please use new url for latest info:
http://blog.widget-labs.com/2007/01/15/minor-update-of-the-grid-component/


I've done some fixes for the Grid Component. Still more of a prototype so you can expect some problems.



You can download new version from here.
You can see new version here.

To use it add the WidgetLibrary.swc into the project and follow an example.


Also the following properties are available:

  • headerLabel
  • headerStyleName
  • headerLabelStyleName
  • displayOptionsLinkStyleName
  • dataGridStyleName
  • columnEditorStyleName
  • columnEditorTitle

To control order of the columns you can define array of ColumnDescriptor and use dimensionData property. For example:

[Bindable]
public var testDimensionArr:Array =
[
new ColumnDescriptor("Author", "Author", "", true),
new ColumnDescriptor("Book", "Book", "", true),
new ColumnDescriptor("Edition", "Edition", "", true)
];

and add the following property to the Table tag: dimensionData="{testDimensionArr}"

Saturday, January 6, 2007

Flex Builder 2.0.1 is out but not without glitches

If you've tried to import or use your old (from 2.0) Flex Library project with 2.0.1 and experience crashes mostly likely the cause of the problem is dependency on the additional library.

For example if you created folder inside of Flex Library Project, then copied some 3rd party library there and linked the library to your project almost immediately Flex Builder may crash with the following not so helpful message:












One way to avoid the problem is to move the folder outside of the project.