by Adobe
Adobe logo

Created

4 May 2011

 
Communicating between MXML components

 
 

 
Explanation

This sample project shows you how to break an application up into multiple MXML components that broadcast custom events. To look at the code, right-click on the SWF in the browser and select View Source or download the sample files and follow the instructions to import the Flash Builder FXP. Multiple application versions are provided: an XML version that does not require a server as well as versions using Flash Remoting with PHP, Java, and ColdFusion servers.
 
 
Creating MXML components
As your application gets larger and more complicated, you should break out your logic into packages of ActionScript classes and your views into multiple MXML files (called MXML components). This will allow multiple developers to work on the application at once, make your application easier to maintain and scale, and enable reusability of the different classes and components.
 
This sample application has the same functionality as the last sample project with an interface to view and modify employee data from a database. It has been re-architected to consist of five separate views: an Employees view to display the employee master list, an EmployeeDetails view to display details for a selected employee, an EmployeeForm view with a form for updating or adding an employee, a Departments view to display the department information, and finally, the main view that has the main navigation buttons and controls the display of the other views.
 
Keep in mind when looking at this sample application that there is no one correct way to architect an application; there are infinite ways. The purpose of this sample is to illustrate one example of a refactored application and illustrate the main concept behind creating reusable application building blocks: by creating custom components that broadcast custom events.
 
To create a new component in Flash Builder, you select New > MXML Component and in the resulting dialog box, you choose what to name the component, where to store it, and what component to base it on. The new MXML file looks just like the main MXML file except that the root tag is not the Application tag but some other component that the new component is extending. A component can only be included in an application, not run on its own.
 
In the sample application, take a look at the Departments.mxml file in the com.adobe.samples.xyz.views folder. It defines a class called Departments that extends the Group class. It contains a DataGrid and a bindable, public variable called departments that is used as the dataProvider for the DataGrid.
 
<?xml version="1.0" encoding="utf-8"?> <s:Group xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/mx" > <fx:Script> <![CDATA[ import mx.collections.ArrayCollection; [Bindable]public var departments:ArrayCollection; ]]> </fx:Script> <s:DataGrid dataProvider="{departments}" requestedRowCount="6"> <s:columns> <s:ArrayList> <s:GridColumn headerText="Name" dataField="name" width="180"/> <s:GridColumn headerText="ID" dataField="id" width="40"/> <s:GridColumn headerText="Manager" dataField="manager" width="180"/> <s:GridColumn dataField="budget" headerText="Budget" width="85"/> <s:GridColumn dataField="actualexpenses" headerText="Expenses" width="85"/> </s:ArrayList> </s:columns> </s:DataGrid> </s:Group>
 
Using MXML components
To use this component in the application, you add a tag with the name of the class, just as you do for the components in the Flex framework.
 
<Departments/>
When the compiler sees this tag, it needs to know what class to make an instance of. You make this association by defining a namespace that associates a new prefix with a location of classes so when you use an XML tag with that prefix, the compiler can locate the class. In this application, a namespace called views has been defined for classes located in the com.adobe.samples.xyz.views package. Using a reverse domain name structure for organizing classes is common to guarantee uniqueness of namespaces.
 
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/mx" xmlns:views="com.adobe.samples.xyz.views.*" ...>
You now use the views prefix in tags to create instances of classes that are located in the com.adobe.samples.xyz.views folder.
 
<views:Departments/>
The main application file, the main view in this sample, controls the instantiation and layout of the different views by including them in or excluding them from different states.
 
<s:Application ...> (...) <s:states> <s:State name="XYZEmployees"/> <s:State name="XYZEmployeeDetails"/> <s:State name="XYZEmployeeForm"/> <s:State name="XYZDepartments"/> </s:states> <s:VGroup top="35" left="35"> <s:Label text="XYZ Corporation Directory" fontSize="20" fontWeight="bold" color="#1239E3"/> <mx:Spacer height="15"/> <s:HGroup> <s:Button label="Employees" id="empBtn" enabled.XYZDepartments="true" enabled="false" .../> <s:Button label="Departments" id="deptBtn" enabled.XYZDepartments="false" .../> </s:HGroup> <views:Departments includeIn="XYZDepartments".../> <views:Employees excludeFrom="XYZDepartments" .../> <views:EmployeeDetails includeIn="XYZEmployeeDetails" .../> <views:EmployeeForm id="form" includeIn="XYZEmployeeForm" employee="{employee}" .../> </s:VGroup> </s:Application>
 
