Communicating between MXML components
- Download the sample code and files (ZIP, 167 KB)
- You can also view the full source code by right-clicking the Flex application and selecting View Source.
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.
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>
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>
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.
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.
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);
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. 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";
}
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);
}
}
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.