My First Flex App
From BriansWiki
Here's the details on what I learned creating my first Flex based application. The goal was to use a ColdFusion CFC as the backend data handler and serve up XML for a Flex UI. Send me an email to let me know what you think or if you find any really stupid stuff:
Thanks,
Contents |
[edit] Code
For those of you who want to cut to the chase, the final code is located here:.
NOTE: This code is dependent on SQLServer data so it won't run unless you change these references to something local to you.
[edit] Requirements
Allow users to search automated collection records (IVR), display the results in a hierarchical tree control and show the details for any selected level in the hierarchy. We also needed a report to document the search results.
[edit] Layout
I went for a 3 column VBox layout with search, tree and grid controls side by side:
[edit] Screen Shots
[edit] Data
[edit] Back End
For the backend, we have a MS SQL table called IVRStatus that contains all the raw data; I used the create CFC wizard in Flex Builder to create gateway and DAO objects as well as a action script class to bind to.
In the gateway CFC, I added 3 functions:
[edit] GetCollTypes
Once the user has entered a start and end date, we use this method to query the IVRStatus table to get all the unique collection types for that period. The data is formatted using Ray Camden’s toXML utility CFC which accepts a cfQuery object and returns xml with parent and child node names of your choosing, like this:
<cfset colTypesXML = toXML.queryToXML(qColTypes, "dataset", "row")>
[edit] GetIVRStatusMX
This function is used to return the Search results as an xml list for the tree Control to load. It follows this format:
<folder type=‘#callType#’ hour=‘#callHour#’ status=‘#status#’ result=‘#result#’ label=‘#value#’ />
In this xml, all the details are in the attributes and every node is called "folder". This kind of annoyed me since it is not very readable, but it was the only way I got the tree control to load properly. If someone knows a better way, please let me know.
[edit] GetIVRDetails
This gets the complete records for a given position in the tree as indicated by the nodes attributes. It also uses the toXML functions described above.
[edit] Front End
[edit] Remote Object
The remote Object is the link from the UI to the backend, as you can see; this is where we define the gateway CFC and all the methods we want to use:
<mx:RemoteObject
id="myService"
destination="ColdFusion"
source="FlexDataTest.IVRStatusGateway"
showBusyCursor="true">
<mx:method name="getIVRDetails" result="detailsXMLHandler(event)" fault="Alert.show(event.fault.message)"/>
<mx:method name="getIVRStatusMX" result="callsXMLHandler(event)" fault="Alert.show(event.fault.message)"/>
<mx:method name="getColTypes" result="colTypesXMLHandler(event)" fault="Alert.show(event.fault.message)"/>
</mx:RemoteObject>
[edit] Private Data Objects
In the main application I created 3 bindable (which means that they can be used as data providers for FLEX UI elements) XML objects and related handler functions:
[Bindable]
private var xmlColTypes:XML;
[Bindable]
private var xmlDetails:XML;
[Bindable]
private var xmlTreeList:XMLList;
public function colTypesXMLHandler(event:ResultEvent):void {
xmlColTypes = new XML(event.result);
}
public function detailsXMLHandler(event:ResultEvent):void {
xmlDetails = new XML(event.result);
}
private function callsXMLHandler(event:ResultEvent):void {
xmlTreeList = event.result.folder;
}
[edit] Data Casting
For the list of collection types and the details view, the results from the gateway are cast into a new XML document, I think this is due to the fact that the cfc doesn’t return a valid XML document (as far as Action Script is concerned) so we have to make one from the ColdFusion collection object.
The Tree Control on the other hand wants an XML list rather than a document. I figured this out by trial and error; the documentation says that you can use an XML document, but I could never get it to work.
The other trick is to set the tree data provider explicitly to the "folder" nodes collection:
xmlTreeList = event.result.folder;
The display text for a node is in the "label" attribute, the other attributes are not displayed but they are used to define the position in the tree so that we can do a details query at any level. The icon is also set automatically to a folder if a node has children, so we don’t have to worry about that.
[edit] Events
There are 4 events that we handle in the UI:
[edit] End Date text change
The general approach is to reference an event handler function in the change event of the UI element like this:
<mx:DateField id="endDate" change="getColTypes()"/>
The event handler function is where we do error checking on inputs and build the variables to send to the Remote Object methods:
private function getColTypes():void
{
var start:String="";
var end:String="";
if(startDate.text != ""){
start = startDate.text;
}
if(endDate.text != ""){
end = endDate.text;
}
myService.getColTypes(start,end);
}
[edit] Get Calls button click
This is similar to the end Date change except we use the click event:
<mx:Button label="Get Calls" click="getIVRStatus(event)" />
private function getIVRStatus(event:Event):void
{
var start:String="";
var end:String="";
var colType:String="";
if(startDate.text != ""){
start = startDate.text;
}
if(endDate.text != ""){
end = endDate.text;
}
if(cmbColType.text == "All"){
colType = "coll%";
} else {
colType = cmbColType.text
}
myService.getIVRStatusMX(start,end,colType);
}
[edit] Call Tree node click
The Tree node has a change event that is essentially the node click event. The setup for the tree is easy once you give it an XML list with the correct structure. Notice the "labelField" property is set to "@label"; this is the attribute that contains the display text for a tree node.
<mx:Tree
showRoot="false"
id="callTree"
dataProvider="{xmlTreeList}"
labelField="@label"
enabled="true" height="399" width="180"
change="populateGrid(event)" >
</mx:Tree>
The populate grid function looks at the current node and gets the attributes that define the position:
private function populateGrid(event:Event):void
{
var node:XML=event.target.selectedItem;
var start:String="";
var end:String="";
var type:String="";
var hour:int=0;
var status:String="";
var result:String="";
if(startDate.text != ""){
start = startDate.text;
}
if(endDate.text != ""){
end = endDate.text;
}
if(node.@type != undefined){
type = node.@type;
}
if(node.@hour != undefined){
hour = node.@hour;
}
if(node.@status != undefined){
status = node.@status;
}
if(node.@result != undefined){
result = node.@result;
}
myService.getIVRDetails(start,end,type,hour,status,result);
}
[edit] Report button click
The report button is different in the sense that is not passing a call to the Remote Object, instead it is creating another window and displaying a CF Report Builder (.cfr) report in it. This task was made relatively easy by the use of the "ColdFusion/Flex Application Wizard" that creates a new Flex application that displays an existing CF Report Builder report (the trick is when you add a page to the application in the Wizard, select the "ColdFusion FlashPaper Report/Document" page type from the dropdown in the Page Information group). After I created the report project, I pulled out the create window function and any dependent objects and copied them to the IVR Status application.
The most confusing parts of this process was reconciling the different application layouts; my app has no Object Factory or Controller, but the Report Wizard does. I resolved this by moving the package that defines the dynamic window manager to the root level of the simple IVR Status application. I know it would be more proper to change IVR status to the Report Builder Wizard architecture, but as a first app I wanted to keep things relatively simple.
Practically speaking all I had to do was move the "src.com" folder to the root level like this:
<root> <assets> <bin> <com> <html-template> </root>
I also removed any extraneous classes and folders to simplify the app.
The button click event looks like this:
<mx:Button label="Report" click="WindowManager.add(getIVR_Status_Report(event), this, false);" />
The getIVR_Status_Report function is like the other event handlers in that it does the validation of input parameters, but it is different in that it creates and passes a value object (VO) to the IVR_Status_Report function which contains the search parameters.
For this call, all our objects are in Action Script so there is no hassle passing the object around. I would like to use this approach with the Remote Object too, but that is left as an exercise for the reader :).
private function getIVR_Status_Report(event:Event):UIComponent
{
var start:String="";
var end:String="";
var colType:String="";
var searchVars : SearchVO = new SearchVO();
if(startDate.text != ""){
start = startDate.text;
}
if(endDate.text != ""){
end = endDate.text;
}
if(cmbColType.text == "All"){
colType = "coll%";
} else {
colType = cmbColType.text
}
searchVars.startDate = start;
searchVars.endDate = end;
searchVars.colType = colType;
var view:IVR_Status_Report = new IVR_Status_Report();
view.setSearchVars(searchVars);
return view;
}
[edit] Report Design
Besides all the grouping and totaling going on, the report design is pretty straight forward. The major difference is that we are going to need to rely on the default query since I don't know how to create and pass a cfquery object from Flex to CF. What we can do is to pass parameters by the URL, so in the report definition create 3 input parameters and then reference them in the query, like this:
SELECT LEFT(CallType, 11) as callType, DATEPART(hour, [DateTime]) as callHour, Status, Result, Count(TN) as Calls FROM IVRStatus WHERE (CallType LIKE ‘#param.colType#’) AND DATETIME >= ‘#param.startDate#’ AND DATETIME <= ‘#param.endDate#’ group by callType, DATEPART(hour, [DateTime]), status, result order by callType, DATEPART(hour, [DateTime]), status, result
Then when the new IVR_Status_Report object is created I set the query values into the new "setSearchVars" public method:
public function setSearchVars(searchVars:SearchVO):void
{
startDate = searchVars.startDate
endDate = searchVars.endDate
colType = searchVars.colType
cmd = "http://dev3.aroundcny.com/programs/IVROutbound/reports/ivrStatusReport.cfr?startDate="+startDate+"&endDate="+endDate+"&colType="+colType;
}
Finally, the the function that creates the report looks like this:
<cfComponents:FlashPaperLoader
id="flashpaperWrapper"
instanceid="{new Date().getTime()}"
wrapperUrl="/CFIDE/scripts/fpwrapper.swf"
reportUrl = "{cmd}"
width="100%" height="100%" />
The last trick here is that the cmd is declared as "bindable" so I can use the concatenated "cmd" string in FlashPaperLoader function call.
[edit] Moving to production
Checkout this page for issues on moving Flex apps
