Friday, March 9, 2007

Just quick update:

I am moving my blog to the new address:
http://blog.widget-labs.com/

Monday, March 5, 2007

Cascade List component

UPDATE:

I moved my blog. Please use new url for latest info:
http://blog.widget-labs.com/2007/03/05/cascade-list-component/



I would like to share a Cascade List component that allows to work with tree like structures:


It should accept the same data you can pass to flex Tree control but would display it by using List elements stacked together. You can sign up for listItemSelected event to listen for selection and control style by styleName property. See link to an example below for more details.

You can see a demo here.

Source code and example can be found here.

Saturday, February 24, 2007

Logging and Debugging deployed Flex applications

I want to share my experience with troubleshooting Flex problem in deployed environment. I am working on a application that uses web service as back end. I do development and basic testing in Flex Builder so I can run debugger and see trace outputs.

But lately I decided to try run my app from web container (tomcat). The first problem I encountered was my login screen doesn't work. I pressed login and nothing happened. I have code that uses Application.application.url parameter to figure out host site and based on url decide where to locate web services. In local debug environment I use hardcoded url (and it works fine), in deployed one I have code that construct url dynamically (and obviously it wasn't working well).

What should I do?

Luckily Flex Builder provides an easy way to run remote application (loaded from web container) in debug mode. First you need to make sure that you used -debug=true flag when you compiled your application. Once you done it, you can create a new debug run configuration in Flex Builder and specify url of deployed application and related project:



Now you just need to set breakpoints, press debug and see why something doesn't work.

Remote debug mode is quite convenient if you have full control over environment. But eventually your application would be deployed in customer site and most likely you would not be able to use debugger to trace down your problem.

To address this problem you need to make sure you have enough log statements in your code that would help to pinpoint the problem so make sure you are aware about mx.logging.* package.

It's very similar to java logging api and should not take long to figure out. The main caveat is that to use default trace facility(mx.logging.targets.TraceTarget) you need to run debug version of flash player. TraceTarget allows you to output message to system.out (eclipse console) or flashlog.txt. To make sure that flashlog.txt is created, you need to check that you have mm.cfg in your user home directory (in my case I didn't have mm.cfg created by installer). If you don't have mm.cfg - create it in C:\Documents and Settings\{username}\( obviously this window specific, check manual for linux path) and populate with parameter you want to use for ex.:

ErrorReportingEnable=1
TraceOutputFileEnable=1
MaxWarnings=100

Now you would be able to see flashlog.txt in c:\Documents and Settings\{username}\Application Data\Macromedia\Flash Player\Logs\.


Most likely you would not be able to use debug flash player in customer environment. You can write your own extension of flex logging classes to make sure you can capture trace event somehow. But the easier way to start with already existing flex logging extension "trace panel" from Adobe Flex Exchange (or directly from author's site http://www.faratasystems.com/?page_id=45) . It provide log capturer win32 application and source code for AbstractTarget that communicates with the win32 app and shows log statement when using none-debug version of flash player.



Hope this was helpful. If know some other techniques that can be used in Flex to simplify problem troubleshooting let me know.

Thursday, February 15, 2007

Handling Web Service exception in Flex code

In this post I'll explain how to propogate exception details from Web Services into Flex. I am using java XFire web service implementation but similar steps can be applied to different ws implementations.

Let assume you defined a web servie method:

public String getSomething() throws ClientException

where ClientException would contain information like exception id and message.

If the method throw an exception, on Flex side you want to extract id and message to present it to user.To do this in Flex we can use standard "fault" handler (I assume you know how to call web service from Flex so I would go into much details there)



service.doSomething.addEventListener("result", responder.result);
service.doSomething.addEventListener("fault", responder.fault);
service.doSomething();


Here we use already defined web service (service) and assign "result" handler to handle normal flow and "fault" handler to handler any exception, including our own ClientException.

responder.fault may look like:



public function fault(info:Object):void {

var ns:Namespace = new Namespace("http://www.acme.com");
default xml namespace = ns;

var xml:XML = new XML(info.fault.faultDetail);

if (xml.id[0] == "123") {
show(xml.message[0]);
}
}



Here we acccess Flex provided info object that includes fault member variable. Using fault var we can access exception details (in this case id and message) and process this data.

So far nothing really special done on Flex side.

Now let's look how to implement server side. I would not go into details how expose web service in XFire (you can find this info in XFire docs). Instead let's focus on Flex specific moments. The most important one is that Flex would not process SOAP fault message if HTTP status code other then 200. Here is excerpt from Flex manual:

> "On web browsers, when a service returns any status code other than 200,
> Adobe Flash Player
> cannot read the body of the response. If the status code is 500 and the
> body contains a
> fault, there is no way to get to the fault. The proxy works around this
> issue by forcing the
> status code for faults to 200; the player passes the body of the
> response along with the
> fault intact. "

It means that we need to add a code on server side to ensure that response code is equal to 200 when we throw an exception. When exception thrown from XFire HTTP status code would be 500 (web services standard implies it), so we need to create a servlet filter that would set HTTP status code to 200 to allow Flex web service engine access SOAP body.

This is also pretty standard stuff, you would need to define filter and associate it with XFire servlet in web.xml (you can find more info on filter here):



Exception filter would like:



import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;

public class ExceptionFilter implements Filter {

public void destroy() {
}

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {

HttpServletResponse httpResponse = (HttpServletResponse) response;
ExceptionHttpServletResponseWrapper wrapper = new ExceptionHttpServletResponseWrapper(httpResponse);

chain.doFilter(request, wrapper);
}

public void init(FilterConfig arg0) throws ServletException {
}

}



And you to define HttpServletResponsWrapper that would control setStatus() method to ensure that status code is 200.



import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;

public class ExceptionHttpServletResponseWrapper extends
HttpServletResponseWrapper {

public ExceptionHttpServletResponseWrapper(HttpServletResponse response) {
super(response);
}

@Override
public void setStatus(int statusCode) {
if (statusCode == 500) {
super.setStatus(200);
}
}
}



The above code can be used for any web service implementation.

We still need to add some specific way XFire handles user's exception. One way to it is to create a custom ClientExceptionDetails that contain id and message.



public class ClientExceptionDetails {

private int id;
private String message;

public ClientExceptionDetails(int id, String message) {
super();
this.id = id;
this.message = message;
}

public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}



And make sure that ClientException uses this info:



import javax.xml.namespace.QName;

import org.codehaus.xfire.fault.FaultInfoException;
import org.codehaus.xfire.fault.XFireFault;

public class ClientException extends FaultInfoException {

private ClientExceptionDetails faultDetail;

public ClientException(String message, ClientExceptionDetails detail) {
super(message);

this.faultDetail = detail;
}

public ClientExceptionDetails getFaultInfo() {
return faultDetail;
}

public static QName getFaultName() {
return new QName("http://www.acme.com", "ClientException");
}
}



(Check XFire doc for more info about FaultInfoException)

That's all, now when you throw ClientException, XFire would use ClientExceptionDetails to populat SOAP body with id and message that you can extract later on Flex side from fault.faultDetail.

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}"