this content element requires the Flash 7.0 player. You can download it here
Part II: The AS code
written by: Seth Hillinger aka shi11
CakePHP->AMF(action messaging format)->Flash. AMFPHP is a missing link between PHP and Flash using Remote Objects. Without Patrick Mineault and his teams efforts there would be no open source AMFPHP available. Then combine that with an open source, MVC CakePHP framework using AMFPHP, and we are breaking down the XML barrier. This is a beautiful example of the DRY philosophy. No more serializing/deserializing. No longer will PHP need to convert it's object types to XML so that Flash can convert the XML back to it's AS2 type.
This example is an extension of the fine actionscript.org amfphp tutorial by Jesse Stratford. If you want to learn more, Jesse does a great job of explaining AMFPHP more indepth. And of course there's also amfphp.org
Getting Flash Setup:
Unpack Flash files to the CakeAMFPHP directory.
We should have the following structure:
The Code
Since this code is an extension of Jesse's tutorial I will highlight my changes and briefly cover their method functionality. This tutorial assumes you understand advanced AS2 code and already have understanding of AMFPHP although this may be your first time using it. There are two things to really take note of below: the reference to the cake_gateway.php file and the SERVICE_NAME. As you learned in part I, the gateway file is a modified gateway made specifically for cake. The SERVICE_NAME refers to the name of the PHP Class which you also created on the previous page.
//Import Remoting classes
import mx.remoting.*;
import mx.remoting.debug.*;
import mx.rpc.*;
//The helpful Delegate for UI component event listeners
import mx.utils.Delegate;
//For customizing the datagrid columns
import mx.controls.gridclasses.DataGridColumn;
//For data verification
import mx.controls.Alert;
class BulletinBoardsController
{
//A reference to out root movie
var root:MovieClip;
//The offset the currently viewed post
var offset:Number = 0;
//The location of the gateway and the name of the service
//static var GATEWAY_URL:String = "http://localhost/CakeAMFPHP/cake_gateway.php"
//public service instead
static var GATEWAY_URL:String = "http://yoursite.com/CakeAMFPHP/cake_gateway.php"
//Name of Service
static var SERVICE_NAME:String = "BulletinBoardsController"
//Max number of posts per request
static var MESSAGES_PER_PAGE:Number = 10;
//The service reference
var service:Service;
function BulletinBoardsController(root:MovieClip)
{
//Remember the root
this.root = root;
//Start doing the magic
init();
}
}
Next, in the init method below, initialize the NetDebugger.
The NetConnection debugger is located on
On Windows at C:\Documents and Settings\{Your username}\LocalSettings\Application Data\Macromedia\Flash {version}\{language}\configuration\WindowSWF\NetConnection Debugger.swf.
On OS X it should be in: /Users/{username}/Library/Application Support/Macromedia/Flash {version}/{language}/Configuration/WindowSWF\NetConnection Debugger.swf.
Jesse says*: since it's a very useful tool, you'll probably want to make a shortcut on your desktop to it so you'll have it handy whenever you need it.
amfPHP call and response
Following init() are the two workhorse Controller functions. Read() and Post(). Think of Cake as your Model. These Controller methods interact with the Model. service.amfRead calls the Cake BulletinBoardsController.php amfRead() function and service.amfCreate references it's Cake twin. Following the service calls is a RelayResponder to handle the results.
///////////////////////////////////////////////////////////////////////////
// Remoting-related methods (interesting stuff)
///////////////////////////////////////////////////////////////////////////
//This function creates the service, calls Read and waits for results
function init()
{
//Start the NetConnection debugger
NetDebug.initialize();
//Create the service
service = new Service(GATEWAY_URL, null, SERVICE_NAME);
//Read the first set of posts
Read(int(0));
//Make the UI work (boring)
initUI();
}
//Ask the remote server to read some posts
function Read(start:Number)
{
//arg1: message start, arg2: message offset
var pc:PendingCall = service.amfRead(start, MESSAGES_PER_PAGE);
pc.responder = new RelayResponder(this, 'handleRead', null);
//Remember the offset
offset = start;
}
//Ask the remote server to insert a post
function Post(message:Object,user:Object)
{
var pc:PendingCall = service.amfCreate(message,user);
pc.responder = new RelayResponder(this, 'handlePost', 'handleRemotingError');
}
Handling the results
Pretty self explanatory here. The response from CakeAMFPHP triggers one of the following. If the amfRead response has no errors, it binds the datagrid to the ResultEvents results. If a post is successful it hides the form and reloads the datagrid. If there are errors, it throws a trace to the NetDebugger.
//Get data back from server, show the post
function handleRead(re:ResultEvent)
{
//Step 1: Take the returned RecordSet, and bind it to the datagrid
root.readMessages.dgMessages.dataProvider = re.result;
//There is no step 2.... Mouhahah!
NetDebug.trace(re.result.length);//use this for testing
//Reset the selectedIndex of the datagrid to 0
root.readMessages.dgMessages.selectedIndex = 0;
//Fake as though the datagrid was clicked
onDgChange({target:root.readMessages.dgMessages});
//Enable/disable next and previous buttons
root.readMessages.btnPrev.enabled = offset > 0;
root.readMessages.btnNext.enabled = re.result.length == MESSAGES_PER_PAGE;
}
//Insert request was answered, hide the post interface
function handlePost(re:ResultEvent)
{
resetPost(); //Clear for next time
hidePost(); //Hide interface
//Refresh posts
Read(0);
}
// need to test this function. taken from drswank.
function handleRemotingError( fault:FaultEvent ):Void
{
NetDebug.trace({level:"None", message:"Error: " + fault.fault.faultstring });
}
The Contoller Events
When the datagrid changes it calls onDgChange() and fills in the textArea with Cake ModelName fields using modelName_fieldname syntax. onPost sends a message and user object to Post().
//Called when datagrid is clicked. Inspect the data and show
//it in the textarea below
function onDgChange(evtObj:Object)
{
var datagrid = evtObj.target;
var data:Object = datagrid.dataProvider.getItemAt(datagrid.selectedIndex);
if(data == null)
{
root.readMessages.txtMessage.text = ""; //If empty, clear textarea
}
else
{
//Fill in the textarea with the data return from the CakeAMFPHP service
//The data in the recordset has the ModelName underscore field(ie: BulletinBoard_field). This allows some Cake associations to work. Let us know if you get dot syntax to work.
root.readMessages.txtMessage.text =
'' + data.BulletinBoard_message + '
----------------------------------------
' +
'Posted by ' + data.User_name + '' +
' of ' + data.BulletinBoard_url + '' +
' at '+data.BulletinBoard_created + '';
}
}
//Submit a post to the remote server
function onPost(evtObj:Object)
{
//Gather info from UI
var message = new Object();
var user = new Object();
user.name = root.postMessages.txtName.text;
message.email = root.postMessages.txtEmail.text;
message.url = root.postMessages.txtUrl.text;
message.message = root.postMessages.txtMessage.text;
//Check if data was input
if(user.name == '' || message.email == '')
{
Alert.show('Please input name and email', 'Validation Error', Alert.OK);
return;
}
//Now submit
Post(message, user);
}
///////////////////////////////////////////////////////////////////////////
// UI-related methods (not-so interesting stuff)
///////////////////////////////////////////////////////////////////////////
function onPrev()
{
Read(offset - MESSAGES_PER_PAGE); //Read the previous set of posts
}
function onNext()
{
Read(offset + MESSAGES_PER_PAGE); //Read the next set of posts
}
function onRefresh()
{
Read(offset); //Refresh the UI
}
function onNewPost()
{
root.postMessages._visible = true; //Make the post interface appear
}
function onReset()
{
resetPost(); //Reset the post interface
}
function onCancel()
{
hidePost(); //Hide the post interface on cancel
}
Finally, the View
Although Jesse has this in one file, I would like to separate the following code into a BulletinBoardView.
function initUI()
{
//Hide the insert insterface
root.postMessages._visible = false;
//Add an event listener so clicking the datagrid will show the
//message in the textarea below
root.readMessages.dgMessages.addEventListener('change', Delegate.create(this, onDgChange) );
//Add various button click handlers
root.readMessages.btnPrev.addEventListener('click', Delegate.create(this, onPrev));
root.readMessages.btnNext.addEventListener('click', Delegate.create(this, onNext));
root.readMessages.btnRefresh.addEventListener('click', Delegate.create(this, onRefresh));
root.readMessages.btnNew.addEventListener('click', Delegate.create(this, onNewPost));
//Same for the post interface
root.postMessages.btnPost.addEventListener('click', Delegate.create(this, onPost));
root.postMessages.btnReset.addEventListener('click', Delegate.create(this, onReset));
root.postMessages.btnCancel.addEventListener('click', Delegate.create(this, onCancel));
//Stop clicks from going through background
root.postMessages.background.onPress = Delegate.create(this, null);
root.postMessages.background.useHandCursor = false;
//Set some styles
_global.style.setStyle('themeColor', 'haloBlue');
_global.style.setStyle('fontFamily', 'Verdana');
_global.style.setStyle('fontSize', 10);
//Set the columns for the datagrid // yo, down here.ok
var dgc:DataGridColumn = new DataGridColumn();
dgc.headerText = 'Posted';
dgc.columnName = 'BulletinBoard_created';//data return from CakeAMFPHP recordset: notice the ModelName and underscore
dgc.width = 140;
root.readMessages.dgMessages.addColumn(dgc);
var dgc:DataGridColumn = new DataGridColumn();
dgc.headerText = 'Author';
dgc.columnName = 'User_name';//data return from CakeAMFPHP recordset: notice the ModelName and underscore
dgc.width = 120;
root.readMessages.dgMessages.addColumn(dgc);
var dgc:DataGridColumn = new DataGridColumn();
dgc.headerText = 'Snippet';
dgc.columnName = 'BulletinBoard_message';//data return from CakeAMFPHP recordset: notice the ModelName and underscore
dgc.width = 240;
root.readMessages.dgMessages.addColumn(dgc);
root.readMessages.dgMessages.setStyle('hGridLines', true);
root.readMessages.dgMessages.setStyle('hGridLineColor', 0xdddddd);
//Set the message text field to html mode, set the font
var styles:TextField.StyleSheet = new TextField.StyleSheet();
styles.setStyle("html", {fontFamily:"Verdana,Arial,Helvetica,sans-serif", fontSize:"11px"});
root.readMessages.txtMessage.html = true;
root.readMessages.txtMessage.styleSheet = styles;
}
//Clear the post interface fields
function resetPost()
{
root.postMessages.txtName.text = "";
root.postMessages.txtEmail.text = "";
root.postMessages.txtUrl.text = "http://";
root.postMessages.txtMessage.text = "";
}
//Hide the post interface
function hidePost()
{
root.postMessages._visible = false;
}
}
Ok, that is it. When testing the swf make sure to export it to the app/webroot/swfs directory. Once again, big ups to Jesse and Patrick for pulling out their creative machetes and clearing a path for us to follow. If Bob Ross were a programmer, I think at this point he'd say something like: Happy Collaborating. -Seth
- Part 1: Getting Setup
- Part 2: The Cake Code
