Exchanging an Array of Objects Between CF and Flex and Updating Objects in a DataGrid
In a previous blog entry I discussed how to exchange user defined objects between CF and Flex. To continue with that theme let's examine how we can send an array containing Person objects from CF to Flex, display each Person object's fields in a DataGrid, edit the fields in the DataGrid, and then send back to CF each Person object so CF can update that record in the database.
I'm using the same Person CFC, Person DAO, and Person ActionScript class I used in my Flex application example for the previous blog entry about exchanging user defined objects between CF and Flex. What I've added for this example Flex application is a PersonGateway object that has one method--findByZipCode. This method finds each record that matches the zip code provided as an argument, creates a Person object using the record's column values, adds the Person object to an array, and then returns the array.
<cfcomponent displayname="PersonGateway" output="false" hint="Person Gateway">
<!---document each instance variable--->
<cfproperty name="dsn" displayname="Datasource Name"
hint="Datasource name used to connect CF to the database"
type="string">
<!---pseudo constructor--->
<cfscript>
variables.dsn = "BrucePresenter" ;
</cfscript>
<cffunction name="init" access="public" output="false" returntype="PersonDAO" hint="Constructor for this CFC">
<!--- take in the datasource name as an argument and set it to the variables scope so it's available
throughout the CFC --->
<cfargument name="dsn" type="string" required="true" />
<cfset variables.dsn = arguments.dsn />
<!--- return the object itself --->
<cfreturn this />
</cffunction>
<!--- FindByZip: finds people in the zip code passed as an argument--->
<cffunction name="findByZip" access="remote" output="false" returntype="array"
hint="Finds the people with matching zip code, creates a Person object for each one, and
places the Person object into an array. ">
<cfargument name="aZIP" type="string" required="true" />
<!--- var scope everything! --->
<cfset var getPeople = "" />
<cfset var peopleAry = ArrayNew(1)>
<cfquery name="getPeople" datasource="#variables.dsn#">
SELECT TOP 10 [presenterID] as personID, lastName, firstName, phone
FROM presenters
WHERE zip = <cfqueryparam value="#arguments.aZIP#" cfsqltype="cf_sql_varchar" />
</cfquery>
<cfif getPeople.recordcount EQ 0>
<cfthrow type="Person.NotFound" message="I didnt find any people with a zip of: #arguments.aZIP#">
</cfif>
<cfoutput query="getPeople">
<!---update the state of the person object--->
<cfscript>
aPerson = createObject("Component", "cfobjectdatagrid.Person");
aPerson.setPersonID( personID );
aPerson.setLastName( lastName ) ;
aPerson.setFirstName( firstName ) ;
aPerson.setPhoneNumber( phone ) ;
</cfscript>
<cfset ArrayAppend(peopleAry,aPerson)>
</cfoutput>
<cfreturn peopleAry >
</cffunction><!---end read method--->
</cfcomponent>
In my Flex example application (right click on the Flex app to view source), I call the findByZip method. Function handleFindResult is called when the findByZip method returns the array. Inside this function I first assign the result (cast to an Array) to an Array variable. Then I use this Array to create an ArrayCollection object (called peopleAryCol).
Since I've mapped my ActionScript Person class to the CFC Person equivalent, Flex understands that the objects stored in the ArrayCollection are of type Person. The peopleAryCol is bound as the data provider for the DataGrid component. So when the peopleAryCol is created in the handleFindResult method, the DataGrid creates a row for each Person object stored in the ArrayCollection. I've specified the DataGridColumns for the DataGrid so that I could properly name the column header and also because I did not want to expose the personID field to the user.
Note that I've made the DataGrid editable (the default for editable is false). By doing this, the user can click on any field in the DataGrid and change it. I've not specified a specific ItemEditor component so the default text input component is used, which works for this example. If you want to learn more about using a DataGrid and ItemEditors consult chapter 22 in the Flex Developer's Guide.
When the user changes any field, Flex automatically updates the field and the corresponding field in the Person object used to populate that row. Unbelievable, the program designer doesn't need to do anything special to get the user's changes into the Person objects. I had not used a DataGrid for editing before and this feature just made me go WOW! The time and amount of code this feature saved me from developing is just another reason I love using Flex.
After the user makes their changes, she clicks on the Update button. This calls the update function. In this function I loop through each Person object in the peopleAryCol and send that Person object to the update function in my PersonDAO service. The update function in the PersonDAO CFC will use the state of the Person object passed to it to update the specific record in the database table.
There are a couple of areas I want to explore further and improve my code. Currently, my update function in the Flex app, loops through every Person object in the ArrayCollection and sends each Person object to the update function in my PersonDAO CFC. This seems wasteful; for example, if I'm displaying 30 Persons in my DataGrid and the user changes 2 of them, I still send all 30 Person objects back to CF one at a time. I need to research to find out if I can determine which specific Person object's have changed (I know I could hard code this check by cloning the ArrayCollection before the user makes any changes and then comparing the Person object in the ArrayCollection that is the DataProvider to the Person object that is in the clone). I may also refactor this to only call the update function after a specific field has been changed (I know there is an event triggered when the user moves out of a field after changing it).
I'd be interested in any examples you may have that show other ways to use the DataGrid for editing and passing those edits back to CF to update the database.
I am especially interested in the looping mechanism you use in the CF end to update all the returning records.
Thanks.
As to your question, the function update() in the MXML file is used to loop over every object in the peopleAryCol (the data provider for the data grid) and send each object to an update method in the PersonDAO CFC. The looping is not done in CF.
Here is a bit of the code, maybe you can give me a hand.
Thanks
<cffunction name="logmein" access="remote" output="false" returntype="array">
<cfargument name="username" default="1" >
<cfargument name="password" default="1" >
<cfset var rsUser = "">
<cfset var arrayUser = ArrayNew(1)>
<cfquery name="rsUser" datasource="crv">
Select * from tblLogin Where Username = #arguments.username# and Password = #arguments.password#
</cfquery>
<cfif rsUser.recordcount neq 1>
<cfthrow type="myLogin.Status" message="Invalid">
</cfif>
<cfif rsUser.Access eq 0>
<cfthrow type="myLogin.Status" message="Denied">
</cfif>
<cfoutput query="rsUser">
<cfscript>
aUser = createObject("Component", "test._models.myLogin");
aUser.setUserID(UserID);
aUser.setCompanyID(CompanyID);
aUser.setCompany(Company);
aUser.setLocationID(LocationID);
aUser.setLocation(Location);
aUser.setUsername(Username);
aUser.setPassword(Password);
aUser.setFirstName(FirstName);
aUser.setLastName(LastName);
aUser.setStatus(Status);
</cfscript>
<cfset ArrayAppend(arrayUser,aUser)>
</cfoutput>
<cfreturn arrayUser>
</cffunction>
The error I get is "Component names cannot start or end with dots"
I guess the basic idea is once a match is found (valid username and password combination) then send an object packaging the user info to perpetuate in the application (replacing the job of session variables, if you will). In the case an account is not found then let Flex know that and finally in the case an account has been found but has been denied access (a database field boolean = 0) then let Flex know that as well as to handle and customize a different response (state) to the user.
At least that is what I was trying to accomplish.
Thanks for your help.
Second if you can use the CFC method in a CF template, but not in Flex, check the RemoteObject tag in Flex.
Lastly, I don't think Flex can interpret Exceptions thrown by CF. All Flex will do in my experience is show an an Alert Message with the exception's text.
I've not used Flex as the front end for a user login system yet. I recommend you research this in mxna and in google as I vaguely recall seeing a post about how to do this.
http://www.markdrew.co.uk/blog/index.cfm/2006/11/2...
Mark demonstrates that creating thousands of Objects in CF is a very time consuming process when compared to just creating Structures or even plain Java Objects.
If you need to exchange large amounts of data between CF and Flex, creating the objects in CF may not be fast enough.
This is great! Thank you very much for showing us how to do all this stuff with Flex!
I have been able to get my user objects into my datagrid now. However, I am having trouble creating a custom event and dispatching an event that will populate a user's record in a form by clicking on a record in the datagrid. The datagrid and the form are in two separate child components. I am trying to use event bubbling without any success. Would it be possible for you to create a tutorial on this?
Thanks again!
James
Read through my series of tutorials on creating a login system with flex. The loginform.mxml component uses a custom event and also exposes a property that a another component may access.
I've related the selectedItem in a datagrid to a form's controls but both were in the same component. I've not done that when both were in separate components. I think in the datagrid component you would need to expose a property that was bound to the datagrid's selectedItem.
I actually found your site by searching for Flex login tutorials! Great tutorial! I took your tutorial and started building off it. I tried creating a custom event class based on your Member class:
package events
{
import flash.events.Event;
import Member;
public class MemberEvent extends Event
{
public var aMember:Member;
public function MemberEvent(member:Member, type:String, bubbles:Boolean){
super(type, bubbles);
aMember = member;
}
public override function clone():Event{
return new MemberEvent(aMember, type, bubbles);
}
}
}
This is my datagrid in my RecordList component:
<mx:DataGrid id="membersList" dataProvider="{memberAryCol}" height="100%" width="100%" change="processRecordSelection()">
<mx:columns>
<mx:DataGridColumn dataField="lastName" headerText="Last Name" />
<mx:DataGridColumn dataField="firstName" headerText="First Name" />
<mx:DataGridColumn dataField="userName" headerText="User Name" />
<mx:DataGridColumn dataField="password" headerText="Password" />
<mx:DataGridColumn dataField="email" headerText="Email" />
</mx:columns>
</mx:DataGrid>
This is my function that gets called when a user clicks on a record in the datagrid:
public function processRecordSelection():void {
var o:MemberEvent = new MemberEvent(membersList.selectedItem, "dgSelectedMember", true);
dispatchEvent(o);
}
My event:
<mx:Metadata>
[Event(name="dgSelectedMember", type="events.MemberEvent")]
</mx:Metadata>
I get a compile error saying that my datatypes don't match (Member and Object). So then I changed my custom event class to this:
package events
{
import flash.events.Event;
public class MemberEvent extends Event
{
public var aMember:Object;
public function MemberEvent(member:Object, type:String, bubbles:Boolean){
super(type, bubbles);
aMember = member;
}
public override function clone():Event{
return new MemberEvent(aMember, type, bubbles);
}
}
}
There where no errors using this, but my signal was not received in the UpdateMember component with the form. I added a listener in this component:
public function init():void {
parent.addEventListener( "dgSelectedMember", receiveDGResultHandler );
}
private function receiveDGResultHandler():void {
Alert.show('Event signal from datagrid received!');
}
Any suggestions would be greatly appreciated! :)
James
when the application receives the event, it could then call an exposed set method in the updateMember component and pass it the datagrid's selectedItem object.
The set method in the updateMember child could be bound to the form controls.
So I see something like this:
User selects a row in the datagrid - event generated - event received by the application - application calls a set method in the updatemember child and passes it the selectedItem object.
I've not done much work with bubbling events. There is some information about bubbling events in the Rich Internet Applications With Adobe Flex and Java book I'm reading now. When I get more time, I'll check it out and let you know what I find out.
You may also want to post your question on flexcoders yahoo group.
I was able to get event bubbling to work between to child components using "parent.addEventListener( "dgSelectedMember", receiveDGResultHandler);" in the receiving child component. However, it only worked when dispatching a standard event. When I tried using my custom event class shown above, no signal was received? So far there has been no response from flexcoders or Adobe's support forums, but I'm going to keep trying.
I don't think you need a custom event. You could use a standard event and just expose as a property an Object that represents the selectedItem from the DataGrid. In the main application you can bind another Object to that exposed property and bind an exposed property for your custom form component to that main application Object.
I got it to work based on your "Exposing Properties and Events When Creating A Custom Component In Flex" tutorial. I tried to paste only the code pertaining to this topic ...
My RECORDLIST component (datagrid):
Metadata:
<mx:Metadata>
[Event(name="dgSelectedMember", type="flash.events.Event")]
</mx:Metadata>
Script:
[Bindable]
private var _record:Member;
//public function to get and set
//_record Object
public function get record():Member {
return _record;
}
public function set record(record:Member):void {
_record = record;
}
public function processRecordSelection():void {
//update _record Object with current
//values in the datagrid
_record = membersList.selectedItem as Member;
dispatchEvent(new Event("dgSelectedMember"));
}
Datagrid:
<mx:DataGrid id="membersList" dataProvider="{memberAryCol}" height="100%" width="100%" change="processRecordSelection()">
<mx:columns>
<mx:DataGridColumn dataField="memberID" headerText="Member ID" />
<mx:DataGridColumn dataField="lastName" headerText="Last Name" />
<mx:DataGridColumn dataField="firstName" headerText="First Name" />
<mx:DataGridColumn dataField="userName" headerText="User Name" />
<mx:DataGridColumn dataField="password" headerText="Password" />
<mx:DataGridColumn dataField="email" headerText="Email" />
</mx:columns>
</mx:DataGrid>
My UPDATEMEMBER component:
Script:
[Bindable]
private var _record:Member;
//public function to get and set
//_record Object
public function get record():Member {
return _record;
}
public function set record(record:Member):void {
_record = record;
}
The form:
<mx:Form xmlns:mx="http://www.adobe.com/2006/mxml">;
<mx:FormItem label="First Name:" styleName="title">
<mx:TextInput id="firstName" text="{_record.firstName}" />
</mx:FormItem>
<mx:FormItem label="Last Name:" styleName="title">
<mx:TextInput id="lastName" text="{_record.lastName}" />
</mx:FormItem>
<mx:FormItem label="Email:" styleName="title">
<mx:TextInput id="email" text="{_record.email}" />
</mx:FormItem>
<mx:FormItem label="User Name:" styleName="title">
<mx:TextInput id="userName" text="{_record.userName}" />
</mx:FormItem>
<mx:FormItem label="Password:" id="passwordFormItem" styleName="title">
<mx:TextInput id="password" text="{_record.password}" displayAsPassword="true" />
</mx:FormItem>
<mx:FormItem label="Confirm:" styleName="title">
<mx:TextInput id="confirm" text="{_record.password}" displayAsPassword="true"/>
</mx:FormItem>
<mx:ControlBar x="0" y="709">
<mx:Button id="updateBtn" label="Update"
enabled="{(userName.text.length == 0 || password.text.length == 0 || firstName.text.length == 0 || lastName.text.length == 0 || email.text.length == 0 || confirm.text.length == 0) ? false : true}"
toolTip="{'Click to update member'}" />
</mx:ControlBar>
</mx:Form>
My APPLICATION file:
Script:
[Bindable]
private var aRecord:Member;
private function receiveDGResultHandler(event:Event):void {
aRecord = dgRecords.record ;
}
2 Child Components:
<comps:RecordList id="dgRecords" dgSelectedMember="receiveDGResultHandler(event)" />
<comps:UpdateMember id="updateMember" record="{aRecord}" />
I wish I could have gotten the Event Bubbling to work with a custom event class so I could bypass any code in the Application file. However, I am very happy that it is working! :)
Thanks again for all your help, Bruce!
sorry for so many questions. Great stuff.....
One way you could compare the two arrays is using nested for loops. See the array's section of programming ActionScript 3.0 here:
http://livedocs.adobe.com/flex/201/html/Part5_Prog...
Type Coercion failed: cannot convert Object@30348a1 to Person
Im not sure what to do?
Thanks
Thanks, I am very slow. You have helped me greatly in the past. I am confused, Im thinking your code must work. So I shouldnt probably go changing the array or should I? Maybe its my data base table. I have presenterID set up as the ID field so its datatype is int, with identity Spec... to yes. and the last first, phone, zip are char.
I have also been looking at you grid checkbox example. But since I have been working for months with no avail with updating grids to a DB I better step back and work back up to where I am on the project as a whole. I get the data into the grid fine. just converting problem...
Thanks
George
In web method (AddDetails) Gender & "SpouseFind find" are CHAR Data type,
I am giving value in Single charater in sql and string Flex form.
But it will be displayed ERROR HTTP REQUEST ERROR #2032 sTREAM ERROR).
* what is the equal data type "char" for flex
When i enter "student No" no and click retrive button If there is no record, it gives object not found error.
I Need to customize recored not found message same as
when i submit button click it should display record successfully.
is there any sample code ?
regards
ganesh