Defining a component API
In order to build loosely coupled components, those that can be used independently with no dependencies on other components, you define a public application programming interface (API) for the component that consists of public properties, methods, and events that can be used to interact with and use this component.
 
The Departments component we just looked at has a bindable, public variable called departments that the DataGrid control inside it is bound to. When using this component in an application, instead of "reaching inside" it and setting the value of the DataGrid's dataProvider directly, the component's API is used to interact with the component; its public departments property is assigned a value.
 
<views:Departments includeIn="XYZDepartments" departments="{departments}"/>
Because the DataGrid inside is bound to this variable, it is updated whenever the value of this public property changes.
 
In this code, the departments property is set equal to the bindable departments property of the main application, which in this sample, is our central repository for the application data. (This data could also be encapsulated in a separate ActionScript class.) Because departments in the main application is also bindable:
 
[Bindable]protected var employees:ArrayCollection=new ArrayCollection();
... whenever its value changes, the data displayed in the Departments view is updated.
 
The same mechanism is used to display data in the EmployeeDetails component. It has a bindable, public variable called employee of type Employee that all of the Label controls inside it are bound to.
 
<?xml version="1.0" encoding="utf-8"?> <s:HGroup xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/mx"> <fx:Script> <![CDATA[ import com.adobe.samples.xyz.valueObjects.Employee; [Bindable]public var employee:Employee; ]]> </fx:Script> <s:Form width="275"> <s:FormItem label="Last Name"> <s:Label id="lastnameLabel" text="{employee.lastname}"/> </s:FormItem> <s:FormItem label="First Name"> <s:Label id="firstnameLabel" text="{employee.firstname}"/> </s:FormItem> (...) </s:Form> </s:Group>
When this component is used in the application, its employee variable is assigned a value and that is the data the component displays.
 
<views:EmployeeDetails id="details" includeIn="XYZEmployeeDetails" employee="{employee}"/>
The employee property is set equal to the bindable employee property of the main application:
 
[Bindable]protected var employee:Employee;
... and whenever the data model's value changes, the data displayed in the EmployeeDetails view is updated.
 
Although only public properties were defined in these views, public methods could be defined as well. You can also define events, which is what we'll take a look at next.
 
 
Broadcasting events
The two views we looked at so far were non-interactive display views. In other cases, the user will be able to interact with the component and events will occur inside the component. In order to make this component reusable, we want it to contain only view code and no business logic. The logic it contains should be just for managing itself. The application logic should be handled in other classes. In this application, all the business logic is centralized into the main application. (As the application gets larger, business logic is separated out into separate ActionScript classes.)
 
The question is, then, how do you have the main application do something (execute some business logic, update some data, change states, or do whatever needs to be done) when something happens inside a component. One way would be to just "reach inside" the component again by registering to listen for an event of one of the object's inside it.
 
someComponent.someControlInside.addEventListener("click",doDomething);
This breaks encapsulation again and makes the components tightly coupled and the code brittle. If the component was moved to inside another view or the control inside it changed to a different one with a different event being broadcast upon user interaction, the code would break.
 
What you want to do instead, is define a custom event for the component and have the component dispatch that event when something happens inside it. The code using this component can then register to listen for that event and respond by executing its own code when that event is broadcast. That's the idea, so now let's see how it's implemented.
 
Inside the component, the [Event] metadata tag is used to define the event as part of the component's API and specify what type of event object it will broadcast for this event.
 
<fx:Metadata> [Event(name="customEventName",type="flash.events.Event")] </fx:Metadata>
The goal is for when some event occurs in the component (for example, when a button is clicked or the selected item in a DataGrid changes), the component creates an instance of the flash.events.Event class (in this case) with a type of customEventName and broadcasts it. The component's dispatchEvent() method is used to broadcast an event into the Flash Player event stream.
 
