Pages

Friday, August 29, 2008

Flex and Ruby on Rails with RubyAMF

My earlier Flex blogs often talks about Flex in combination with java but you don't need to java you can use Ruby on Rails too. There are a lot blog entries how to do this. I already made a little demo application where I use RoR and the HttpService in Flex. This solution is based on REST Web Services. RoR gives xml as output.
In this Blog entry I will use RubyAMF. With RubyAMF I can use RemoteObject, RubyAMF will convert the Ruby datatypes to the Flex datatypes and it is really fast.
If you want more information on RubyAMF just click here or go to the RubyAMF discussion groups

Let's create a new Flex Ruby application where we will use RubyAMF. I will create a small comic entry application.

Create a new RoR application:
rails comics_catalog

Change directory to the new comics_catalog folder:
cd comics_catalog

Edit the database connection file ( comics_catalog\config\database.yml ) with the right connection.

Update the project with RubyAMF:
ruby script/plugin install http://rubyamf.googlecode.com/svn/tags/current/rubyamf
This will download RubyAMF code and configuration from subversion to your project.

Create the comic table and controller
ruby script/generate rubyamf_scaffold comic

Edit the comic table configuration. Go to comics_catalog\db\migrate\ folder and edit the 20080827214400_create_comics.rb file. You will have an other filename because of the timestamp. The script need to look like this.

class CreateComics < ActiveRecord::Migration
def self.up
create_table :comics do |t|
t.string :name
t.string :description
t.float :price
t.timestamps
end
end

def self.down
drop_table :comics
end
end

Let's create the comic table in the database
rake db:migrate

The last step in RoR is to configure RubyAMF. For this we need to go the comics_catalog\config folder where we will edit the rubyamf_config.rb file.

require 'app/configuration'
module RubyAMF
module Configuration
ClassMappings.ignore_fields = ['created_at','created_on','updated_at','updated_on']
ClassMappings.translate_case = true
ClassMappings.assume_types = false
ParameterMappings.scaffolding = false


ClassMappings.register(:actionscript => 'Comic',
:ruby => 'Comic',
:type => 'active_record',
:attributes => ["id","name","description","price", "created_at", "updated_at"])

ClassMappings.force_active_record_ids = true
ClassMappings.use_ruby_date_time = false
ClassMappings.use_array_collection = false
ClassMappings.check_for_associations = false
ParameterMappings.always_add_to_params = true
end
end

