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).

2 comments:

Kitty said...

高収入 チャットレディー 求人
稼げる 高額携帯チャトレ募集
高時給 稼げる チャットレディ
稼げる 高額メールレディ募集
高収入 稼げる 副業
高時給 稼げる メールレディ
週払い 稼げる 仕事
稼げる 高収入チャットレディー募集
高時給 稼げる メールレディー
高収入 チャットレディー 募集
人妻 稼げる 在宅バイト
高時給 稼げる メールレディ
未経験者 稼げる 仕事
稼げる ライブチャット
人妻 稼げる アルバイト
稼げる 高収入副業
週払い 高額 在宅ワーク
素人 稼げる 在宅バイト
簡単 お小遣い稼ぎ
稼げる 携帯チャットレディ募集
高時給 稼げる アルバイト
初めて 稼げる 副業
週払い 稼げる 在宅ワーク
稼げる 高時給ライブチャット募集
夜だけ 稼げる 在宅ワーク
夜だけ 稼げる 高額メールレディ募集
高収入 稼げる 副業
稼げる 高時給携帯チャットレディ募集
高時給 稼げる 副業
稼げる 高収入在宅ワーク

Kitty said...

不倫希望新妻出会い系無料 ♪ エッチ好き新妻出逢い ♪ 愛人主婦出逢い系 ♪ 割切り40代出会い ♪ セフレ募集30代出逢い ♪ 割切り30代出会い ♪ 近所の主婦即ハメ出会い ♪ 愛人希望30代出会いサイト ♪ 淫乱人妻と出会える ♪ 割り切ったギャル妻無料出会い ♪ セフレ募集中ギャル妻出会い系サイト ♪ ドエム奥さん出会い系サイト ♪ 浮気希望奥さん出会い ♪ 愛人希望新妻と出会い ♪ 淫乱ギャル妻とやれる ♪ 愛人奥さん即エッチ出会い ♪ ドM30代とやれる出会い系 ♪ エッチ好き主婦と出会える ♪ セフレ希望主婦即エッチ出会い ♪ 愛人希望奥さん即エッチ出会い ♪