RIA con flex2sdk, un semplice user manager

Requisiti per la lettura

Premessa

flex2sdk e' il tool gratuito di Adobe per la creazione e la compilazione di progetti in flex2 e actionscript3. Nella pratica con questo software, costituito da un gruppo di elementi come ad esempio il framework di componenti flex2 e diverse utility a linea di comando, si possono costruire sia RIA che progetti in puro AS3 a costo zero. Il grosso vantaggio nell'adozione di questo software e' la possibilita' di lavorare piu' o meno occasionalmente su ogni sistema senza riguardo al sistema operativo o ad altro software installato.
Si puo' quindi dimostrare utile per la compilazione di progetti in remoto, o per modifiche al volo, dal momento che flex2sdk viene distribuito come file zippato e non richiede praticamente di installare nient'altro che una jre. Il rovescio della medaglia e' la mancanza di un editor visuale e di un IDE, entrambi disponibili con flex builder, ma questo non impedisce di poter costruire anche da zero dei progetti validi, anche solo a scopo didattico, di valutazione e prototipi.
Il tutorial ha come scopo l'introduzione all'uso di flex2sdk attraverso l'analisi di un progetto di base, ma completo di funzionalita', andremo ad implementare una sorta di interfaccia semplificata per la gestione di utenti: visualizzazione, modifica, aggiunta e rimozione di record. Per farlo oltre a flex2 useremo amfphp, a riprova del fatto che tale tecnologia ben si sposa con qualsiasi altra lato server. I dati com'e' naturale pensare, sono presenti in una tabella mysql di nome users.

L'applicazione principale

Si chiama FlexGridTest, un nome come un altro, eccola in azione:

flexgridtest in azione: interfaccia principale flexgridtest in azione: editing di un record


<?xml version="1.0" encoding="utf-8"?>
<mx:Application
	xmlns:mx="http://www.adobe.com/2006/mxml"
	xmlns:custom="components.*"
	creationComplete="initFlexGridTest()"
	backgroundColor="#EEEEEE"
	paddingTop="0" paddingLeft="0" paddingRight="0" paddingBottom="0"
	backgroundAlpha="0"
	frameRate="31" >

    <mx:Script source="fgt.as" />
	<mx:Style source="components/datagrid.css" />

    	<mx:Panel id="mainPanel" title="DataGrid test" height="100%" width="100%"
        paddingTop="2" paddingLeft="2" paddingRight="2" paddingBottom="2" >

	        <mx:DataGrid id="dg" width="100%" height="100%">
				<mx:columns>
					<mx:DataGridColumn dataField="id" visible="false" />
					<mx:DataGridColumn headerText="name" dataField="sName" />
					<mx:DataGridColumn headerText="surname" dataField="sSurname" />
					<mx:DataGridColumn headerText="role" dataField="eRole" />

					<mx:DataGridColumn headerText="options"
						textAlign="center"
						resizable="false"
						itemRenderer="components.IRRowOptions" width="100" sortable="false" />
				</mx:columns>
	        </mx:DataGrid>

        	<mx:ControlBar width="100%">
	            <mx:Spacer width="100%"/>
				<mx:Button id="btReloadItems" label="reload" />
				<mx:Button id="btAddItem" label="add item" />
			</mx:ControlBar>

		</mx:Panel>

</mx:Application>

le applicazioni in flex vengono costruite a partire da particolari file xml, la loro estensione e' .mxml, partono con un header che contiene le caratteristiche di massima, seguono gli elementi che costituiscono l'interfaccia, ognuno dei quali puo' disporre di alcuni attributi come la posizione, le dimensioni, aspetti grafici e codice actionscript.
La nostra e' composta da un datagrid e due pulsanti situati in basso a destra, la loro funzione e' rispettivamente il reload dei dati di tabella e l'aggiunta di un nuovo record.
Ogni record elencato nel datagrid dispone di due pulsanti per l'editing e la cancellazione, questo accorgimento rende l'interazione col datagrid piu' intuitivo e diretto.
Nel datagrid sono specificati i vari campi ognuno col suo attributo datafield che corrispondera' ad una colonna di tabella che avremo preparato in mysql. Per brevita' ci limiteremo ad osservare alcuni punti salienti dell'applicazione, uno di questi e' nell'header:
	creationComplete="initFlexGridTest()"