I use ClassMappings.ignore_fields because I want to ignore the automatic created fields Ruby will fill these field automatically.
ClassMappings.translate_case is important if you want to Java naming style. RubyAMF will convert created_at to createdAt.
ClassMappings.assume_types is important if you do your know class mapping. In our case we will create the comic class in Flex too so we don't need this. This will give us a little more performance.
ParameterMappings.scaffolding is handy if you just want to pass an comic object or id to the delete method else you need to use {id:dg.selecteditem.id}
ClassMappings.register(:actionscript => 'Comic'. Don't use the package name just put in the actionscript class name.

Start the Ruby server
ruby script/server

Now we can go to Flex, where we create an new application.

first create a comic actionscript object

package vo
{

[RemoteClass(alias="Comic")]
[Bindable]
public class Comic
{
public var id:int;
public var name:String;
public var description:String;
public var price:Number;
public var createdAt:Date;
public var updatedAt:Date;
public function Comic()
{
}


}
}

And here is the mxml code.

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
creationComplete="loadAll2();">

<mx:Script>
<![CDATA[
import mx.rpc.AsyncToken;
import mx.rpc.events.FaultEvent;
import mx.rpc.events.ResultEvent;
import mx.controls.Alert;
import vo.Comic;

[Bindable]
private var comics:Array = new Array();

private function loadAll2():void {
var token:AsyncToken = AsyncToken(comicService.find_all());
token.kind = "fill";
}

private function save2():void {
var comic:Comic = new Comic();
comic.id = dg2.selectedItem.id;
comic.name = Name2.text;
comic.description = Description2.text;
var token:AsyncToken = AsyncToken(comicService.save(comic));
token.kind = "save";

}

private function create2():void {
var comic:Comic = new Comic();
comic.name = Name2.text;
comic.description = Description2.text;
var token:AsyncToken = AsyncToken(comicService.save(comic));
token.kind = "create";
}


private function destroy2():void {
var token:AsyncToken = AsyncToken(comicService.destroy(dg2.selectedItem.id));
token.kind = "delete";
}

private function faultHandler(event:FaultEvent):void {
Alert.show(event.fault.faultString + " : " + event.fault.faultCode + " : " + event.fault.faultDetail , "Error in LoginCommand");
}

private function resultHandler2(event:ResultEvent):void {
if ( event.token.kind == "fill" ) {
comics = event.result as Array;
} else {
loadAll2();
}
}



]]>
</mx:Script>
<mx:RemoteObject id="comicService" destination="rubyamf"
endpoint="http://localhost:3000/rubyamf_gateway/"
source="ComicsController"
showBusyCursor="true"
result="resultHandler2(event)"
fault="faultHandler(event)" />

<mx:ApplicationControlBar>
<mx:Button label="Create" click="create2()"/>
<mx:Button label="Update" click="save2()"/>
<mx:Button label="Delete" click="destroy2()"/>
<mx:Button label="Refresh" click="loadAll2()"/>
</mx:ApplicationControlBar>

<mx:DataGrid id="dg2" dataProvider="{comics}">
<mx:columns>
<mx:DataGridColumn dataField="id" headerText="Key"/>
<mx:DataGridColumn dataField="name" headerText="Name"/>
<mx:DataGridColumn dataField="description" headerText="Description"/>
</mx:columns>
</mx:DataGrid>
<mx:Form >
<mx:FormItem label="Name">
<mx:TextInput id="Name2" text="{dg2.selectedItem.name}" />
</mx:FormItem>
<mx:FormItem label="Description">
<mx:TextInput id="Description2" text="{dg2.selectedItem.description}"/>
</mx:FormItem>
</mx:Form>
</mx:Application>


That's All

Sunday, August 24, 2008

Flex upload and download with Blazeds

With Actionscript 3 and Flashplayer 10 you can now use the new FileReference features. For example you download and upload a file with RemoteObjects ( Blazeds / LifeCycle ). In the early days you had to use a upload and download servlets.
To use the new FileReference features please read these two articles flexexamples.com and using flashplayer 10

Here are some screenshots of the flex application.
First we can upload a file. The datagrid show the status of the uploaded files.

Off course we can download some files from a remote server. First we need to get a list of the remote files. For this we need to press the Get remote files button.

We can select a file and press the Retrieve File Button. When the status is ready we can save this file by pressing the Save File Button.

Here you can download the Flex source code

The code of the upload panel

<?xml version="1.0" encoding="utf-8"?>
<mx:Panel xmlns:mx="http://www.adobe.com/2006/mxml"
layout="vertical" width="100%" height="100%"
title="Upload Files">


<mx:Script>
<![CDATA[
import mx.rpc.AsyncToken;
import mx.rpc.events.FaultEvent;
import mx.rpc.events.ResultEvent;

private var refUploadFile:FileReference;

private var UploadFiles:Array = new Array();


// Called to add file(s) for upload
private function addFiles():void {
refUploadFile = new FileReference();
refUploadFile.browse();
refUploadFile.addEventListener(Event.SELECT,onFileSelect);
refUploadFile.addEventListener(Event.COMPLETE,onFileComplete);
}

// Called when a file is selected
private function onFileSelect(event:Event):void {
UploadFiles.push({ name:refUploadFile.name,
size:formatFileSize(refUploadFile.size),
status:"initial"});
listFiles.dataProvider = UploadFiles;
listFiles.selectedIndex = UploadFiles.length - 1;

refUploadFile.load();
for ( var i:int = 0 ; i < UploadFiles.length ; i++ ) {
if( UploadFiles[i].name == refUploadFile ) {
UploadFiles[i].status = "loaded";
listFiles.dataProvider = UploadFiles;
break;
}
}
}

// Called to format number to file size
private function formatFileSize(numSize:Number):String {
var strReturn:String;
numSize = Number(numSize / 1000);
strReturn = String(numSize.toFixed(1) + " KB");
if (numSize > 1000) {
numSize = numSize / 1000;
strReturn = String(numSize.toFixed(1) + " MB");
if (numSize > 1000) {
numSize = numSize / 1000;
strReturn = String(numSize.toFixed(1) + " GB");
}
}
return strReturn;
}

private function onFileComplete(event:Event):void
{
refUploadFile = event.currentTarget as FileReference;
var data:ByteArray = new ByteArray();
refUploadFile.data.readBytes(data,0,refUploadFile.data.length);

var token:AsyncToken = AsyncToken(
remoteUpload.doUpload(data, refUploadFile.name)
);

token.kind = refUploadFile.name;

for ( var i:int = 0 ; i < UploadFiles.length ; i++ ) {
if( UploadFiles[i].name == refUploadFile ) {
UploadFiles[i].status = "upload";
listFiles.dataProvider = UploadFiles;
break;
}
}
}

private function uploadResultHandler(event:ResultEvent):void
{
for ( var i:int = 0 ; i < UploadFiles.length ; i++ ) {
if( UploadFiles[i].name == event.token.kind ) {
UploadFiles[i].status = "finished";
listFiles.dataProvider = UploadFiles;
break;
}
}
}

private function faultResultHandler(event:FaultEvent):void
{
for ( var i:int = 0 ; i < UploadFiles.length ; i++ ) {
if( UploadFiles[i].name == event.token.kind ) {
UploadFiles[i].status = "error";
listFiles.dataProvider = UploadFiles;
break;
}
}
}


]]>
</mx:Script>

<mx:RemoteObject id="remoteUpload" destination="FileUtils"
result="uploadResultHandler(event)"
fault="faultResultHandler(event)"/>


<mx:Canvas width="100%" height="100%">
<mx:DataGrid id="listFiles" left="0" top="0" bottom="0" right="0"
allowMultipleSelection="true" verticalScrollPolicy="on"
draggableColumns="false" resizableColumns="false" sortableColumns="false">
<mx:columns>
<mx:DataGridColumn headerText="File" width="150" dataField="name" wordWrap="true"/>
<mx:DataGridColumn headerText="Size" width="50" dataField="size" textAlign="right"/>
<mx:DataGridColumn headerText="Status" width="50" dataField="status" textAlign="right"/>
</mx:columns>
</mx:DataGrid>
</mx:Canvas>
<mx:ControlBar horizontalAlign="center" verticalAlign="middle">
<mx:Button id="btnAdd" toolTip="Add file(s)" click="addFiles()"
label="Upload Files" width="150"/>
</mx:ControlBar>
</mx:Panel>


The code of the download panel

<?xml version="1.0" encoding="utf-8"?>
<mx:Panel xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical"
width="100%" height="100%" title="Download Files">

<mx:Script>
<![CDATA[
import mx.collections.ArrayCollection;
import mx.rpc.events.FaultEvent;
import mx.rpc.events.ResultEvent;
import mx.rpc.AsyncToken;

private var UploadFiles:Array = new Array();
private var UploadFilesColl:ArrayCollection = new ArrayCollection();
private var fileData:ByteArray = new ByteArray();
private var fileName:String;


private function uploadResultHandler(event:ResultEvent):void
{
if ( event.token.kind == "remoteFileList") {
UploadFilesColl = event.result as ArrayCollection;
for ( var i:int = 0 ; i < UploadFilesColl.length ; i++ ) {
UploadFiles.push({ name:UploadFilesColl[i]
, status:"initial"});
}
listFiles.dataProvider = UploadFiles;
} else {
fileData = event.result as ByteArray;
fileName = event.token.kind;
for ( var b:int = 0 ; b < UploadFiles.length ; b++ ) {
if( UploadFiles[b].name == event.token.kind ) {
UploadFiles[b].status = "Ready";
listFiles.dataProvider = UploadFiles;
break;
}
}

}
}

private function faultResultHandler(event:FaultEvent):void
{
}

private function saveFile(event:Event):void
{
var fileReference:FileReference = new FileReference();
fileReference.save(fileData,fileName);
}

private function getRemoteFiles(event:Event):void
{
var token:AsyncToken = AsyncToken(remoteDownload.getDownloadList());
token.kind = "remoteFileList";
}

private function getDownload(event:Event):void
{
var token:AsyncToken = AsyncToken(
remoteDownload.doDownload(listFiles.selectedItem.name));
token.kind = listFiles.selectedItem.name;
}
]]>
</mx:Script>


<mx:RemoteObject id="remoteDownload" destination="FileUtils"
result="uploadResultHandler(event)"
fault="faultResultHandler(event)"/>


<mx:Canvas width="100%" height="100%">
<mx:DataGrid id="listFiles" left="0" top="0" bottom="0" right="0"
verticalScrollPolicy="on"
draggableColumns="false" resizableColumns="false" sortableColumns="false">
<mx:columns>
<mx:DataGridColumn headerText="File" width="150" dataField="name" wordWrap="true"/>
<mx:DataGridColumn headerText="Status" width="50" dataField="status" textAlign="right"/>
</mx:columns>

</mx:DataGrid>
</mx:Canvas>
<mx:ControlBar horizontalAlign="center" verticalAlign="middle">
<mx:Button id="btnList" toolTip="List remote files"
width="150"
label="Get Remote Files"
click="getRemoteFiles(event)"/>
<mx:Button id="btnRetrieve" toolTip="Retrieve file"
width="150" click="getDownload(event)" label="Retrieve File"/>
<mx:Button id="btnSave" toolTip="Save file"
width="150" click="saveFile(event)" label="Save File"/>
</mx:ControlBar>



</mx:Panel>

The java code

package nl.ordina.flex;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

import java.util.ArrayList;
import java.util.List;

public class FileUtils {


public String doUpload(byte[] bytes, String fileName) throws Exception
{
fileName = System.getProperty("java.io.tmpdir") + "/" + fileName;
File f = new File(fileName);
FileOutputStream fos = new FileOutputStream(f);
fos.write(bytes);
fos.close();
return "success";
}

public List getDownloadList()
{
File dir = new File(System.getProperty('java.io.tmpdir'));
String[] children = dir.list();
List dirList = new ArrayList();
if (children == null) {
// Either dir does not exist or is not a directory
} else {
for (int i=0; i<children.length; i++) {
// Get filename of file or directory
dirList.add( children[i]);
}
}
return dirList;
}

public byte[] doDownload(String fileName)
{
FileInputStream fis;
byte[] data =null;
FileChannel fc;

try {
fis = new FileInputStream(System.getProperty("java.io.tmpdir") + "/" + fileName);
fc = fis.getChannel();
data = new byte[(int)(fc.size())];
ByteBuffer bb = ByteBuffer.wrap(data);
fc.read(bb);
} catch (FileNotFoundException e) {
// TODO
} catch (IOException e) {
// TODO
}
return data;
}
}

Sunday, August 17, 2008

Cairngorm Flex MVC is now opensource

Cairngorm is MVC framework which you can use in Adobe Flex or Adobe AIR. In Flex you can put all your code in one mxml but this makes hard to develop and maintain when the project has a particular size. Adobe has developed Cairngorm for it's own projects and it is very positif they released it to the opensource community. Adobe already released BlazeDS, so what's next?
You can download Cairngorm from here.
Here is a cairngorm example application which gives you a nice overview what Cairngorm does. This is a login application. Use admin as username and password.

Sunday, August 10, 2008

MVC in Flex

In Flex you can put all your code in one mxml. Off course this works perfectly but is not easy to maintain and all the components are tidely connected. The Model View Controller design pattern can help you to split up the flex application so we have loosely coupled and cleaner components. These components can be easily reused without breaking up the whole application. With MVC design pattern helps you to make a big flex application without having a code nightmare.
Here is an example of a simple mxml application where the label get it's data directly from the list.

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:List id="list" >
<mx:dataProvider>
<mx:Array>
<mx:Object label="one"/>
<mx:Object label="two"/>
</mx:Array>
</mx:dataProvider>
</mx:List>
<mx:Label text="{list.selectedItem.label}"/>
</mx:Application>

Let's change this Flex application in an Model View Controller application. I know this is an overkill for this example but in the large application you will benefit from this approach. Create an new component List1 in the new folder view.

<?xml version="1.0" encoding="utf-8"?>
<mx:List xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:dataProvider>
<mx:Array>
<mx:Object label="one"/>
<mx:Object label="two"/>
</mx:Array>
</mx:dataProvider>
</mx:List>

Do the same for the Label and name this component Label1. Store Label1 in the view folder.

<?xml version="1.0" encoding="utf-8"?>
<mx:Label xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:text>empty</mx:text>
</mx:Label>

We can use these components after adding a namespace to the application mxml.

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns:view="view.*">
<view:List1/>
<view:Label1/>
</mx:Application>

This time we have the same application but there is no interaction between the components. We can change this by fire an event when the list is changed. This event can be captured by the controller which stores the new list value in the model. Events make these components loosely coupled. The label component can retrieve the new list value from the model or actionscriptcan monitor the list value of the model so when it is changed, it can update a local variable.
Create a new event and fire this event when the list component is changed.
Let's create a new actionsript class ChangeEventClass which extends from Event. Store this class in the event folder. We want to pass the new value of the list to the other classes. So we need to build a custom event.

package event
{
import flash.events.Event;
public class ChangeEventClass extends Event {

public static const name:String = "changeEvName";
public var newValue:String;
public function ChangeEventClass( prop:String
, type:String
, bubbles:Boolean=false
, cancelable:Boolean=false){
newValue = prop;
super(type, bubbles, cancelable);
}
}
}

Now we can change the list component where we use the change property to fire the new event. It is very important to set the bubbles parameter to true else the Controller class can not capture the event. Default only the parent component can listen for this event.

<?xml version="1.0" encoding="utf-8"?>
<mx:List xmlns:mx="http://www.adobe.com/2006/mxml" change="changeList(event);">
<mx:Script>
<![CDATA[
import event.ChangeEventClass;

private function changeList(event:Event):void {
var eventObj:ChangeEventClass =
new ChangeEventClass( selectedItem.label
, ChangeEventClass.name
, true);
dispatchEvent(eventObj);
}
]]>
</mx:Script>

<mx:dataProvider>
<mx:Array>
<mx:Object label="one"/>
<mx:Object label="two"/>
</mx:Array>
</mx:dataProvider>
</mx:List>

We want to capture this event and store the value in the model. Let's create the model first. The model has to be a singleton class so an other component can retrieve this value. Create a new actionscript class Global and put this class in the model folder. The listValue has to be bindable else we can not retrieve this value from the label component.

package model
{
public class Global
{
public function Global(){
if( instance != null ){
throw( new Error( "there can be only one instance of global" ) );
}
}

private static var instance:Global;

[Bindable]
public var listValue:String;

public static function getInstance():Global{
if( instance == null ){
instance = new Global();
}
return instance;
}
}
}

The next step is to create a Controller class and put this class in the controller folder. In this class we will capture the list change event and store the new value in the model variable.

package controller
{
import event.ChangeEventClass;
import flash.events.Event;
import model.Global;
import mx.core.UIComponent;
import mx.events.FlexEvent;

public class Controller extends UIComponent{
public function Controller() {
addEventListener( FlexEvent.CREATION_COMPLETE, setupEventListeners );
}
private function setupEventListeners( event:Event ):void {
systemManager.addEventListener(ChangeEventClass.name,changeEvNameHandler);
}
private function changeEvNameHandler(listEvent:ChangeEventClass):void {
Global.getInstance().listValue = listEvent.newValue;
}
}
}

The application mxml will now look like this. We can import the Controller class to this mxml.

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns:view="view.*"
xmlns:controller="controller.*">
<controller:Controller/>
<view:List1/>
<view:Label1/>
</mx:Application>

We only have to change the label component so its retrieve its text value from the model class.

<?xml version="1.0" encoding="utf-8"?>
<mx:Label xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:Script>
<![CDATA[
import model.Global;
]]>
</mx:Script>

<mx:text>{Global.getInstance().listValue}</mx:text>
</mx:Label>

Or we can listen when the listValue changes in the model class.

<?xml version="1.0" encoding="utf-8"?>
<mx:Label xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:Script>
<![CDATA[
import model.Global;
import mx.events.PropertyChangeEvent;
import mx.binding.utils.ChangeWatcher;

private var stateWatcher:ChangeWatcher = ChangeWatcher.watch( Global.getInstance()
, "listValue"
, handle_stateChange );
private function handle_stateChange( event:PropertyChangeEvent ):void {
text = Global.getInstance().listValue;
}
]]>
</mx:Script>
</mx:Label>

Now we have a MVC flex application.