this.dispatchEvent(new Event("customEventName "));
A new instance of the flash.events.Event class is created and dispatched. The listener will receive this Event object instance. Event is the base class for all event classes and has a type property that holds the type of event that occurred (like click or change or customEventName) and a target property that is a reference to the object that broadcast the event.
 
The type property is set when the Event object is created; the Event class constructor has one required argument, a string equal to the type of event being broadcast; it is used to set the event object's type property. This should match the name of the event declared in the event metadata tag. In this example, this string is customEventName.
 
The code that instantiates this custom component can now register to listen for this custom event and register an event handler.
 
<views:SomeView customEventName="onCustomEvent(event)"/>
Loosely-coupled components like this that define and broadcast custom events are the core building blocks for Flex applications. In fact, this is how the components in the Flex framework itself are built.
 
 
Defining custom Event classes
The component does not have to broadcast an instance of the flash.events.Event class. Any type of event object can be created and broadcast. If you want to pass some data with the event object (for example, some information about the event that occurred like what employee was selected in a DataGrid), you define a custom Event subclass with a custom property or properties to hold this data. The data will then be available in the listener as a property of the event object it receives.
 
The Employees and EmployeeForm components in this application broadcast instances of an EmployeeEvent class which has a public property called employee.
 
package com.adobe.samples.xyz.events { import flash.events.Event; import com.adobe.samples.xyz.valueObjects.Employee; public class EmployeeEvent extends Event { public var employee:Employee; public static const EMPCHANGE:String="empChange"; public static const EMPADD:String="empAdd"; public static const EMPUPDATE:String="empUpdate"; public static const EMPEDIT:String="empEdit"; public static const EMPDELETE:String="empDelete"; public function EmployeeEvent(type:String, employee:Employee){ super(type); this.employee=employee; } }
Inside the constructor, the constructor of the parent class (Event) is called using super(). As discussed, the parent's constructor has one required argument, type, and so we also make type a required argument for this class so it can be passed to the parent's constructor. We add a second required argument to the constructor of type Employee so when an instance of this class is created, an Employee object must be passed to it and this value gets assigned to the event object's public employee property.
 
A number of static constants have also been defined in this class, one for each of the events that are going to broadcast this type of event. That way instead of using a literal string when specifying the event type:
 
new EmployeeEvent("empChange",employee);
... which can have mispellings that won't be caught, you can use constants that can be selected from code-hinting and for which you get compile-time checking.
 
new EmployeeEvent(EmployeeEvent.EMPCHANGE,employee);
 
Broadcasting custom events
Now we're ready to go take a look at the other two views, Employees and EmployeeForm, which both dispatch EmployeeEvent objects when events occur inside the component.
 
Let's first take a look at Employees.mxml. Like the Departments component we looked at earlier, it has a DataGrid bound to a bindable, public property. Here, it is bound to the employees property. It also contains an HGroup with three buttons, Update, Delete, and Add which are enabled or disabled depending upon the state of the component. The component has three states, MainEmployees, MainEmployeeDetails, and MainEmployeeForm. Although the component itself does not display the details or the form, it has different states because the buttons need to be displayed, removed, disabled or enabled based on the user action.
 
<s:VGroup xmlns:fx="http://ns.adobe.com/mxml/2009" ...> <fx:Script><![CDATA[ [Bindable]public var employees:ArrayCollection; public var employee:Employee; (...) ]]></fx:Script> (...) <s:states> <s:State name="MainEmployees"/> <s:State name="MainEmployeeDetails"/> <s:State name="MainEmployeeForm"/> </s:states> <s:DataGrid id="empDg" dataProvider="{employees}" requestedRowCount="6" selectionChange="empDg_selectionChangeHandler(event)" editable="true" gridItemEditorSessionSave="empDg_gridItemEditorSessionSaveHandler(event)" .../> </s:DataGrid> <s:HGroup width="100%" horizontalAlign="right"> <s:Button id="updateBtn" label="Update" includeIn="MainEmployeeDetails,MainEmployeeForm" click="updateBtn_clickHandler(event)" enabled.MainEmployeeForm="false"/> <s:Button id="deleteBtn" label="Delete" includeIn="MainEmployeeDetails,MainEmployeeForm" click="deleteBtn_clickHandler(event)" enabled.MainEmployeeForm="false"/> <s:Button id="addBtn" label="Add " click="addBtn_clickHandler(event)" enabled.MainEmployeeForm="false"/> </s:HGroup> </s:VGroup>
The component also has five events declared.
 
<fx:Metadata> [Event(name="empChange",type="com.adobe.samples.xyz.events.EmployeeEvent")] [Event(name="empEdit",type="com.adobe.samples.xyz.events.EmployeeEvent")] [Event(name="empUpdate",type="com.adobe.samples.xyz.events.EmployeeEvent")] [Event(name="empAdd",type="com.adobe.samples.xyz.events.EmployeeEvent")] [Event(name="empDelete",type="com.adobe.samples.xyz.events.EmployeeEvent")] </fx:Metadata>
These events are broadcast when the user selects a row in the DataGrid, edits the data in a cell in the DataGrid, clicks the Update button, clicks the Add button, or clicks the Delete button.
 
Let's look at the handlers for each of these events. When the user selects a different employee in the Data Grid, the employee variable is set equal to that for the selected item, an instance of the EmployeeEvent class with a type of empChange is created and dispatched with the employee variable., and the component's state is changed to MainEmployeeDetails in which the Update and Add buttons are displayed.
 
protected function empDg_selectionChangeHandler(event:GridSelectionEvent):void{ employee=empDg.selectedItem as Employee; this.dispatchEvent(new EmployeeEvent(EmployeeEvent.EMPCHANGE,employee)); currentState="MainEmployeeDetails"; }
The application logic, what to do when this action occurs, is not contained in this component. It just notifies the main application that this happened, so the main application can do something.
 
When the user edits a cell in the DataGrid, an instance of the EmployeeEvent class with a type of empEdit is created and dispatched with the employee variable.
 
protected function empDg_gridItemEditorSessionSaveHandler(event:GridItemEditorEvent):void{ this.dispatchEvent(new EmployeeEvent(EmployeeEvent.EMPEDIT,employee)); }
When the user clicks the Add button, an instance of the EmployeeEvent class with a type of empAdd is created and dispatched with a new Employee object and any selected row in the DataGrid deselected (so one user is not selected while adding another in a form) and the state changed.
 
protected function addBtn_clickHandler(event:MouseEvent):void{ this.dispatchEvent(new EmployeeEvent(EmployeeEvent.EMPADD,new Employee())); currentState="MainEmployeeForm"; empDg.selectedIndex=-1; }
Similarly, when the user clicks the Update button, an instance of the EmployeeEvent class with a type of empUpdate is created and dispatched with the selected employee and the state changed:
 
protected function updateBtn_clickHandler(event:MouseEvent):void{ currentState="MainEmployeeForm"; this.dispatchEvent(new EmployeeEvent(EmployeeEvent.EMPUPDATE,employee)); }
... and when the Delete button is clicked, an instance of the EmployeeEvent class with a type of empDelete is created and dispatched with the employee variable and the state changed.
 
protected function deleteBtn_clickHandler(event:MouseEvent):void{ currentState="MainEmployees"; this.dispatchEvent(new EmployeeEvent(EmployeeEvent.EMPDELETE,employee)); }
The Employees component only contains view code and no business logic. It does not directly manipulate the application data (by changing employees) or make calls to the server (to delete an employee). Instead when something happens in the component, in this case when the Add, Update, or Delete buttons ar clicked or when a row in the DataGrid is selected or edited, it broadcasts an event so that the application using it can respond and act accordingly.
 
 
Handling events
Now, let's look at the code in the main application handling these events dispatched from the component. First, an instance of the view is created and event listeners registered for the events.
 
<views:Employees id="main" excludeFrom="XYZDepartments" employees="{employees}" empChange="main1_empChangeHandler(event)" empEdit="main1_empEditHandler(event)" empDelete="main1_empDeleteHandler(event)" empUpdate="main1_empUpdateHandler(event)" empAdd="main1_empUpdateHandler(event)"/>
Inside the empChange handler, the employee object that the EmployeeDetails component is bound to is updated and the state changed to one that displays the EmployeeDetail component.
 
protected function main1_empChangeHandler(event:EmployeeEvent):void{ employee=event.employee; currentState="XYZEmployeeDetails"; }
Inside the empEdit handler, the employee object is updated and this new value passed to the server to be saved in the database using a CallResponder to handle the result or fault. In the result handler, the application state is changed to display the EmployeeDetails view (in case this update came from a form instead of the DataGrid) and the public currentState property of the main view, the instance of the Employees component, set so the correct buttons are displayed and enable or disabled.
 
protected function main1_empEditHandler(event:EmployeeEvent):void{ employee=event.employee; //for CF employeeService.updateEmployee({item:employee}); //for Java and PHP,employeeService.updateEmployee(employee); } protected function updateEmployeeResult_resultHandler(event:ResultEvent):void{ currentState="XYZEmployeeDetails"; main.currentState="MainEmployeeDetails"; }
You see similar logic inside the empDelete handler, except a different method of the service object is called and then the local employees property is updated in the result handler and the state is changed to only show the Employees view.
 
protected function main1_empDeleteHandler(event:EmployeeEvent):void{ employee=event.employee; deleteEmployeeResult.token = employeeService.deleteEmployee(employee.id); } protected function deleteEmployeeResult_resultHandler(event:ResultEvent):void{ employees.removeItemAt(employees.getItemIndex(employee)); currentState="XYZEmployees"; }
The same function handles the empAdd and empUpdate events. For both, the employee property is populated from the event object and the state changed to show the EmployeeForm view.
 
protected function main1_empUpdateHandler(event:EmployeeEvent):void{ employee=event.employee; currentState="XYZEmployeeForm"; }
 
Following more events
Next, let's take a look at the EmployeeForm view and follow the events it broadcasts. Like the Employees component, its controls are bound to a public, bindable employees property but instead of a DataGrid, it contains a Form. It has two states so the "submit" button can have the appropriate label.
 
<s:HGroup xmlns:fx="http://ns.adobe.com/mxml/2009" ...> <fx:Script><![CDATA[ [Bindable]public var employee:Employee; (...) ]]></fx:Script> (...) <s:states> <s:State name="FormAdd"/> <s:State name="FormUpdate"/> </s:states> <s:Form width="275" defaultButton="{button}"> <s:FormItem label="Last Name"> <s:TextInput id="lastnameTextInput" text="{employee.lastname}" width="160"/> </s:FormItem> <s:FormItem label="First Name"> <s:TextInput id="firstnameTextInput" text="{employee.firstname}" width="160"/> </s:FormItem> <!-- more form items --> <s:FormItem> <s:Button id="button" label.FormAdd="Add" label.FormUpdate="Update" click="button_clickHandler(event)"/> </s:FormItem> </s:Form> </s:HGroup>
The component has two events declared.
 
<fx:Metadata> [Event(name="empAdd",type="com.adobe.samples.xyz.events.EmployeeEvent")] [Event(name="empUpdate",type="com.adobe.samples.xyz.events.EmployeeEvent")] </fx:Metadata>
These events are broadcast when the user adds or updates an employee. The button has a single click handler (for either state) and inside it, the employee object is updated with the values from the form fields and an empAdd or empUpdate event is broadcast.
 
protected function button_clickHandler(event:MouseEvent):void{ employee.lastname = lastnameTextInput.text; employee.firstname = firstnameTextInput.text; //other fields set if(employee.id==0){ this.dispatchEvent(new EmployeeEvent(EmployeeEvent.EMPADD,employee)); } else{ this.dispatchEvent(new EmployeeEvent(EmployeeEvent.EMPUPDATE,employee)); } }
In the main application, an instance of the EmployeeForm view is created and included in the XYZEmployeeForm state, and event listeners are registered for its events.
 
<views:EmployeeForm includeIn="XYZEmployeeForm" employee="{employee}" empAdd="employeeform1_empAddHandler(event)" empUpdate="employeeform1_empUpdateHandler(event)"/>
Inside the empAdd handler for this component, the new employee is sent to the server and in the CallResponder result handler, the new employee is added to employees and the state changed to show the details.
 
protected function employeeform1_empAddHandler(event:EmployeeEvent):void{ employee=event.employee; //for CF createEmployeeResult.token = employeeService.createEmployee({item:employee}); //for Java and PHP, createEmployeeResult.token = employeeService.createEmployee(employee); } protected function createEmployeeResult_resultHandler(event:ResultEvent):void{ employee.id=event.result as int; employees.addItem(employee); currentState="XYZEmployeeDetails"; main.currentState="MainEmployeeDetails"; }
Inside the empUpdate handler for this component, the edited employee is sent to the server and the same result handler is used that was used to handle the update results when the DataGrid was edited.
 
protected function employeeform1_empUpdateHandler(event:EmployeeEvent):void{ employee=event.employee; //for CF updateEmployeeResult.toke=employeeService.updateEmployee({item:employee}); //for Java and PHP, updateEmployeeResult.token=employeeService.updateEmployee(employee); } protected function updateEmployeeResult_resultHandler(event:ResultEvent):void{ currentState="XYZEmployeeDetails"; main.currentState="MainEmployeeDetails"; }
The application has two remaining events to take a look at. The EmployeeForm view has a listener registered for its addedToStage event.
 
<?xml version="1.0" encoding="utf-8"?> <s:HGroup addedToStage="hgroup_addedToStageHandler(event)" ...>
When this view is added (whever the main application switches to the XYZEmployeeForm state that includes it), the view can set its state based on the value of the employee variable. This way it can manage its own display and set the button's label to Add or Update as appropriate.
 
protected function hgroup_addedToStageHandler(event:Event):void{ if(employee.id!=0){ currentState="FormUpdate"; } else{ currentState="FormAdd"; } }
Lastly, the Employees view has a listener registered for its currentStateChange event.
 
<?xml version="1.0" encoding="utf-8"?> <s:VGroup currentStateChange="group_currentStateChangeHandler(event)" ...>
This view changes state when a user interacts with it (by clicking the Add, Update, or Delete button or selecting a row in the DataGrid) and it is also changed programatically in two handlers in the main application after an employee is added or updated in the database. Here is one of those handers again where main is the id of the Employees view instance.
 
protected function updateEmployeeResult_resultHandler(event:ResultEvent):void { currentState="XYZEmployeeDetails"; main.currentState="MainEmployeeDetails"; }
A listener has been added to the Employees component so that whenever its state is set programatically it can execute some code. In this case, if a new employee has been added to the database, it is selected and scrolled to in the DataGrid. This is the same code that was used and discussed in the previous data sample project , but now it is inside a view and the view is managing changes to itself.
 
protected function group_currentStateChangeHandler(event:StateChangeEvent):void{ if(currentState=="MainEmployeeDetails"&&employee!=empDg.selectedItem ){ empDg.setSelectedIndex(employees.getItemIndex(employee)); empDg.ensureCellIsVisible(empDg.selectedIndex); } }
 
Using a microarchitecture
As your application gets even larger and teams of developers work on it, you may want to use some consistent methodology to organize its files, centralize the application data and data services, and handle communication between all the components. Flex applications can be built using all the design patterns that have proven useful over the years in enterprise application development. Many Flex specific microarchitectures have been and continue to be developed. The oldest and most established is Cairngorm, an open source microarchitecture that uses commands and delegates, front controllers, a singleton data model, a singleton service store, and an event dispatcher. Other popular frameworks include Pure MVC, Mate, Parsley, Swiz, and Spring ActionScript. For more information about these and other frameworks, see Flex Architecture on the Adobe Developer Center.