Although some Flex applications use data more extensively than others, nearly all use data to some extent. The Flex SDK is a robust set of tools for working with data. This chapter examines how to work with data on the client side without an extensive discussion of client/server data communication, which is covered in Chapter 16. Rather, this chapter focuses primarily on the following topics: modeling data and data binding.
When working with data, you generally want to store it in some sort of data repository within memory. These repositories are known as data models. In the first section of this chapter, we’ll look at each of the three basic ways to store data in data models.
You can use ActionScript for all your data management needs, but then you’re not really using the power of the Flex framework. To simplify linking data from a data model to a control or from one control to another control or component you can use a powerful feature called data binding.
Once you know the basics of working with data in a Flex application, you’ll have a foundation for sending and receiving data. That topic is discussed further in Chapter 16.
You can work with data in many ways in Flex applications, including as low-level ActionScript solutions and high-level MXML solutions; this section looks at both techniques. You can use these data models as a repository for data retrieved from a remote procedure call (RPC) such as a web service method call. You can also use a data model to store user input data before sending it to the server. You can even use a data model simply as a mechanism for populating form input, such as a combo box.
The <mx:Model>
tag
allows you to create an object that represents a data
structure using MXML. To use the tag practically, you must always
specify an id
attribute:
<mx:Model id="example" />
Once you’ve created a model this way, you can do one of two things:
Create a data structure using tags.
Specify a source attribute to populate the model from a file.
If you want to populate a model object within your MXML document, you can specify the structure using tags. Tags are useful when you want to use the data model for storing user input or data you retrieve from an RPC. The tags are arbitrary XML tags that you select to represent the data structure. The data model must contain only one root node. The following example uses a data model to represent a user:
<mx:Model id="userData"> <user> <email></email> <phone></phone> <address> <city></city> <state></state> </address> </user> </mx:Model>
Of course, you can populate the data model with real data, as in the next example:
<mx:Model id="userData"> <user> <email>[email protected]</email> <phone>123 555-1212</phone> <address> <city>Exampleville</city> <state>CA</state> </address> </user> </mx:Model>
However, in most cases when you want to initialize a data model with data, you should use an external file, as described in the next section. When you create the structure of a data model in the MXML document in this fashion, you won’t initialize the object with data. Instead, you’ll use it with data binding (discussed in the Data Binding” section later in this chapter) to store data from user input or data retrieved with RPCs.
If you want to use a data model to store static data, often the
best approach is to load that data from a file rather than defining
the structure and initializing the object data, all from the MXML
document. For example, consider a data model in which you want to
store all the U.S. state names. If you define the structure and
initialize it within the MXML document, your MXML document becomes
cluttered with lots of lines that are not used to define the user
interface or the business logic, but rather are used just to populate
a data model. It is better to place that data in a separate file and
simply reference it in the MXML document. The <mx:Model>
tag makes that an easy
task.
If you specify a source attribute for an <mx:Model>
tag, the object looks to the file you specify, and it loads
the data from that file at compile time, not at runtime. Specifying
the source
attribute achieves
exactly the same effect as placing the contents of the source file
within the <mx:Model>
tag,
but it allows you to place the data in a separate file to clean up the
MXML. Once the .swf is compiled,
you won’t need to distribute the data source file with the .swf file as all the data is compiled into
the .swf. This is also a
potential downside since it also increases the file size of the
.swf file.
Note that while using <mx:Model>
tags may
seem like a solution for application-wide data sources, you’ll
likely find that it is preferable to use ActionScript data model
classes for that purpose. You can read about using ActionScript data
model classes in the “Using ActionScript Classes” section of this
chapter.
Consider the following XML document called states.xml, an XML file that contains the names of all 50 U.S. states (to save space, this example has been shortened, but you can assume the actual file contains all 50 states):
<states> <state>Alabama</state> <state>Alaska</state> <state>Arizona</state> <state>Arkansas</state> <state>California</state> <state>Colorado</state> <state>Connecticut</state> <state>Delaware</state> <state>Florida</state> <state>Georgia</state> <state>Hawaii</state> <state>Idaho</state> <state>Illinois</state> <!-- additional states... --> </states>
You can populate a model with that data by adding the source
attribute to the <mx:Model>
tag, as shown in the
following example:
<mx:Model id="statesModel" source="states.xml" />
Note that the preceding assumes that states.xml is in the same directory as the main MXML file for the application.
Having a data model doesn’t do you much good unless you can reference the data stored within it—either to update it or to retrieve it. In many, if not most cases, you will use something called data binding to reference model data. (The details of data binding are discussed later in this chapter, in the Data Binding” section.) You can also reference a model using ActionScript. You’ll see how the same concepts apply when using data binding later in this chapter.
To understand how to reference data in a data model it’s important to understand how an <mx:Model>
tag translates into
ActionScript. Unlike many MXML tags, the <mx:Model>
tag does not correspond to
an ActionScript class by the same name. There is no Model
class. Rather, an object created using
the <mx:Model>
tag is an
instance of the ObjectProxy
class.
An ObjectProxy
object
is essentially a wrapper for an Object
instance. In practical terms, you can
treat an ObjectProxy
object exactly
as you would an Object
instance.
The primary purpose of ObjectProxy
is to enable data binding, which would not be available for a simple
Object
instance.
The fact that an object created with <mx:Model>
is an ObjectProxy
object in ActionScript
immediately tells you that you can access the data using standard
ActionScript dot syntax. The only thing you need to now know about
these objects is how the tag-based data structure translates to
ActionScript. As we’ve discussed, a model created with <mx:Model>
can have only one root
node. That root node is always synonymous with the model
object itself. The child nodes of the root node become properties of
the ObjectProxy
object, and the
child nodes of the child nodes become properties of the properties of
the object.
The following example defines a simple data model and a button.
When the user clicks the button, it calls trace()
and displays the value of the
email
property of the data model.
Because the root node of the data model (user
) is synonymous with the data model
object (userData
), you do not need
to (nor can you) treat the root node as a child of the data model; to
retrieve the email value, userData.email
is used, not userData.user.email
:
<mx:Model id="userData"> <user> <email>[email protected]</email> <phone>123 555-1212</phone> <address> <city>Exampleville</city> <state>CA</state> </address> </user> </mx:Model> <mx:Button click="trace(userData.email)" />
You can also assign values to the data model properties using
standard ActionScript expressions. The following example uses the same
data model as the preceding example, but with two buttons—one that
updates the city
value by appending
a random number, and one that traces the value:
<mx:VBox> <mx:Button click="userData.address.city = 'Exampleville' + Math.round(Math.random() * 10)" label="Update City" /> <mx:Button click="trace(userData.address.city)" label="Trace" /> </mx:VBox>
There is one conversion that takes place that might not be intuitive. When a data model structure consists of two or more sibling nodes with the same name, they are converted into an array. Consider the states example again:
<states> <state>Alabama</state> <state>Alaska</state> <state>Arizona</state> <state>Arkansas</state> <state>California</state> <state>Colorado</state> <state>Connecticut</state> <state>Delaware</state> <state>Florida</state> <state>Georgia</state> <state>Hawaii</state> <state>Idaho</state> <state>Illinois</state> <!-- additional states... --> </states>
The states data is loaded into a data model using the source
attribute, as follows:
<mx:Model id="statesModel" source="states.xml" />
Here, statesModel.state
is an
array that contains the name of each state
as an element. The following traces
true when the user clicks the button because statesModel.state
is an array:
<mx:Button click="trace(statesModel.state is Array)" />
The following example uses an ActionScript function to loop through all the elements of the array and display them in a text area:
<mx:Script> <![CDATA[ private function displayStates():void { for(var i:uint = 0; i < statesModel.state.length; i++) { statesTextArea.text += statesModel.state[i] + " "; } } ]]> </mx:Script> <mx:Model id="statesModel" source="states.xml" /> <mx:VBox> <mx:Button click="displayStates()" /> <mx:TextArea id="statesTextArea" height="500" /> </mx:VBox>
The <mx:Model>
tag is
useful when you want to work with data stored in traditional types such
as objects, strings, and arrays. If you want to work with XML-formatted
data, you can use the <mx:XML>
tag to create an XML-based data model (that you can access using
E4X).
The <mx:Model>
and
<mx:XML>
tags are structurally
very similar. As with <mx:Model>
, you should always specify an
id
attribute when creating an
<mx:XML>
tag:
<mx:XML id="example" />
Also, as with <mx:Model>
,
there are two basic ways to create the structure and/or initialize an <mx:XML>
data model:
Create a data structure using tags.
Specify a source attribute to populate the model from a file.
You can specify an XML structure using tags in the MXML document
much as you would for an ObjectProxy
-based model created with
<mx:Model>
. The following
defines the structure for an <mx:XML>
tag:
<mx:XML id="chaptersXml" xmlns=""> <chapters label="Chapters"> <chapter label="Chapter 1"> <file label="File 1.1" /> </chapter> <chapter label="Chapter 2"> <file label="File 2.1" /> </chapter> </chapters> </mx:XML>
As with <mx:Model>
,
the structure for <mx:XML>
must have only one root node.
You can load the XML data for an <mx:XML>
tag using the source
attribute just as you would with
<mx:Model>
. When you use the
source
attribute, that data is
loaded at compile time, and it is compiled into the .swf. That means you do not need to
distribute the source file with the .swf, and it means the data is static. The
following loads the data from chapters.xml:
<mx:XML id=chaptersXml" source="chapters.xml" />
When you use an <mx:XML>
tag, it creates an XML object
in ActionScript. By default, the XML object is a top-level E4X
XML
object. However, you can use the format
attribute to specify whether to use
an E4X XML
object or a legacy
flash.xml.XMLNode
object. The
default value for the format
attribute is e4x
. Setting the value
to xml
creates an XMLNode
object instead. For all the examples in this book, E4X XML data
models are used unless otherwise noted.
When you want to reference the data from a model created using
<mx:XML>,
the root node of
the data is synonymous with the data model object. The following uses
E4X syntax to trace the label
attribute of the first <chapter>
child node:
<mx:XML id="chaptersXml"> <chapters label="Chapters"> <chapter label="Chapter 1"> <file label="File 1.1" /> </chapter> <chapter label="Chapter 2"> <file label="File 2.1" /> </chapter> </chapters> </mx:XML> <mx:Button click="trace(chaptersXml.chapter[0].@label)" />
The following example assigns the data model as the data provider for a tree component:
<mx:XML id="chaptersXml"> <chapters label="Chapters"> <chapter label="Chapter 1"> <file label="File 1.1" /> </chapter> <chapter label="Chapter 2"> <file label="File 2.1" /> </chapter> </chapters> </mx:XML> <mx:VBox> <mx:Button click="chapters.dataProvider = chaptersXml" /> <mx:Tree id="chapters" width="200" labelField="@label" /> </mx:VBox>
Although the <mx:Model>
and <mx:XML>
data models
provide a simple and convenient way to work with data, they are not the
ideal solution in most cases. Even though they work well for simple,
static data (such as a list of U.S. state names), they are not well
suited for complex data, dynamic data, or data that has rules applied to
it. In those cases, it is better to use a custom ActionScript class as
the data model. Although there’s nothing wrong with using the <mx:Model>
and <mx:XML>
tags, it is best to be very
sparse in your use of them. Remember that MXML is primarily intended for
creating user interfaces and layout. ActionScript, on the other hand, is
ideal for managing and working with data. Here are a few of the
advantages of using a custom ActionScript class:
With <mx:Model>
and
<mx:XML>
you cannot
enforce data types, but with ActionScript classes you can (i.e., a
string must be a String
, and an
int must be an int
).
You cannot verify that a value assigned to an object created
using <mx:Model>
or
<mx:XML>
is a valid
value. For example, if a particular property can have values only
in the range from 1 to 10, you can't verify that when assigning
values to <mx:Model>
or
<mx:XML>
. However, an
ActionScript class setter method can test for valid values. If a
value is invalid, the class can discard the new assignment,
convert the value to a value in the valid range, or throw an
error.
When you assign a value to a property of an instance created
from <mx:Model>
or
<mx:XML>,
you're simply
assigning a value without the ability to run any business logic.
However, with an ActionScript class you can run any ActionScript
operations you want when getting or setting values.
You cannot employ sophisticated design patterns with
<mx:Model>
and <mx:XML>
, yet you can with an
ActionScript class. For example, you may need to have a single, managed
instance of a data model such as a user account data model. That
way, it is accessible throughout the application. With an
ActionScript class you can employ the Singleton design pattern to
accomplish this goal.
It’s worth noting that the main application class is a type of
Singleton class in that there is only one instance per application,
and that one instance is globally accessible (via mx.core.Application.application
). You could,
theoretically, add a data model instance to the main class and access
it globally through the main class. Although that would accomplish the
goal, the architectural soundness of that decision is questionable. It
is generally advisable to use an ActionScript class as the data
model.
Writing an ActionScript class as a data model is quite simple. You merely need to define a new class with public accessor methods for all the properties. The class in Example 14-1 defines a data model for a user. Note that all the getters and setters are strongly typed and several of the setters use data testing.
Example 14-1. User class
package com.oreilly.programmingflex.data { public class User { private var _nameFirst:String; private var _nameLast:String; private var _email:String; private var _lastLogin:Date; private var _userType:uint; public function get nameFirst():String { return _nameFirst; } public function set nameFirst(value:String):void { _nameFirst = value; } public function get nameLast():String { return _nameLast; } public function set nameLast(value:String):void { _nameLast = value; } public function get email():String { return _email; } public function set email(value:String):void { var expression:RegExp = /[A-Z0-9._%-]+@[A-Z0-9.-]+.[A-Z]{2,4}/i; if(expression.test(value)) { _email = value; } else { _email = "invalid email"; } } public function get lastLogin():Date { return _lastLogin; } public function set lastLogin(value:Date):void { _lastLogin = value; } public function get userType():uint { return _userType; } public function set userType(value:uint):void { if(userType <= 2) { _userType = value; } } public function User() {} } }
You can then create an instance of the model class using MXML or
ActionScript. With MXML you have to define the namespace, then use
<namespace:Class>
to create the
instance:
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:data="com.oreilly.programmingflex.data.*" layout="absolute"> <data:User id="user" email="[email protected]" lastLogin="{new Date()}" nameFirst="Abigail" nameLast="Smith" userType="1" /> </mx:Application>
With ActionScript you need to import the class, and then use the constructor as part of a new statement:
import com.oreilly.programmingflex.data.User; private var user:User; private function initializeHandler(event:Event):void { user = new User(); user.email = "[email protected]'; // etc. }
In the next section, we’ll look at data binding. If you want to
enable the data binding feature for a custom ActionScript-based data
model, you must use the [Bindable]
metadata tag when declaring the
class:
[Bindable] public class User {
If you create the instance using MXML, the instance is
automatically enabled for data binding, assuming the class uses the
[Bindable]
metadata tag. However, if
you create the instance using ActionScript, you must also use the
[Bindable]
tag when declaring the
variable you use to store the reference:
[Bindable] private var user:User;
We’ll talk more about data binding and the [Bindable]
metadata tag in the Data Binding” section later in this chapter.
Another way that you can work with data is to use a collection. In
this case, we’re using the word collection to refer
to a class that implements the mx.collections.ICollectionView
interface.
Two of the primary such collection classes are mx.collections.ArrayCollection
and mx.collections.XMLListCollection
.
In some cases, collections can be substitutes for the other
techniques we described earlier. For example, you can use a collection
in place of a Model
tag if you intend
to use the data as a data provider for a list component. However, when
you are using an ActionScript class to model data, a collection isn’t
necessarily a substitute, but rather a complement. The primary value of
a collection in such a case is that it provides a data-bindable way to
store data.
You can use an ArrayCollection
instead of an Array
when you’d like
to use data binding (see the next section for more information about
data binding). This is because an Array
does not allow for data binding. A
change to an element in an array will not trigger changes in other
components. However, ArrayCollection
instances are data-bindable, and if you add or remove an element from
the collection, it triggers binding changes. As with many things in
Flex, you can create an ArrayCollection
instance using ActionScript or
MXML. First we’ll look at an example using MXML. In the following code,
we create an ArrayCollection
instance
using an ArrayCollection
tag. We are
assigning the collection to the dataProvider
property of a combo box
component. You’ll notice that nested in the ArrayCollection
tag is an Array
tag. That’s because an ArrayCollection
object is a
data-bindable wrapper for an array.
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical"> <mx:ComboBox> <mx:dataProvider> <mx:ArrayCollection id="letters"> <mx:Array> <mx:String>A</mx:String> <mx:String>B</mx:String> <mx:String>C</mx:String> <mx:String>D</mx:String> </mx:Array> </mx:ArrayCollection> </mx:dataProvider> </mx:ComboBox> </mx:Application>
You can omit the <mx:Array>
tag in the preceding
example and it will still work. When you nest values inside an
<mx:ArrayCollection>
tag, the
Flex compiler assumes they're elements of an array. Therefore, you may
sometimes see code where the <mx:Array>
tag is absent. Either way
works. In this case, we are opting for the explicit use of the
<mx:Array>
tag for
clarity.
Once we’ve created an ArrayCollection
instance we can update it
using various methods. We can use the addItem()
method to add elements to the
collection. We can use the removeItemAt()
method to remove elements at
specific indexes. We can use the getItemAt()
method to retrieve elements at
specific indexes. Adding and removing elements using these methods
triggers data binding changes, as you can see in the next example. In
this code, we add a button that adds a random letter to the collection
when the user clicks it. Because the update triggers data binding, the
following code will result in the combo box redrawing itself, and it
will display a new item in the drop-down list.
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical"> <mx:ComboBox> <mx:dataProvider> <mx:ArrayCollection id="letters"> <mx:Array> <mx:String>A</mx:String> <mx:String>B</mx:String> <mx:String>C</mx:String> <mx:String>D</mx:String> </mx:Array> </mx:ArrayCollection> </mx:dataProvider> </mx:ComboBox> <mx:Button label="Change" click="letters.addItem(String.fromCharCode (Math.round(Math.random() * 26) + 65))" /> </mx:Application>
As we said earlier, we can also create ArrayCollection
instances using ActionScript.
When we create ArrayCollection
instances in this way, we can create the empty instance and add elements
programmatically using the addItem()
method, or we can initialize the collection by passing an array to the
constructor, as in the following example.
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical"> <mx:Script> <![CDATA[ import mx.collections.ArrayCollection; [Bindable] private var letters:ArrayCollection = new ArrayCollection(["A", "B", "C", "D"]); ]]> </mx:Script> <mx:ComboBox dataProvider="{letters}" /> <mx:Button label="Change" click="letters.addItem(String.fromCharCode (Math.round(Math.random() * 26) + 65))" /> </mx:Application>
The XMLListCollection
class
is to XMLList
objects
what the ArrayCollection
is to arrays. XMLListCollection
instances wrap XMLList
objects. XMLListCollection
provides the same API for
adding, retrieving, and removing elements as the ArrayCollection
class does. However, when you
call these methods, you use XML elements instead of nonhierarchical
values, as you would with ArrayCollection
. The following code
illustrates how you can create an XMLListCollection
using MXML:
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical"> <mx:Script> <![CDATA[ private function addNewItem():void { var item:XML = <element label="Numbers"> <element label="1" /> <element label="2" /> </element> collection.addItem(item); } ]]> </mx:Script> <mx:Tree labelField="@label" width="200"> <mx:dataProvider> <mx:XMLListCollection id="collection"> <mx:XMLList xmlns=""> <element label="Letters"> <element label="A" /> <element label="B" /> </element> </mx:XMLList> </mx:XMLListCollection> </mx:dataProvider> </mx:Tree> <mx:Button label="Change" click="addNewItem();" /> </mx:Application>
In this example, we create an XMLListCollection
with an ID of collection
, and we assign it as the dataProvider
for a tree component. The
XMLListCollection
instance wraps an
XMLList
object with one element
called Letters
. When the user clicks
the button we use the addItem()
method to add a second element to the XMLListCollection
. Because XMLListCollection
updates data binding, the
changes are reflected in the UI immediately.
You can also create an XMLListCollection
object using ActionScript.
The following example illustrates how to do this. This code results in
the same behavior as the preceding example, but it uses ActionScript to
create the XMLListCollection
instance.
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical"> <mx:Script> <![CDATA[ import mx.collections.XMLListCollection; private var xmlList:XMLList = new XMLList(<element label="Letters"> <element label="A" /> <element label="B" /> </element>); [Bindable] private var collection:XMLListCollection = new XMLListCollection(xmlList); private function addNewItem():void { var item:XML = <element label='Numbers'> <element label="1" /> <element label="2" /> </element> collection.addItem(item); } ]]> </mx:Script> <mx:Tree labelField="@label" width="200" dataProvider="{collection}" /> <mx:Button label="Change" click="addNewItem();" /> </mx:Application>