questo e' il punto di ingresso dell'applicazione, a partire dalla function in questione andremo a scriptare tutto il resto, il codice e' specificato dal nodo mx:script,
	    <mx:Script source="fgt.as" />
Da notare l'associazione all'evento creationComplete, in tal modo tutto cio' che imporremo sara' eseguito solo ad interfaccia inizializzata, ovvero dopo il preload e la costruzione degli elementi visuali nel browser.

si e' scelto di separare il codice flex dall'actionscript per diverse ragioni, personalmente lo ritengo un approccio piu' razionale, dall'altro posso usare un editor AS3 coi benefici derivanti (tag coloring, code hint e quant'altro), in circolazione ci sono diversi editor ottimi e liberi, per elencare alcuni: flashdevelop, se|py ed eclipse integrato con alcuni plug-in.

Prima di continuare diamo un'occhiata a come gestire questo progetto, e' composto di diversi file sorgente e contributi vari e della sezione di pubblicazione, ecco la disposizione che di solito prediligo in eclipse:

flex2sdk folder project

nell'immagine si vede gia' una buona parte dei file che eventualmente andremo a creare (il codice sorgente di tutto il progetto e' qui), le righe evidenziate corrispondono alle cartelle che andremo a creare da subito, src conterra' i nostri file sorgente, deploy il risultato della compilazione, le sottodirectory lib e misc conterranno rispettivamente del codice php di supporto e css/js.
La cartella src avra' anch'essa delle sottodirectory: prj e components, le vedremo piu' avanti.

fgt.as

e' il codice principale, col punto d'ingresso e l'infrastruttura generale, andiamo a vederla per sommi capi:

/**
 * @description user manager application code
 * this file is intended only for inclusion
 * jaco_at_pixeldump
 */

import mx.core.*;
...

import prj.*;
import prj.jaco.amf.*;

private var phpGateway:String = "http://p4nb/flashservices/gateway.php";
private var gw:RemotingConnection;
private var pop:ItemEditor;

/**
 * @description ENTRY POINT
 */
private function initFlexGridTest():void{
	gw = new RemotingConnection(phpGateway);
	setup_button_events();
	reload_items(null);
}

/**
 * @description open a pop-up for adding/editing item
 */
public function item_view(itemData:Object):void {
	pop = ItemEditor(PopUpManager.createPopUp(this, ItemEditor, true));
	pop.set_itemData(itemData);
	PopUpManager.centerPopUp(pop);
	pop.btSave.addEventListener(MouseEvent.CLICK, onIESaveClick);
	pop.btClose.addEventListener(MouseEvent.CLICK, onIECloseClick);
}

/**
 * @event
 * @description trigger when server send response on news item delete request
 */
private function onIESaveClick(evt:MouseEvent):void {
	var itemData:Object = pop.get_itemData();
	onIECloseClick(null);
	gw.call("ItemsManager.save_update_item", new Responder(onItemsResult, onFault), itemData);
}

/**
 * @event
 * @description
 */
public function onItemsResult( result:Object ) : void {
	dg.dataProvider = result;
	var columns:Array = dg.columns;
	columns[0].visible = false;
	dg.columns = columns;
}

/**
 * @event
 * @description
 */
public function onFault( fault : String ) : void {
    Alert.show("sorry there was some error talking to server");
}

/**
 * @event
 * @description trigger when user confirm to delete news
 */
public function request_item_delete(evt:CloseEvent):void {
	if (evt.detail==Alert.YES){
		var sid:String = dg.selectedItem.id;
		gw.call( "ItemsManager.request_drop_item", new Responder(onItemsResult, onFault), sid);
	}
}

/**
 * @event
 * @description
 */
public function reload_items(evt:MouseEvent):void {
	gw.call( "ItemsManager.request_items", new Responder(onItemsResult, onFault));
}

/**
 * @event
 * @description
 */
public function pop_add_item(evt:MouseEvent):void { item_view(null); }

/**
 * @event
 * @description
 */
private function onIECloseClick(evt:MouseEvent):void { PopUpManager.removePopUp(pop);
}

/**
 * @description assign button events
 */
private function setup_button_events():void {
	btReloadItems.addEventListener(MouseEvent.CLICK, reload_items);
	btAddItem.addEventListener(MouseEvent.CLICK, pop_add_item);
}

Il codice parte coi soliti import e alcune variabili globali, una importante e' phpGateway che contiene l'url del gateway amfphp, naturalmente va settata secondo la propria installazione di amfphp. la initFlexGridTest si limita a creare un oggetto remoting, a settare gli eventi dei due pulsanti e a caricare i dati degli utenti nel datagrid. L'oggetto remoting e' in realta' una classe che eredita da NetConnection, presente in prj.jaco.amf:

package prj.jaco.amf {


    import flash.net.*;
	import mx.rpc.http.*;
	import mx.rpc.events.*;

    public class RemotingConnection extends NetConnection {

		public function RemotingConnection( sURL:String ){
			objectEncoding = ObjectEncoding.AMF3;
			if (sURL) connect( sURL );
		}
    }
}

Tale classe ha lo scopo di semplificare la creazione di oggetti remoting pronti all'uso, sono impostati per lavorare con AMF3, percio' e' consigliato installare amfphp1.9, le versioni precedenti supportano solo il formato AMF0, eventualmente cambiare questo parametro.

la setup_buttons imposta gli eventi sui pulsanti, in AS3 e' sufficiente invocare addEventListener, una volta presa l'abitudine, gestire eventi diviene presto una semplice formalita', gestori d'evento AS1/2 addio!

infine si chiama la reload_items per popolare il datagrid, funziona chiamando un metodo remoto in amfphp attraverso il metodo call, dell'oggetto remoting creato in precedenza e impostando i gestori d'evento che reagiranno in caso di risposta valida o problemi di altra natura. Possiamo riutilizzare un oggetto remoting per qualsiasi metodo che abbiamo implementato in amfphp, per tale motivo e' stato scelto di crearne uno a livello di applicazione.

Scorrendo il codice si notera' che la maggior parte riguarda la gestione di eventi. Alcuni di questi vengono generati da un cosiddetto ItemRenderer che in pratica e' un componente personalizzato visualizzato in ogni nuova riga del datagrid, nella colonna options. Tale component e' residente nel file IRRowOptions.mxml ed e' costituito da due pulsanti di tipo immagine:
<?xml version="1.0" encoding="utf-8"?>
	<mx:HBox horizontalAlign="center" verticalAlign="middle" xmlns:mx="http://www.adobe.com/2006/mxml">
		<mx:Button id="btEditRow" styleName="editButton" click="request_item_edit()" />
		<mx:Button id="btDropRow" styleName="dropButton" click="request_item_drop()" />

<mx:Script>
        <![CDATA[

			import mx.controls.*;
			...

           	public var app:Object = mx.core.Application.application;

            public function request_item_drop():void {
            	var dg:DataGrid = app.dg;
            	var si:Object = dg.selectedItem;
            	var id:String = si.id;
            	var sName:String = si.sName;
            	var sSurname:String = si.sSurname;
            	var eRole:String = si.eRole;

            	var msg:String = "do you really want to delete this item?";
            	msg += "\nid: " +id;
            	msg += "\nname: " +sName;
            	msg += "\nsurname: " +sSurname;
            	msg += "\nrole: " +eRole;
             	Alert.show(msg, "DELETE: Please confirm", 3, this, app.request_item_delete);
            }

            public function request_item_edit():void {
            	var dg:DataGrid = app.dg;
            	var si:Object = dg.selectedItem;
            	app.item_view(si);
			}
         ]]>

    </mx:Script>
	</mx:HBox>

Stavolta, visto che il codice non e' molto, si e' preferito mettere tutto insieme, il codice flex e actionscript. Da rilevare il fatto che il codice AS3 va inserito in un tag CDATA, per non violare la condizione well-formed del file mxml.
I pulsanti prendono le immagini dagli stili css rispettivamente editButton e dropButton, i quali sono presenti nel file datagrid.css, il cui path e' specificato nel file principale FlexGridTest.mxml.
Tali stili imporranno al compilatore di includere le immagini (embed) nel file swf di produzione, diversamente da html non sara' quindi necessario pubblicare ne' il file css ne' i contributi settati negli stili descritti nel css.

Popup - ItemViewer

con la function item_view andremo a creare una pop-up in flex per l'editing o l'aggiunta di un record tramite la classe statica PopUpManager che istanzia una classe ItemEditor, il cui file corrispondente si trova in prj.ItemEditor:
/**
 * @author jaco
 * created on 07/mar/07
 */

package prj {

	import mx.containers.*;
	...


	public class ItemEditor extends Panel {

		public var btClose:Button;
		public var btSave:Button;

		private var currentID:Number = -1;
		public var roles:Array = [ {label:"guest", data:"guest"},
                				   {label:"user", data:"user"},
                				   {label:"admin", data:"admin"} ]


		private var fiName:FormItem;
		...

		private var lbName:TextInput;
		...

		function  ItemEditor(){ setup_layout(); }


		private function setup_layout():void {

			setStyle("backgroundColor", "#999999");
			...

			width = 300;
			height = 200;

			horizontalScrollPolicy = "off";
			verticalScrollPolicy = "off";

			var contentBox:VBox = new VBox();
			contentBox.setStyle("borderSides", "10");
			...

			var topSpacer:Spacer = new Spacer();
			topSpacer.height = 5;

			var lb:Label = new Label();
			lb.setStyle("fontWeight", "bold");
			lb.setStyle("paddingLeft", "10");
			lb.text = "Create/Edit item";

			var f:Form = new Form();

			fiName = new FormItem();
			fiName.label = "name";
			fiName.required = true;

			fiSurname = new FormItem();
			...

			fiRole = new FormItem();
			fiRole.label = "role";

			lbName = new TextInput();
			lbSurname = new TextInput();
			cbRole = new ComboBox();
			cbRole.dataProvider = roles;

			var cb:ControlBar = new ControlBar();
			cb.percentWidth = 100;
			cb.setStyle("backgroundColor", "#999999");

			var spacer:Spacer = new Spacer();
			spacer.percentWidth = 100;

			btSave = new Button();
			btSave.label = "save";

			btClose = new Button();
			btClose.label = "close";

			cb.addChild(spacer);
			cb.addChild(btSave);
			cb.addChild(btClose);

			fiName.addChild(lbName);
			fiSurname.addChild(lbSurname);
			fiRole.addChild(cbRole);

			f.addChild(fiName);
			f.addChild(fiSurname);
			f.addChild(fiRole);

			contentBox.addChild(topSpacer);
			contentBox.addChild(lb);
			contentBox.addChild(f);
			addChild(contentBox);
			addChild(cb);

			setup_textInput_events();
		}


		private function setup_textInput_events():void{
			lbName.addEventListener(KeyboardEvent.KEY_UP, this.check_form);
			lbSurname.addEventListener(KeyboardEvent.KEY_UP, this.check_form);
		}


		private function check_form(evt:KeyboardEvent):void{
			var sName:String = lbName.text;
			var sSurname:String = lbSurname.text;

			if(sName.length > 2 && sSurname.length > 2){
				btSave.visible = true;
			}
			else btSave.visible = false;
		}

		private function handleInvalid(evt:ValidationResultEvent):void {
			btClose.visible = false;
		}

		public function set_itemData(itemData:Object):void{

			var roleOrder:Array = ["guest", "user", "admin"];

			if(!itemData) btSave.visible = false;
			else {
				lbName.text = itemData.sName;
				lbSurname.text = itemData.sSurname;
				currentID = itemData.id;
				var roLength:int = roleOrder.length;

				for(var i:int = 0; i < roLength; i++){
					if(roleOrder[i] == itemData.eRole){
						cbRole.selectedIndex = i;
						break;
					}
				}
			}
		}

		public function get_itemData():Object {
			var itemData:Object = new Object();
			itemData.id = currentID;
			itemData.sName = lbName.text;
			itemData.sSurname = lbSurname.text;
			itemData.eRole = cbRole.selectedItem.data;

			return itemData;
		}
	}
}
Curiosamente questo file contiene piu' codice di quello principale, alcune parti ripetitive sono state tagliate, fate sempre riferimento al codice sorgente.
E' un esempio di componente creato in solo Actionscript, ho scelto di farlo in questo modo a scopo dimostrativo.
Diversamente dal codice flex, ogni elemento viene creato, istanziato e inizializzato run-time, perfino gli aspetti relativi allo stile vengono settati in quel modo. Questo component si limita a controllare che i campi "name" e "surname" siano popolati con almeno tre lettere per poter salvare il record.
Sempre attraverso PopupManager andremo a chiudere la pop quando i pulsanti close e save verranno cliccati. I metodi invocati dai gestori d'evento sui pulsanti sono presente nel codice principale, per tale motivo sono stati dichiarati public.

Compilazione

in flex2sdk e' sufficiente aprire una console/terminale/dosbox/quella-che-e' e invocare mxmlc, spostarsi nella directory src:
mxmlc FlexGridTest.mxml -output ../deploy/FlexGridTest.swf
Se si riceve un errore sulla mancanza di mxmlc e' chiaro che questi manca nel path environment, in tal caso o si chiama mxmlc esplicitamente col percorso di installazione nel proprio file system o si aggiunge il path nelle variabili d'ambiente, chiaramente la seconda scelta e' assai piu' comoda.
Se tutto e' in regola si otterra' FlexGridTest.swf di circa 250kb, tale peso e' dovuto al framework flex e solo in minima parte al codice e ai contributi utilizzati da noi, e' questo un prezzo che si paga per avere un'applicazione pronta per la pubblicazione.

Il backend

Ora che abbiamo terminato il lavoro a livello di flex, avremo bisogno di un'installazione valida di amfphp, per questa fase rimando alla documentazione e ai vari tutorial sull'argomento. Noi andremo ad integrare quattro file php, due dei quali sono situati in deploy/lib:

config.inc.php contiene alcuni settaggi per il backend, per l'accesso al db e i path degli script db_utils.php contiene alcuni wrapper d'accesso al db e al lancio delle query.
ItemsManager.php andra' nella directory services di amfphp insieme a ItemsManager.methodTable.php, questi costituiscono l'interfaccia tra flex e il server.

I metodi esposti da ItemsManager sono essenzialmente tre e lavorano tutti sull'unica tabella oggetto di questo tutorial, users: Siamo prossimi al termine del tutorial, completano l'applicazione alcuni file per la distribuzione in internet:

Hardening

Finora per ovvi motivi ci si e' limitati al prototipo dell'applicazione, e' chiaro che in un ambiente di produzione reale e' necessario prendersi cura dei vari aspetti legati alla sicurezza, la gestione degli errori etc. P.e. si puo' implementare una forma di validazione coi le RegExpValidator, che permette di imporre dei vincoli efficaci ai campi di input contro i tentativi di sql injection. Stesso discorso lato server/amfphp che puo' avvalersi di appositi metodi come mysql_real_escape_string e naturalmente le espressioni regolari. Un esempio on-line dell'applicazione lo trovate qui
qui il codice sorgente.

03.2k7

:)
jaco_at_pixeldump