In this lesson, you will:
• Populate a List control with a dataset
• Populate a DataGroup with a dataset and display the information using a renderer
• Create an MXML component to be used as a renderer
• Use the Generate Getter/Setter wizard
• Learn about virtualization
• Respond to a user’s selection from a list
In this lesson, you’ll develop your skill in working with datasets. A dataset is really nothing but several data elements consolidated in a single object, like an Array, XMLList, ArrayCollection, or XMLListCollection. Up to this point, you’ve learned a few ways to display, manipulate, or loop over these datasets. In this chapter, you’ll learn about Flex components that automatically create a visual element for each item in a dataset.
In this lesson, you’ll learn about Lists and DataGroups. Both List and DataGroup instances can create a visual element for each item in its dataset (which is set to the DataGroup’s dataProvider
property). What is shown for each element will depend on the itemRenderer being used. You’ll learn about itemRenderers in this lesson as well.
The List class, much like the DataGroup class, has a dataset in its dataProvider and will visually represent each item using its itemRenderer. Lists add another piece of functionality, in that they manage the user’s selection of items from the list and provide an API for determining which item(s) if any, are selected.
In the course of this lesson, you’ll rework the ShoppingView component. Instead of having a hard-coded set of ProductItems as children, the component uses a DataGroup to dynamically create one ProductItem for each element in the groceryInventory
ArrayCollection. In this process, you’ll rework the ProductItem class to be an itemRenderer. You’ll also finish building out the functionality of the List displaying categories at the top of the application and will learn how to make the ShoppingView change the contents of its groceryInventory
property when the user selects one of the categories.
In the application, you have already used two List instances, one with a horizontal layout to display the categories across the top of the application, and the other to display the items in the shopping cart. From your use of these two Lists, you know that the List class is provided with a dataset via dataProvider
property (one list is using a XMLListCollection, and the other an ArrayCollection), and the list will display one item for each element in its dataProvider.
In Lesson 6, “Using Remote XML Data,” you used a list to display the categories in the control bar. In that list, you specified a labelField
to indicate which property the list should display. Using the labelField
property is a very effective way of specifying which property of an object will be shown for each item of the list; however, it is limited in that it can display only text. If you want to format the data, or concatenate multiple properties, you’ll need to use a labelFunction
.
A labelFunction
is a function that is used to determine the text to be rendered for each item in a List. This is done with the labelFunction
property. The function will accept an Object as a parameter (if you are using strongly typed objects, you can specify the actual data type instead of the generic). This parameter represents the data to be shown for each item displayed by the List. The following code shows an example of a labelFunction
, which displays the category of an item with its name and cost.
<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
creationComplete="generateCollection()">
<fx:Script>
<![CDATA[
import mx.collections.ArrayCollection;
[Bindable]
private var dp:ArrayCollection;
private function generateCollection():void{
var arrayData:Array = new Array();
var o1:Object = new Object();
o1.name = "banana";
o1.category="fruit";
o1.cost=.99;
arrayData.push(o1);
var o2:Object = new Object();
o2.name = "bread";
o2.category="bakery";
o2.cost=1.99;
arrayData.push(o2);
var o3:Object = new Object();
o3.name = "orange";
o3.category="fruit";
o3.cost=.52;
arrayData.push(o3);
var o4:Object = new Object();
o4.name = "donut";
o4.category="bakery";
o4.cost=.33;
arrayData.push(o4);
var o5:Object = new Object();
o5.name = "apple";
o5.category="fruit";
o5.cost=1.05;
arrayData.push(o5);
dp = new ArrayCollection(arrayData);
}
private function multiDisplay(item:Object):String{
return item.category+": "+item.name+" $"+item.cost;
}
]]>
</fx:Script>
<s:List dataProvider="{dp}"
labelFunction="multiDisplay"
/>
</s:Application>
If you saved and ran this application, it would appear like this:
Each object from the dp
ArrayCollection is passed into the labelFunction()
before it is rendered, and whatever value is returned from that function is what will be shown. In this case, you are displaying the category name, the item’s name, and then its cost.
Although the multiDisplay
function accepts parameters
private function multiDisplay(item:Object):String
you only pass a reference to the function to the List’s labelFunction
property.
labelFunction="multiDisplay"
Flex will automatically call the function with the correct arguments as it renders each item from the dataProvider.
In this next exercise, you’ll use a labelFunction
to format the data rendered in the shopping cart list.
Alternatively, if you didn’t complete the previous lesson or your code is not functioning properly, you can import the FlexGrocer.fxp project from the Lesson10/start folder. Please refer to the appendix for complete instructions on importing a project should you skip a lesson or if you have a code issue you cannot resolve.
renderProductName()
, which accepts a ShoppingCartItem
as a parameter and returns a String
.
private function renderProductName( item:ShoppingCartItem ):String {
}
Product
, which is equal to the product
property of the parameter to the function. Then, construct and return a string that concatenates parentheses around the item.quantity
, followed by product.prodName
, a dollar sign, and then the item’s subtotal
.
private function renderProductName( item:ShoppingCartItem ):String {
var product:Product = item.product;
return '(' + item.quantity + ') ' + product.prodName + ' $' + item.subtotal;
}
cartGroup
, and instruct it to use the renderProductName
labelFunction.
<s:List id="cartList"
dataProvider="{shoppingCart.items}"
includeIn="State1"
labelFunction="renderProductName"/>
In previous lessons, you learned that the Flex 4.x framework includes a container class named Group, which can be used to contain any arbitrary visual elements as children and to which a layout can be applied. A DataGroup follows the same concept, but rather than requiring the number of children to be explicitly defined, it allows you to pass a dataset, and it will automatically create one visual child for each item in the dataset. Take a look at this simple example:
<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark">
<s:DataGroup itemRenderer="spark.skins.spark.DefaultItemRenderer">
<s:dataProvider>
<s:ArrayList>
<fx:String>Jeff Tapper</fx:String>
<fx:String>Mike Labriola</fx:String>
<fx:String>Matt Boles</fx:String>
<fx:String>Steve Lund</fx:String>
</s:ArrayList>
</s:dataProvider>
<s:layout>
<s:VerticalLayout/>
</s:layout>
</s:DataGroup>
</s:Application>
Here, you have a simple Flex application with only one child, a DataGroup container. The DataGroup is instructed to use a class called DefaultItemRenderer to render each item. You’ll examine the DefaultItemRenderer and alternatives to it shortly.
After the items are rendered, a dataset is assigned to the DataGroup’s dataProvider
property. In this case, the dataset is an ArrayList. In Lesson 8, “Using Data Binding and Collections,” you learned that ArrayCollections not only provide the benefit of data binding but also have a rich set of additional features for sorting, filtering, and finding data quickly. An ArrayList is like an ArrayCollection in that it proxies an Array to provide data binding. Unlike the ArrayCollection, the ArrayList does not provide the additional functionality of sorting, filtering, or searching for items. This is why the ArrayList can be thought of as a lighter-weight version of the ArrayCollection class, concerned only with providing bindability to an underlying Array.
The DataGroup has its layout set to be vertical. When this runs, four instances of the DefaultItemRenderer will be created, one for each item in the ArrayList. The renderer will use a Label component to show each item.
As you saw in the previous example, you tell the DataGroup how to display the elements from its dataProvider by specifying a class to be used as its itemRenderer. In the last example, the DefaultItemRenderer class was utilized, which simply uses a label to display each element. You can easily create your own itemRenderer as well.
When you create your own itemRenderers, your new class can either implement the IDataRenderer interface, or you can subclass a class that already implements that interface, such as the DataRenderer class. The IDataRenderer interface simply dictates that the implementing classes have get and set functions for the data
property, which is data-typed generically as an Object. The way the itemRenderer generally works is that one instance of the renderer will be created for each element in the dataProvider (this isn’t entirely true, but the nuances of this function will be revealed later in this lesson, when you learn about virtualization), and the data
property of the itemRenderer will be set with the data for that element in the dataProvider.
In this exercise, you’ll create an itemRenderer that implements the IDataRenderer interface and displays the element in a TextInput instead of a Label.
In the DataGroup.mxml file in the default package of the src directory, you’ll find the code base shown in the previous section.
This will create an MXML file with the following contents:
<?xml version="1.0" encoding="utf-8"?>
<s:TextInput xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark">
<fx:Declarations>
<!-- Place non-visual elements (e.g., services, value objects) here -->
</fx:Declarations>
</s:TextInput>
<s:TextInput tag>
, setting an implements
attribute equal to the value mx.core.IDataRenderer
.
<?xml version="1.0" encoding="utf-8"?>
<s:TextInput xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
implements="mx.core.IDataRenderer">
<fx:Declarations>
<!-- Place non-visual elements (e.g., services, value objects) here -->
</fx:Declarations>
</s:TextInput>
<fx:Script>
block and a private variable, called data
, with a data type of Object.
<fx:Script>
<![CDATA[
private var data:Object;
]]>
</fx:Script>
This wizard will create the public get and set functions for the data
property and rename the private data
to _data
. The resulting code will look like this:
private var _data:Object;
[Bindable(event="dataChange")]
public function get data():Object
{
return _data;
}
public function set data(value:Object):void
{
if( _data !== value)
{
_data = value;
dispatchEvent(new Event("dataChange"));
}
}
As you learned in Lesson 8, code constructed in this way indicates that any elements bound to this class’s data
property will be updated automatically when this class dispatches an event named dataChanged
.
text
property to the toString()
method of the data
property.
Your renderer is now complete. All that remains is to tell the DataGroup to use it. The complete code for the renderer should look like this:
<?xml version="1.0" encoding="utf-8"?>
<s:TextInput xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
implements="mx.core.IDataRenderer"
text="{data.toString()}">
<fx:Script>
<![CDATA[
private var _data:Object;
[Bindable(event="dataChange")]
public function get data():Object
{
return _data;
}
public function set data(value:Object):void
{
if( _data !== value)
{
_data = value;
dispatchEvent(new Event("dataChange"));
}
}
]]>
</fx:Script>
<fx:Declarations>
<!-- Place non-visual elements (e.g., services, value objects) here -->
</fx:Declarations>
</s:TextInput>
<s:DataGroup itemRenderer="TextInputDataRenderer">
An alternative to implementing the IDataRenderer
class yourself is to use a base class, such as the DataRenderer class, that already implements this class. You’ll do this in the next exercise as you change ProductItem to be a DataRenderer.
For the remainder of this lesson, you’ll not be able to add or remove products from your shopping cart. That is a consequence of the major refactor you are about to perform. However, you’ll make it work again in the next lesson.
In this exercise, you’ll switch the VGroup that has the ProductItem instances to be a DataGroup that uses ProductItem as a DataRenderer.
Alternatively, if you didn’t complete the previous lesson or your code is not functioning properly, you can import the FlexGrocer-PreDataRenderer.fxp project from the Lesson10/intermediate folder. Please refer to the appendix for complete instructions on importing a project should you skip a lesson or if you have a code issue you cannot resolve.
width="100%"
attribute to the tag.
As mentioned earlier, the DataRenderer class is a subclass of Group that implements the IDataRenderer interface.
<s:DataRenderer xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
width="100%">
...
</s:DataRenderer>
product
property to the value passed to the function. You’ll need to cast the value as a Product.
override public function set data(value:Object):void{
this.product = value as Product;
}
Overriding means that you are changing the behavior of the set data method in the DataRenderer class and replacing it with your own behavior in this class. If you are unfamiliar with the concept of overriding a method, please refer to the many great articles on object oriented programming on Wikipedia.
With this small change, your ProductItem class can now function as a DataRenderer. Each time the data
property is set, it is in turn passed to the product
property, which is already bound to the controls. Next you’ll change the ShoppingView class to use a DataGroup with your new renderer.
<s:DataGroup width="100%" height="100%"
width.cartView="0" height.cartView="0"
visible.cartView="false">
</s:DataGroup>
Next, you’ll need to specify the dataProvider and itemRenderer.
itemRenderer
attribute to the opening DataGroup tag, which specifies components.ProductItem
as the itemRenderer.
<s:DataGroup width="100%" height="100%"
width.cartView="0" height.cartView="0"
visible.cartView="false"
itemRenderer="components.ProductItem">
</s:DataGroup>
dataProvider
attribute to the DataGroup, which is bound to the groceryInventory
property.
<s:DataGroup x="0" y="0" width="100%" height="100%"
width.cartView="0" height.cartView="0"
visible.cartView="false"
itemRenderer="components.ProductItem"
dataProvider="{groceryInventory}">
</s:DataGroup>
If you save the files and run the application, you’ll see the products are all rendered on top of each other, with the text being unreadable. This is happening because you haven’t specified a layout object for the DataGroup to use.
layout
property.
<s:DataGroup x="0" y="0" width="100%" height="100%"
width.cartView="0" height.cartView="0"
visible.cartView="false"
itemRenderer="components.ProductItem"
dataProvider="{groceryInventory}">
<s:layout><s:VerticalLayout/></s:layout>
</s:DataGroup>
Now as you save and run the application, the products render properly.
The “add to cart” functionality no longer works when you have completed these steps. This is expected. In the next lesson, you’ll use events to fix this problem.
Each visual object takes processor time to create and RAM to store. It is inherently inefficient to create and store visual objects that are not displayed to the user. Virtualization solves this problem by creating visual objects only for the elements that will be seen. In situations in which the user needs to scroll to see more elements, the objects are not created initially. Instead, as the user scrolls, the objects that are scrolled off the screen are recycled and reset to display the new elements that are being scrolled on-screen.
With virtualization, if a dataset of 1000 items is set in a DataGroup that has room to show 10 renderers, the application will need to create only 10 instances of the renderers rather than 1000, greatly reducing the impact on the processor and RAM.
To enable virtualization for a DataGroup, you set the useVirtualLayout
property of the Layout class to true
(it is false
by default).
<s:layout>
<s:VerticalLayout useVirtualLayout="true"/>
</s:layout>
As you know, the layout objects are used by many Flex components, not just DataGroups. However, not all these support virtualization. If you try to specify a layout to use virtualization in a component that does not support virtualization, the component will simply ignore that attribute of the layout object. In other words, even if you tell the layout of a Group to use a virtual layout, it will still create all its children, visible or not, because Groups don’t support virtualization.
In this exercise, you’ll take an existing application that has 25 items in a dataProvider of a DataGroup, but has room to show only four items at a time, and instruct it to use virtualization.
In the VirtualizedVGroup.mxml file in the default package of the src directory, you’ll find an application that contains a DataGroup with 25 items in its dataProvider and that uses a variation on the TextInputRenderer you created earlier in this lesson.
trace
statements, one from the creationComplete
event of each of the itemRenderers.
As you scroll through the items, you’ll find you can never see more than five items at any one time (and most times only four items are visible at a time). However, as you can clearly see in the Console, there are far more than five instances of the TextInputDataRenderer created.
useVirtualLayout="true"
. Save and debug the application again. Notice this time there are only five trace
statements of the TextInputDataRenderer instantiated.
Now you can see the real power of virtualization. Rather than having to create an instance of the renderer for each item in the dataProvider, which would be 25 total renderers, only 5 are created, as that is the most that can be seen in the control at any one time. There is no need to create and keep an additional 20 items in memory; instead, the same five renderers will be used to render whichever items need to be seen at any given time.
With the List class, virtualization is enabled automatically, so you do not need to explicitly tell the layout class to use useVirtualLayout
. That much is assumed. In addition to virtualization, Lists also add selectability. Selectability is the idea that the user will be presented with several items and be allowed to choose one or more of them. Lists provide a series of properties, methods, and events surrounding the ideas of selectability. For instance, the selectedIndex
and selectedItem
properties allow you to specify or retrieve what is currently selected in the list.
In this exercise, you’ll build a renderer to display the various categories shown in the top navigation of the application and specify the list displaying the categories to use that new renderer.
ItemRenderer is a subclass of DataRenderer, which additionally implements the methods specified by the itemRenderer interface. These include properties and methods related to displaying which items are and are not selected in a list.
height
of 31
and a width
of 93
. Set the source
of the image to be assets/nav_{data.name.toLowerCase()}.jpg.
<s:Image
source="assets/nav_{data.name.toLowerCase()}.jpg"
height="31" width="93"/>
If you look in the assets directory, you’ll find six files, with names such as nav_dairy.jpg, nav_deli.jpg, and so on. You may notice that the six names are very similar to the names of the categories from the category.xml file, with the difference that the names of the categories in the XML start with an uppercase letter, and in the filenames the categories start with a lowercase letter. To compensate for the difference of the upper- to lowercase letters, invoking the String class’s toLowerCase()
method forces the name to be all lowercase, so it can match the case of the file names. After the toLowerCase()
method, the category that has a name of Dairy is lowercased and is concatenated into nav_dairy.jpg.
text
is bound to the name
property of the data object.
<s:Label text="{data.name}"/>
In addition to the image, the desire is to show the category name below the image.
horizontalAlign="center"
attribute.
<s:layout>
<s:VerticalLayout horizontalAlign="center"/>
</s:layout>
Specifying a horizontalAlign
of center
will align the image and label horizontally to each other. You now have a functioning renderer that you can use in a List class to display the various categories.
The List displaying the categories is instantiated in the main application, FlexGrocer.mxml.
labelField
attribute from the instantiation of the List in the controlBarContent. Replace that attribute with the itemRenderer
for this List to be your newly created NavigationItem class. Change the height
property of the List to 52
to compensate for the larger size of the image and text.
<s:List left="200" height="52"
dataProvider="{categoryService.categories}"
itemRenderer="components.NavigationItem">
<s:layout>
<s:HorizontalLayout/>
</s:layout>
</s:List>
You just passed a dataset to a List control and had an item display for each object in the dataset. At some point you’ll also want to filter the collection of products to show only the products matching the selected category.
The first step will be to create a filter function in the ProductService class, which will accept a category id
and filter the collection to show only the matching products.
selectedCategory
, with a data type of Number
, and a default value of 1
.
private var selectedCategory:Number=1;
filterForCategory()
that accepts a Product
as an argument and returns a Boolean
. In the body of the function, return a Boo
lean indicating whether the catID
of the argument matches the selectedCategory
property.
private function filterForCategory( item:Product ):Boolean{
return item.catID == selectedCategory;
}
handleProductResult()
method, after the products
ArrayCollection is instantiated, specify a filterFunction()
of the products
property to use your new filerForCategory()
method. Next refresh the products
collection.
products.filterFunction = filterForCategory;
products.refresh();
Now, when the collection is created, the filterForCategory()
method is specified as its filter function, and the collection is refreshed, so the filter function will rerun.
filterCollection()
that accepts a numeric argument, named id
. Inside the function set the id
as the value of the selectedCategory
property, and then refresh the collection.
public function filterCollection( id:Number ):void{
selectedCategory = id;
products.refresh();
}
You now have everything you need in place to filter the collection to a specific category. All that remains is to call the filterCollection()
method whenever the category changes.
When the user selects an item from a list, a change
event is broadcast, indicating that the selected item in the list is no longer the same. In this exercise, you’ll handle the change
event, and pass the id
of the selected category to the ProductService to filter the collection so that only matching products are shown.
This will create a method named list1_changeHandler()
for you, which accepts an argument named event
, of type IndexChangeEvent
. This method will automatically be set as the change handler for your list.
protected function list1_changeHandler(event:IndexChangeEvent):void
{
// TODO Auto-generated method stub
}
// TODO
auto-generated method stub of the list1_changeHandler()
with a call to the filterCollection()
method of the productService, passing in the id
of the selected item from the list (event.target.selectedItem.categoryID
).
protected function list1_changeHandler(event:IndexChangeEvent):void
{
productService.filterCollection( event.target.selectedItem.categoryID );
}
Now, as you select products from the top category list, the products displayed in ShoppingView are updated accordingly.
In this lesson, you have:
• Populated a List control with a dataset (pages 242–245)
• Used a DataGroup with a dataset to display information with an itemRenderer (pages 245–246)
• Created an itemRenderer (pages 246–253)