itemEditors - Part 3
The previous article in this series discussed item editing events. Using events can make your application respond to what the user enters and help the user make fewer mistakes.
This article is about using itemRenderers as itemEditors - one class to do both display data and edit the data. I tend to think of it more as an itemEditor used as an itemRenderer. But that's just me.
Download source for these examples.
Further, I have to be honest and say I am not a big fan of the renderer-as-editor; I think renderers should present data and editors should edit it. There are a few occasions when I think it is a good idea to use a single class for both, but those times are very few in my opinion.
Examples
Here is an example of over-using the itemRendererAsEditor. The DataGrid on the left is a nice, clean DataGrid. All of the cells are editable and when you click or tab into a cell its editor appears. The DataGrid on the right uses itemEditors to render the cells and edit them. All you see are the editors: TextInput controls for some columns, a ComboBox for another, and a NumericStepper for the last. Lots going on, very busy to look at.
itemEditors only | itemRenderers as itemEditors |
Here is an example of using the CheckBox as both an itemRenderer and an itemEditor. I think the CheckBox works really well for this. It is clean, simple control and you can readily see whether a value is true or false. Plus you can just click it to change it. Straightforward implementation, good user experience.
Here is another example of using an itemEditor as a renderer. This List control represents a shopping cart. In it are all of the things you have added to your cart while shopping online at your favorite grocery store.
As you can see, the quantity of each item in the cart is represented by a NumericStepper. All the user has to do is change the quantity and the cart is updated. A delete button would also be a good idea here, too.
Shopping Cart
This complex editor/renderer class works as follows:
<?xml version="1.0" encoding="utf-8"?> <mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" verticalAlign="middle" paddingRight="4" paddingLeft="4" > <mx:Script> <![CDATA[ public function get quantity() : Number { return itemQuantity.value; } ]]> </mx:Script> <mx:CurrencyFormatter id="cfmt" precision="2" /> <mx:Label text="{data.name}" fontWeight="bold" fontSize="12"/> <mx:Spacer width="100%"/> <mx:NumericStepper id="itemQuantity" value="{data.quantity}" minimum="0" maximum="100"/> <mx:Label text="{cfmt.format(data.price*itemQuantity.value)}" width="66"/> </mx:HBox>
As with every itemEditor, this one has a property used as the editorDataField. In this case it is the quantity property getter function. The function retrieves the value setting of the NumericStepper (with id itemQuantity).
As an itemRenderer, this component must also display the current quantity (as well as the product name, price, and sub-total). These values are displayed through data binding. The sub-total is actually an ActionScript expression, multiplying the price by the value of the NumericStepper. As the NumericStepper is changed so does the sub-total.
Now you are probably wondering how to get the grand total below the shopping cart to update as the NumericSteppers are changed. Simply changing the sub-total and the quantity field of the itemRenderer/Editor will not update the grand total. Remember that the editor does not commit the new value into the data provider until after the edit completes. In other words, if you increase the value of the NumericStepper for the Snow Peas row, the grand total will not update until focus leaves the Snow Peas row. This is so you can validate the information as shown in previous articles.
For a shopping cart like this, you want the grand total to update as the user changes the NumericSteppers. So you have to force the situation a little.
The first thing you do is have the itemRenderer class implment the IDropInListItemRenderer interface. This gives you access to the listData which contains a reference to the list itself and, through that, to the dataProvider.
The code demonstrating this is available in the download. Look for the ShoppingCartRendererExtra.mxml file.
Once you have the listData you can have the change event on the NumericStepper force an update on the dataProvider:
private function forceUpdate() : void { // Access the collection - listData.owner is the List and from there you have its dataProvider. var ac:ArrayCollection = (listData.owner as List).dataProvider as ArrayCollection; // update the quantity field from the numeric stepper. This is what the List will automatically // do when editing completes, but since you want to see the grand total change as the NumericStepper // changes, you have to force things a bit. data.quantity = itemQuantity.value; // finally, tell the collection the data changed. this will cause the collection to // dispatch its own change event which is then picked up by the main application. ac.itemUpdated(data); }
When the NumericStepper's change event triggers this event handler, the ArrayCollection has the item updated immediately, rather than waiting for the List to complete editing the cell. If the main application is listening for a COLLECTION_CHANGE event on the collection, the grand total can be calculated:
<mx:ArrayCollection id="shoppingCartDB" source="{shoppingCartArray}" collectionChange="updateCartTotal()" /> ... private function updateCartTotal() : void { if( cartTotal ) { var total:Number = 0; for(var i:int=0; i < shoppingCartDB.length; i++) { var record:Object = shoppingCartDB.getItemAt(i); total += record.price * record.quantity; } cartTotal.text = cfmt.format(total); } }
Conclusion
Take care when turning an itemRenderer into an itemEditor. The user should have a straightforward interface with a single purpose when editing a cell or record. I personally prefer to separate the functions, but there are times when using an itemRenderer as an itemEditor can make sense, even if you have to go the extra mile as with this shopping cart grand total example.
Posted by pent at 12:22 PM | Comments (9)
June 04, 2008
itemEditors - Part Two
Editing Events and Complex Editors
In the last article of this series you saw how to make some simple inline itemEditors. If you have read the series on itemRenderers, then you noticed how similar the two are.
The key to making an itemEditor work is a) naming the class using the itemEditor property and b) naming the value property of the itemEditor using theeditorDataField property.
In this article I'll show you how to use events to do some simple data validation and to prevent certain cells from being edited. In the course of this you will see how to make more complex itemEditors.
A word of caution here: by "complex" I do not mean editors with many controls and layouts. I really mean slightly more complex than inline itemEditors. The reason being is that I think it is unfair to ask users to make complex edits within a list or cell of a DataGrid. An editor should be focused only one one thing: the contents of a cell. For example, if you are using the List control and presenting a shopping cart, it is not unreasonable to allow the user to change the quantity of the items in the cart by letting them edit that value right in the cell. What would be unreasonable is to allow them to change the item itself, the colors, quantity, special instructions, and so forth. Or in other words, allow them to shop for items right from the cart when you have a whole site that does that. The cart is just a checkout convenience. Sure, let them add an extra tub of ice cream or delete a bag of chips, but don't have them turn the bag of chips into a two boxes of whole wheat pasta.
The itemEditEnd event
Let's say you have a DataGrid which helps you mange inventory. One of things you can do is change part numbers, but you cannot allow a part number to be blank. Using the default itemEditor, the TextInput control, you can click on a cell in the "Part #" column, press the delete key and erase the part number. This is one technique to prevent that.
<mx:DataGrid x="10" y="64" editable="true" dataProvider="{inventoryDB}" itemEditEnd="verifyInput(event)"> <mx:columns> <mx:DataGridColumn headerText="Product" dataField="product"/> <mx:DataGridColumn headerText="Part #" dataField="part"/> <mx:DataGridColumn headerText="Type" dataField="type" itemEditor="editors.ProductTypeEditor" editorDataField="type"/> <mx:DataGridColumn headerText="Quantity" dataField="quantity"/> </mx:columns> </mx:DataGrid>
The list controls dispatch an itemEditEnd event whenever editing is about to be completed. The event happens before the data is commited back to the dataProvider. By handling this event you have the option of changing the data, validating the data, and stopping the commit if necessary. For this example, the verifyInput() function will make sure the product part number is not empty.
private function verifyInput( event:DataGridEvent ) : void { // it is OK if the user cancels the edit if( event.reason == DataGridEventReason.CANCELLED ) return; // grab the instance of the itemEditor. For this DataGrid, only the // TextInput control is used as the editor, so it is safe to get the // editor no matter what column has been edited. var editor:TextInput = (event.currentTarget as DataGrid).itemEditorInstance as TextInput; // if the edit is on the part number column, make sure it is not blank if( event.dataField == "part" ) { if( editor.text.length == 0 ) { // call event.preventDefault() so the edit will not continue and store the // blank value event.preventDefault(); // give the editor an error to display to the user editor.errorString = "You must enter a part number"; return; } } // handle other columns here }
The event is a DataGridEvent and contains some very useful properties. The reason property tells you why the event was dispatched. If the user pressed the ESCAPE key or clicked outside of the DataGrid the reason will be DataGridEventReason.CANCELLED. You may want to ignore this event as I have done and just let the DataGrid to its default action which is to cancel the edit and restore the previous value.
If you have decided to handle the event then you will need the itemEditor to get to its properties. The event's currentTarget property contains the control which I have cast to DataGrid. The DataGrid has an itemEditorInstance property which I cast to TextInput which is the type of itemEditor for this example.
This event handler is called for any cell so you must determine if the edit is something you are interested in pursuing. I check the event's dataFieldproperty to make sure it is the "part" column. If so, I test the editor's text property to see if there are any characters in it. If there are no characters, two things happen:
First: the event.preventDefault() is called. This is how to prevent the edit from happening - prevent the DataGrid from storing the new value back into the dataProvider. For the user, they will have pressed TAB or ENTER and nothing will appear to happen. The preventDefault() function will keep the itemEditor in place.
Second: I put an errorString onto the TextInput control. This is optional, but it does signal the user that there is something wrong. Afterall, they pressed the TAB or ENTER key and nothing happened.
The itemEditBeginning Event
There are times you might want to prevent a cell from being edited. You could set the DataGridColumn's editable property to false, but that prevents every cell from being edited. Suppose you just want to make some of the cells in the column uneditable? You can determine whether a cell is editable or not using the itemEditBeginning event.
<mx:DataGrid x="10" y="64" editable="true" dataProvider="{inventoryDB}" itemEditEnd="verifyInput(event)" itemEditBeginning="allowForEdit(event)"> <mx:columns> <mx:DataGridColumn headerText="Product" dataField="product"/> <mx:DataGridColumn headerText="Part #" dataField="part"/> <mx:DataGridColumn headerText="Type" dataField="type" itemEditor="editors.ProductTypeEditor" editorDataField="type"/> <mx:DataGridColumn headerText="Quantity" dataField="quantity"/> </mx:columns> </mx:DataGrid>
Handling the itemEditBeginning event gives you the option of dynamically deciding the editability of a cell. In this example, the data has a field calledpermanent on each record. The idea is that permanent=true means the product name is an unchangable value so the product cell for that row cannot be edited. This is handled by the allowForEdit() function:
private function allowForEdit(event:DataGridEvent) : void { // if the field to be edited is a product, prevent the user from making // changes if the permanent flag is true<. You can use more complex logic, // of course. if( event.dataField == "product" ) { var item:Object = ((event.currentTarget as DataGrid).dataProvider as ArrayCollection)[event.rowIndex]; if( item.permanent ) { event.preventDefault(); } } // handle other columns here }
Again, the event is a DataGridEvent and here I have checked the dataField property of the event to make sure it is the "product" field I am dealing with. I can then get the record from the dataProvider of the DataGrid using the currentTarget property of the event and cast that to DataGrid. I then cast the DataGrid's dataProvider to ArrayCollection and get the event.rowIndex value. I could also have used the inventoryDB ArrayCollection directly in this function since they are in the same file, but this is more generic.
Once I have the record I can query its permanent property and if it is true, call the event.preventDefault() function to disable editing of that cell. In the case, the default behavior of itemEditBeginning is to present the itemEditor; preventing the default behavior makes the cell uneditable.
Editing Limitations
While I was proof reading this article I thought of something you might try and do and offer a warning. When you are using these edit events and trying to determine if the event should proceed, you may be tempted to make a call to a backend or server process. For example, you may have a web service where you can validate a part number. You may be tempted, while inside of the itemEditEnd event, to make a web service call and validate what the user just entered. Seems logical, right?
Logical maybe, but it won't work. The reason is that data service calls are asynchronous. You can make the call, sure, but the result will be returned sometime later - well after your event handler has exited. In fact, your call won't actually be made until your function exits. Your call is queued and when the Flex framework exits the function the request will be made and then the result will be returned by your web service's result handler.
So there is no way to do this type of server-side validation while editing cells. You should query the server, when your application starts, for the data to validate against, then use that while the cells are being edited.
Conclusion
The ability to dynamically allow editing and to validate the edit is a excellent way to give your users a better experience. You can help them make fewer mistakes and give feedback during the editing process. You can prevent them from editing certain data and make it easier for yourself to write the application since you do not have to validate what the user cannot change.
In next article I'll cover itemRenderers used as itemEditors.
Posted by pent at 01:15 PM | Comments (1)
May 29, 2008
itemEditors - Part 1
inline itemEditors
I recently completed a series on itemRenderers - customizations to list controls which format the display of the list contents. Displaying and rendering content is very cool and with Flex you can do nearly anything you can imagine.
This new series covers itemEditors - a way to allow data to be changed directly inside of a list control. This first article covers inline itemEditors which are very simple, though quite useful, components you write directly inside your MXML files. The other articles in the series will cover more complex editing, validation, events, and using itemRenderers as itemEditors.
The source code to this article is available by downloading it here.
TextInput Editor
It is nice to edit directly in the list controls. You can imagine a DataGrid of warehouse inventory where you can adjust the content right in the grid without needing a special pop-up. The list controls have a built in editor - a TextInput control - which appears whenever the user clicks the mouse in an editable area, either a row (for a List), a branch (for a Tree), or a cell (for a DataGrid). All you need to do is set the list control's editable property to true. For a DataGrid you can exclude a column from being edited by setting the DataGridColumn's editable property to false.
Before editing a cell
|
After clicking on a cell, the editor opens and the content is ready for editing.
|
itemEditors differ from itemRenderers in that only one instance of the itemEditor is seen - just on the cell being edited. The itemEditor is not seen until the cell to be edited receives input focus. Then the itemRenderer is hidden and the itemEditor is moved to that position, sized to fit the area, and given the data for the record. When editing is finished (by moving focus to another location), the list control copies the new value from the editor to the dataProvider record.
Using the image above as an example, when the user clicks in a cell of the "Part #" column, the dataProvider[row][dataField] value is given to the text property of the itemEditor (TextInput) control. When editing is finished, the text property value from the itemEditor (TextInput) control is copied to the dataProvider[row][dataField]. The dataProvider, being a collection, dispatches an event in response to the update.
While the default TextInput control makes a fine editor, it really only works for the most simple of cases. It works fine for String values, for example, such as a book title, author name, or product number. If you need more control or want to validate the user's input, then you need to take matter into your own hands.
Flex Controls as itemEditors
Here is how you make an itemEditor which only accepts numeric values:
<mx:DataGrid x="46" y="270" editable="true" dataProvider="{employeeDB}"> <mx:columns> <mx:DataGridColumn headerText="Name" dataField="name"/> <mx:DataGridColumn headerText="Position" dataField="position"/> <mx:DataGridColumn headerText="Age" dataField="age"> <mx:itemEditor> <mx:Component> <mx:TextInput restrict="0-9" maxChars="3" /> </mx:Component> </mx:itemEditor> </mx:DataGridColumn> </mx:columns> </mx:DataGrid>
A very common control to use for an itemEditor is the CheckBox. This is very useful for editing Boolean values. Here is an example of using the CheckBox to edit the values for an "In Stock" column of an inventory program:
<mx:DataGrid x="531" y="273" editable="true" dataProvider="{inventoryDB}"> <mx:columns> <mx:DataGridColumn headerText="Product" dataField="product"/> <mx:DataGridColumn headerText="Part #" dataField="part"/> <mx:DataGridColumn headerText="In Stock?" dataField="inStock" labelFunction="inStockLabeler" itemEditor="mx.controls.CheckBox" editorDataField="selected" /> <mx:DataGridColumn headerText="Quantity" dataField="quantity"/> </mx:columns> </mx:DataGrid>
In this example the content of the cells in this column are rendered using a labelFunction (inStockLabeler) which could display anything such as "Yes", "No", "In Stock", or "Out of Stock". The itemEditor property is set to the mx.controls.CheckBox class. And there is another, equally important, property set on the DataGridColumn: editorDataField. This field indicates the property of the itemEditor class to use to fetch the value when editing is finished. In this case it is the CheckBox's selected property. When editing is finished, the DataGrid will use the CheckBox's selected property to replace the inStock property in the data record.
You may wonder why the example with the TextInput did not supply the editorDataField property. That is because the default value for editorDataField is "text" which just happens to be name of the property on the TextInput control holding the value.
You can use this same technique with a number of Flex controls. Here is one for an order quantity column using NumericStepper:
<mx:DataGrid x="531" y="82" editable="true" dataProvider="{inventoryDB}"> <mx:columns> <mx:DataGridColumn headerText="Product" dataField="product"/> <mx:DataGridColumn headerText="Part #" dataField="part"/> <mx:DataGridColumn headerText="In Stock?" dataField="inStock"/> <mx:DataGridColumn headerText="Quantity" dataField="quantity" itemEditor="mx.controls.NumericStepper" editorDataField="value"/> </mx:columns> </mx:DataGrid>
Notice the editorDataField is "value" - the property of the NumericStepper which holds the current value of the control. Make sure you use the fully-qualified class name for the itemEditor property.
Complex Editor
Now suppose you want to do something a little more complex that doesn't have a ready-made Flex control available. Here is one which allows a credit card number to be entered using 4 separate 4-digit fields:
<mx:DataGrid x="46" y="463" editable="true" dataProvider="{accountDB}" width="302"> <mx:columns> <mx:DataGridColumn headerText="Account" dataField="account" width="100"/> <mx:DataGridColumn headerText="Credit Card" dataField="ccard" editorDataField="value"> <mx:itemEditor> <mx:Component> <mx:HBox> <mx:Script> <![CDATA[ public function get value() : String { return part1.text+part2.text+part3.text+part4.text; } override public function set data(value:Object):void { super.data = value; part1.text = value.ccard.substr(0,4); part2.text = value.ccard.substr(4,4); part3.text = value.ccard.substr(8,4); part4.text = value.ccard.substr(12,4); } ]]> </mx:Script> <mx:TextInput id="part1" maxChars="4" restrict="[0-9]" width="40" /> <mx:TextInput id="part2" maxChars="4" restrict="[0-9]" width="40" /> <mx:TextInput id="part3" maxChars="4" restrict="[0-9]" width="40" /> <mx:TextInput id="part4" maxChars="4" restrict="[0-9]" width="40" /> </mx:HBox> </mx:Component> </mx:itemEditor> </mx:DataGridColumn> </mx:columns> </mx:DataGrid>
This inline itemEditor follows the same rules as other itemEditors and names the editorDataField as "value". The component chosen for the itemEditor is the HBox - which does not have a "value" property. To make this itemEditor work, a getter function named value is created to return the concatenation of the 4 input fields. Now when editing for the cell completes, the DataGrid can call upon the value property of the itemEditor and it will receive the combined fields.
You can also see that I have overridden the data setter function. In that function I split up the credit card number among the four TextInput fields. This is the technique you use to display the data to be edited. The editorDataField is the property used to retrieve the new value.
Conclusion
In this article you've seen how to create an inline itemEditor - from simply naming a class to creating a complex class right within the MXML tags. By naming the property of the editor class which contains the final editor value, the DataGrid can retreive the value from the editor instance and replace the current value in the data.
The next article covers more complex itemEditors and editing events.
Posted by pent at 05:39 PM | Comments (7)
April 02, 2008
itemRenderers: Part 5: Efficiency
If you are displaying a large number of itemRenderers - either in the DataGrid or AdvancedDataGrid - your application's performance may be adversely affected if you do not code these itemRenderers effeciently. Here are some tips that might help:
- Limit the number of columns using itemRenderers. Do you really need to have every column be a custom itemRenderer? Sometimes you do, but is all that glitz overwhelming the user?
- Try not to change the style of the elements in your itemRenderer too frequenty. If you need to switch styles (eg, green for positive values, red for negative values), consider having 2 controls preset with those styles and making one visible. Changing styles is one of the more time-consuming tasks in Flex.
- Do not use Containers as the basis for your itemRenderers. Containers have a lot of overhead. They are fine for limited use, but it would be more efficient to write your itemRenderers based on UIComponent.
Switching Styles
Here's an itemRenderer which switches components depending on the value of the data field.
<mx:Canvas> <mx:Script><![CDATA private function lessThanZero() : Boolean { return data.price < 0; } ]]></mx:Script> <mx:Label text="{data.price}" color="#FF0000" visible="{lessThanZero()}" /> <mx:Label text="{data.price}" color="#00FF00" visible="{!lessThanZero()}" /> </mx:Canvas>
This will be faster than setting the style. Some other things to keep in mind:
- Avoid data binding to styles. Not only is changing styles slower than most operations, adding data binding code on top of it just makes it worse.
- Use a Canvas or extend ListItemRenderer or as the root of the itemRenderer. This allows you to place controls on top of each other.
Extending UIComponent
By far the most efficient way to write an itemRenderer is to extend UIComponent using an ActionScript class. You'll have complete control of the code and the renderer will be as efficient as possible.
Let's start with the example above, switching styles, and write a simple itemRenderer extending UIComponent.
package renderers { import mx.controls.listClasses.IListItemRenderer; import mx.core.UIComponent; public class PriceItemRenderer extends UIComponent implements IListItemRenderer { public function PriceItemRenderer() { super(); } } }
You'll notice that not only did I write the class to extend UIComponent, I also have it implementing the IListItemRenderer interface. It is necessary to do this because a list control will expect any renderer to implement this interface and if you do not, you'll get a runtime error as the list attempts to cast the renderer to this interface.
If you read the documentation on IListItemRenderer you'll see that is an amalgamation of many other interfaces, most of which UIComponent implements for you. But there is one interface extended by IListItemRenderer that UIComponent does not implement: IDataRenderer. This requires you to add the code to give the itemRenderer class the data property you've been using all along.
If you attempt to use this class without implementing IDataRenderer you'll get these errors when you compile the code:
1044: Interface method get data in namespace mx.core:IDataRenderer not implemented by class renderers:PriceItemRenderer.
1044: Interface method set data in namespace mx.core:IDataRenderer not implemented by class renderers:PriceItemRenderer.
Edit this class and change it to the following:
package renderers { import mx.controls.listClasses.IListItemRenderer; import mx.core.UIComponent; import mx.events.FlexEvent; public class PriceItemRenderer extends UIComponent implements IListItemRenderer { public function PriceItemRenderer() { super(); } // Internal variable for the property value. private var _data:Object; // Make the data property bindable. [Bindable("dataChange")] // Define the getter method. public function get data():Object { return _data; } // Define the setter method, and dispatch an event when the property // changes to support data binding. public function set data(value:Object):void { _data = value; dispatchEvent(new FlexEvent(FlexEvent.DATA_CHANGE)); } } }
I took the code directly from the Flex documentation for IDataRenderer, so you don't even have to type it yourself.
With that out of the way we can add in the two labels.
- Add variables to hold the two labels.
private var posLabel:Label; private var negLabel:Label;
- Modify the set data function to call invalidateProperties(). This is important because the change of the data has to make the text in the labels change AND to change their visibility.
public function set data(value:Object):void { _data = value; invalidateProperties(); dispatchEvent(new FlexEvent(FlexEvent.DATA_CHANGE)); }
Calling invalidateProperties() tells the Flex framework to call the commitProperties() function at the apppriate time. - Override createChildren() and create the labels, adding them to the display list of the component. Notice that in addition to creating the labels, their styles and visible are also set.
override protected function createChildren() : void { super.createChildren(); posLabel = new Label(); posLabel.visible = false; posLabel.setStyle("color", 0x00FF00); addChild(posLabel); negLabel = new Label(); negLabel.visible = false; negLabel.setStyle("color", 0xFF0000); addChild(negLabel); }
- Override commitProperties() to set the labels' text and visibility. In the past you've been overriding set data to make this type of change, and you can do that in this class, too, if you prefer.
override protected function commitProperties():void { super.commitProperties(); posLabel.text = data.price; negLabel.text = data.price; posLabel.visible = Number(data.price) > 0; negLabel.visible = Number(data.price) < 0; }
- Override updateDisplayList() to size and position the labels. You must size the labels because their default size is 0x0. This is another thing a Container class will do for you. Since this is a pretty simple itemRenderer you can just set the labels' size to match the size of the itemRenderer.
override protected function updateDisplayList( unscaledWidth:Number, unscaledHeight:Number ) : void { super.updateDisplayList(unscaledWidth, unscaledHeight); posLabel.move(0,0); posLabel.setActualSize(unscaledWidth,unscaledHeight); negLabel.move(0,0); negLabel.setActualSize(unscaledWidth, unscaledHeight); }
All this probably seems a bit complicated just to do this, but keep in mind that using a container will add a lot more code than this.
UIComponent Notes
The UIComponent class is the basis for all visual Flex components - controls and containers. Here are some tips about using UIComponent as your itemRenderer.
- UIComponent imposes no layout restrictions on its children (unlike a Container). You have to position and size the children yourself.
- It is also possible to draw graphics and position children beyond the size specified in updateDisplayList().
- If you plan on using variableRowHeight in your list, you should also override the measure() function to give the list an idea of how big the itemRenderer is.
- To use UIComponent as an itemRenderer you must implement IDataRenderer.
- To use the listData property you must implement IDropInListItemRenderer; that was covered in a previous article of this series.
Posted by pent at 11:50 AM | Comments (17)
March 14, 2008
itemRenderers: Part 3: Communication
In the previous article of this series I showed you how to make external itemRenderers in both MXML and ActionScript. In the examples I've been using there is a Button which dispatches a custom event - BuyBookEvent - so the application can react to it. This article covers communication with itemRenderers in more detail.
There is a rule I firmly believe must never be violated: you should not get hold of an instance of an itemRenderer and change it (setting public properties) or call its public methods. This, to me, is a big no-no. The itemRenderers are hard to get at for a reason which I talked about in the first article: the itemRenderers are recycled. Grabbing one breaks the Flex framework.
With that rule in mind, here are things you can do with an itemRenderer:
- An itemRenderer can dispatch events via its list owner (you've seen bubbling, this is a better practice which you'll see below).
- An itemRenderer can use static class members. This includes Application.application. If you have values stored "globally" on your application object, you can reach them that way.
- An itemRenderer can use public members of the list which owns it. You'll see this below.
- An itemRenderer can use anything in the data record. You might, for example, have an item in a record that's not for direct display but which influences how an itemRenderer behaves.
Dynamically Changing the itemRenderer
Here is the MXML itemRenderer from the previous article used for a TileList. We're going to make it bit more dynamic by having it react to changes from an external source (I called this file BookItemRenderer.mxml):
<?xml version="1.0" encoding="utf-8"?> <mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" width="250" height="115" > <mx:Script> <![CDATA[ ]]> </mx:Script> <mx:Image id="bookImage" source="{data.image}" /> <mx:VBox height="115" verticalAlign="top" verticalGap="0"> <mx:Text text="{data.title}" fontWeight="bold" width="100%"/> <mx:Spacer height="20" /> <mx:Label text="{data.author}" /> <mx:Label text="Available {data.date}" /> <mx:Spacer height="100%" /> <mx:HBox width="100%" horizontalAlign="right"> <mx:Button label="Buy" fillColors="[0x99ff99,0x99ff99]"> <mx:click> <![CDATA[ var e:BuyBookEvent = new BuyBookEvent(); e.bookData = data; dispatchEvent(e); ]]> </mx:click> </mx:Button> </mx:HBox> </mx:VBox> </mx:HBox>
Suppose you are showing a catalog of items in a TileList. You also have a Slider (not part of the itemRenderer) which lets the user give a range of prices; all items which fall outside of the range should fade out (the itemRenderers' alpha value should change). You need to tell each itemRenderer that the criteria has changed so that they can modify their alpha values.
Your override of set data might look something like this:
override public function set data( value:Object ) : void { super.data = value; if( data.price < criteria ) alpha = 0.4; else alpha = 1; }
The question is: how to change the value for criteria? The "best practice" for itemRenderers is to always have them work on the data they are given. In this case, it is unlikely, and impractical, to have the test criteria be part of the data. So that leaves a location outside of the data:
- Part of the list itself. That is, your list (List, DataGrid, TileList, etc) could be a class that extends a list control and which has this criteria as a public member.
- Part of the application as global data.
For me, the choice is the first one: extend a class and make the criteria part of that class. After all, the class is being used to display the data, the criteria is part of that display. For this example, I would extend TileList and have the criteria as a public data member.
package { import mx.controls.TileList; public class CatalogList extends TileList { public function CatalogList() { super(); } private var _criteria:Number = 10; public function get critera() : Number { return _criteria; } public function set criteria( value:Number ) : void { _criteria = value; } } }
The idea is that a control outside of the itemRenderer can modify the criteria by changing this public property on the list control.
listData
The itemRenderers have access to another piece of data: information about the list itself and which row and column (if in a column-oriented control) they are rendering. This is known as listData and it could be used like this in the BookItemRenderer.mxml itemRenderer example:
override public function set data( value:Object ) : void { super.data = value; var criteria:Number = (listData.owner as MyTileList).criteria; if( data.price < criteria ) alpha = 0.4; else alpha = 1; }
Place this code into the <mx:Script> block in the example BooktItemRenderer.mxml code, above.
The listData property of the itemRenderer has an owner field which is the control to which the itemRenderer belongs. In this example, it is the MyTileList - my extension of TileList - which is the owner. Casting the owner field to MyTileList allows the criteria to be fetched.
IDropInListItemRenderer
Access to listData is available when the itemRenderer class implements the IDropInListItemRenderer interface. Unfortunately, UI container components do not implement the interface which gives access to the listData. Control component such as Button and Label do, but for containers you have to implement the interface yourself.
Implementing this interface is straightforward and found in the Flex documentation. Here's what you have to do for our BookItemRenderer class:
- Have the class implement the interface.
<mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" ... implements="mx.controls.listClasses.IDropInListItemRenderer">
- Add the set and get functions to the <mx:Script> block in the itemRenderer file.
import mx.controls.listClasses.BaseListData; private var _listData:BaseListData; public function get listData() : BaseListData { return _listData; } public function set listData( value:BaseListData ) : void { _listData = value; }
When the list control sees that the itemRenderer implements the IDropInListItemRenderer interface it will create a listData item and assign it to every itemRenderer instance.
invalidateList()
Setting the criteria in my class isn't as simple as assigning a value. Doing that won't tell the Flex framework that the data has been changed. The change to the criteria must trigger an event. Here's the modification to the set criteria function:
public function set criteria( value:Number ) : void { _criteria = value; invalidateList(); }
Notice that once the _criteria value has been set it calls invalidateList(). This causes all of the itemRenderers to be reset with values from the dataProvider and have their set data functions called.
The process then looks like this:
- The itemRenderer looks into its list owner for the criteria to use to help it determine how to render the data.
- The list owner class, and extension of one of the Flex list classes, contains public properties read by the itemRenderer(s) and set by external code - another control or ActionScript code (perhaps as the result of receiving data from a remote call).
- When the list's property is set it calls the list's invalidateList() method. This triggers a refresh of the itemRenderers, causing them to have their data reset (and back to step 1).
Events
In the previous articles I showed how to use event bubbling to let the itemRenderer communicate with the rest of the application. I think this is certainly quick. But I also think there is a better way, one which fits the assumption that an itemRenderer's job is to present data and the control's job is to handle the data.
The idea of the MyTileList control is that it is the visual - the view - of the catalog of books for sale. When a user picks a book and wants to buy it, it should be the responsibility of the list control to communicate that information to the application. In other words:
<CatalogList bookBuy="addToCart(event)" />
The way things are set up right now, the event bubbles up and bypasses the TileList. The bubbling approach doesn't assoicate the event (bookBuy) with the list control (TileList), allowing you to move the control to other parts of your application. For instance, if you code the event listener for bookBuy on the main Application, you won't be able to move the list control to another part of the application. You'll have to move that event handler, too. If, on the other hand you have the event associated with the control you just move the control.
Look at it this way: suppose the click event on the Button wasn't actually an event dispatched by the Button but bubbled up from something inside of the button. You'd never be able to do: <mx:Button click="doLogin()" label="Log in" /> you would have to put the doLogin() function someplace else and that would make the application even harder to use.
I hope I've convinced you, so here's how to change the example from bubbling to dispatching from the list control.
First, you have to add metadata to the CatalogList control to let the compiler know the control dispatches the event:
import events.BuyBookEvent; import mx.controls.TileList; [Event(name="buyBook",type="events.BuyBookEvent")] public class CatalogList extends TileList {
Second, add a function to CatalogList to dispatch the event. This function will be called by the itemRenderer instances:
public function dispatchBuyEvent( item:Object ) : void { var event:BuyBookEvent = new BuyBookEvent(); event.bookData = item; dispatchEvent( event ); } }
Third, change the Buy button code in the itemRenderer to invoke the function:
<mx:Button label="Buy" fillColors="[0x99ff99,0x99ff99]"> <mx:click> <![CDATA[ (listData.owner as CatalogList).dispatchBuyEvent(data); ]]> </mx:click> </mx:Button>
Now the Button in the itemRenderer can simply invoke a function in the list control with the data for the record (or anything else that is appropriate for the action) and pass the responsibility of interfacing with the rest fo the application onto the list control.
The list control in this example dispatches an event with the data. The application can add event listeners for this event either using ActionScript or, because of the [Event] metadata in the CatalogList.as file, MXML; using [Event] metadata makes it easier for developers to use your code.
Summary
itemRenderers should communicate any actions using events. Custom events allow you to pass information with the event so the consumer of the event doesn't have to reach out to the itemRenderer for any data.
itemRenderers should "react" to changes in data by overriding their set data functions. Inside of the function they can access values in their listData.owner. They could also access data stored in a static class or in the main application via Application.application.
In the next article we'll look at incorporating states into itemRenders.
Posted by pent at 10:25 AM | Comments (17)
March 06, 2008
itemRenderers: Part 2: External renderers
In Part 1 of this series I showed you how to make an inline itemRenderer. That is, an itemRenderer whose MXML tags and ActionScript code are in the same file as the list using the itemRenderer. The code is "in line" with the rest of the code in the file.
You'll also recall that I said you should think of inline itemRenderers are being separate classes. The Flex compiler in fact extracts that inline code and makes a class for you. What we're going to do in this article is make the class ourselves. The benefit of inline itemRenderers is that the code is in the same place as the list, but that's also a drawback when the itemRenderer becomes complex.
Extracting the itemRenderer into an external file has several benefits:
- The itemRenderer can easily be used in multiple lists;
- the code is easier to maintain;
- you can use Flex Builder's Design View to sketch out the initial itemRenderer.
An MXML itemRenderer
From the previous article you saw there was a complex itemRenderer used for a DataGrid:
<mx:DataGridColumn headerText="Title" dataField="title"> <mx:itemRenderer> <mx:Component> <mx:HBox paddingLeft="2"> <mx:Script> <![CDATA[ override public function set data( value:Object ) : void { super.data = value; var today:Number = (new Date()).time; var pubDate:Number = Date.parse(data.date); if( pubDate > today ) setStyle("backgroundColor",0xff99ff); else setStyle("backgroundColor",0xffffff); } ]]> </mx:Script> <mx:Image source="{data.image}" width="50" height="50" scaleContent="true" /> <mx:Text width="100%" text="{data.title}" /> </mx:HBox> </mx:Component> </mx:itemRenderer> </mx:DataGridColumn>
The itemRenderer is based on an HBox, contains an Image and a Text, and the background color is set according to the pubDate field of the item record. You can write this same itemRenderer as an external file using these steps:
- If you are using Flex Builder, create a new MXML Component file (I've named mine GridColumnSimpleRenderer, but use whatever you like) and set the root tag to be HBox. Don't worry about the size.
- If you are using the SDK alone, create a new MXML file (call it GridColumnSimpleRenderer.mxml) and set the root tag to be HBox.
- With the file open, copy everything between <mx:HBox> and </mx:HBox>, but do not copy those tags since they are already in the file. The result should look something like this:
<?xml version="1.0" encoding="utf-8"?> <mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" width="400" height="300"> <mx:Script> <![CDATA[ override public function set data( value:Object ) : void { super.data = value; var today:Number = (new Date()).time; var pubDate:Number = Date.parse(data.date); if( pubDate > today ) setStyle("backgroundColor",0xff99ff); else setStyle("backgroundColor",0xffffff); } ]]> </mx:Script> <mx:Image source="{data.image}" width="50" height="50" scaleContent="true" /> <mx:Text width="100%" text="{data.title}" /> </mx:HBox>
- Save the file.
Now modify the DataGridColumn definition by removing the inline itemRenderer and replacing it with this:
<mx:DataGridColumn headerText="Title" dataField="title" itemRenderer="GridColumnSimpleRenderer">
Now run the application. You'll get a surprise.
The surprise is how tall the rows are. That's because of the presence of height="300" on the itemRenderer.
Determining an itemRenderer's width and height
The list control always sets the itemRenderer's width. In this example, the explicit width="400" is ignored. You should write your itemRenderer to assume the width will change as the user changes the column or list's width.
The height is a different matter. If the list has an explicit rowHeight set, it will impose that height on each row, ignoring any height you've set on the itemRenderer. However, if you set the list's variableRowHeight property to true, then the list will seriously consider the itemRenderer's height. In this example, the height is explicitly set to 300, so each row is 300 pixel's high.
To fix this, remove the explict height from the itemRenderer file and the application will work correctly.
Dynamically Changing the itemRenderer
In this example the set data function has been overridden to examine the data and set the itemRenderer's backgroundColor. This is very common. Overriding set data allows you to intercept the time when the data is being changed for a new row and you can you make style changes.
Common mistakes are:
- Forgetting to call super.data = value; this is VITAL - failure to do this will really mess up your itemRenderer;
- Forgetting to reset the style(s) if any tests fail. It might be tempting to just set the color when the pubDate is in the future, but you have to remember that itemRenderers are recycled and so the else statement is very necessary.
An ActionScript itemRenderer
Now we'll write another itemRenderer, this time using an ActionScript class. In the previous article there is a TileList with this inline itemRenderer:
<mx:itemRenderer> <mx:Component> <mx:HBox verticalAlign="top"> <mx:Image source="{data.image}" /> <mx:VBox height="115" verticalAlign="top" verticalGap="0"> <mx:Text text="{data.title}" fontWeight="bold" width="100%"/> <mx:Spacer height="20" /> <mx:Label text="{data.author}" /> <mx:Label text="Available {data.date}" /> <mx:Spacer height="100%" /> <mx:HBox width="100%" horizontalAlign="right"> <mx:Button label="Buy" fillColors="[0x99ff99,0x99ff99]"> <mx:click> <![CDATA[ var e:BuyBookEvent = new BuyBookEvent(); e.bookData = data; dispatchEvent(e); ]]> </mx:click> </mx:Button> </mx:HBox> </mx:VBox> </mx:HBox> </mx:Component> </mx:itemRenderer>
We'll make that into an ActionScript, external, itemRenderer. You'll need to follow these steps:
- Create a new ActionScript class. Call it BookTileRenderer.as and make it extend HBox, just like the inline itemRenderer.
package { import flash.events.MouseEvent; import mx.containers.HBox; import mx.containers.VBox; import mx.controls.Button; import mx.controls.Image; import mx.controls.Label; import mx.controls.Spacer; import mx.controls.Text; public class BookTileRenderer extends HBox { public function BookTileRenderer() { super(); } } }
- Create member variables to hold the references to the child components.
private var coverImage:Image; private var titleText:Text; private var spacer1:Spacer; private var authorLabel:Label; private var pubdateLabel:Label; private var spacer2:Spacer; private var buyButton:Button;
- Override the createChildren() function to create the child components and add them to the HBox.
override protected function createChildren():void { coverImage = new Image(); addChild(coverImage); var innerBox:VBox = new VBox(); innerBox.explicitHeight = 115; innerBox.percentWidth = 100; innerBox.setStyle("verticalAlign","top"); innerBox.setStyle("verticalGap", 0); addChild(innerBox); titleText = new Text(); titleText.setStyle("fontWeight","bold"); titleText.percentWidth = 100; innerBox.addChild(titleText); spacer1 = new Spacer(); spacer1.explicitHeight = 20; innerBox.addChild(spacer1); authorLabel = new Label(); innerBox.addChild(authorLabel); pubdateLabel = new Label(); innerBox.addChild(pubdateLabel); spacer2 = new Spacer(); spacer2.percentHeight = 100; innerBox.addChild(spacer2); var buttonBox:HBox = new HBox(); buttonBox.percentWidth = 100; buttonBox.setStyle("horizontalAlign","right"); innerBox.addChild(buttonBox); buyButton = new Button(); buyButton.label = "Buy"; buyButton.setStyle("fillColors",[0x99ff99,0x99ff99]); buyButton.addEventListener(MouseEvent.CLICK, handleBuyClick); buttonBox.addChild(buyButton); }
I've indented the code to show the parent-child relationships. Also, make sure you include an event listener on the Buy button. - Override the commitProperties() function and set the user interface controls from the data.
override protected function commitProperties():void { super.commitProperties(); coverImage.source = data.image; titleText.text = data.title; authorLabel.text = data.author; pubdateLabel.text = data.date; }
- Add the click event handler for the Buy button.
private function handleBuyClick( event:MouseEvent ) : void { var e:BuyBookEvent = new BuyBookEvent(); e.bookData = data; dispatchEvent(e); }
- Modify the TileList in the main application to use the itemRenderer ActionScript class. Simply remove the inlineItemRenderer and replace it with an itemRenderer property right in the tag.
<mx:TileList id="mylist" x="29" y="542" width="694" itemRenderer="BookTileRenderer" dataProvider="{testData.book}" height="232" columnWidth="275" rowHeight="135" >
If you are going to use an existing container class, such as HBox, I wouldn't bother doing this in ActionScript. You can see it is more complex than using an MXML file and, quite frankly, there is little performance benefit to it.
Reusable itemRenderers
Here's an example of an itemRenderer that displays a numeric value using the CurrencyFormatter. I call it PriceFormatter:
<?xml version="1.0" encoding="utf-8"?> <mx:Text xmlns:mx="http://www.adobe.com/2006/mxml"> <mx:Script> <![CDATA[ import mx.controls.dataGridClasses.DataGridListData; [Bindable] private var formattedValue:String; override public function set data(value:Object):void { super.data = value; formattedValue = cfmt.format( Number(data[(listData as DataGridListData).dataField]) ); } ]]> </mx:Script> <mx:CurrencyFormatter precision="2" id="cfmt" /> <mx:text>{formattedValue}</mx:text> </mx:Text>
The key to this itemRenderer is shown in red, setting the bindable variable, formattedValue. First, you'll see that <mx:CurrentFormatter> was defined as an MXML tag (you can do this in ActionScript, too, if you prefer) with an id of cfmt. In the example above, the formattedValue is set to the result of calling the CurrentFormatter's format() function.
The function takes a Number as its parameter type, so the value is cast to Number - that's because the dataProvider for the list is XML and everything in XML is text; if you use a Object for your data and you have real numeric values, doing the Number cast will be harmless.
As you know, data is the property which holds the item being displayed by the itemRenderer. Using [ ] notation is another way of accessing the fields of the data item. For example, data['price'] would be the price column. But to make this itemRenderer resuable we cannot code for a specific field, so a more generic way is needed.
That's where listData comes in. All Flex components which implement the IDropInListItemRenderer interface have a listData property.
Most controls such as Text, Label, Button, CheckBox, and so forth, implement IDropInListItemRenderer. Most containers, such as HBox, Canvas, etc. do not implement that interface. If you want to use listData in an itemRenderer that extends a Container you will have to implement IDropInListItemRenderer yourself - I'll cover that in the next article.
The listData given to an itemRenderer contains, among other things, the rowIndex and the control which owns the itemRenderer - the DataGrid, List, or TileList. When you have an itemRenderer being used for the DataGrid, the listData is actually a DataGridListData object - which includes the columnIndex and the dataField associated with the DataGridColumn. Here's the breakdown of the statement above, starting from the inside:
- listData as DataGridListData - This casts the listData to a DataGridListData object so you have access to its dataField
- .dataField - the field for the column being rendered. This is what makes this itemRenderer generic. You can use this itemRenderer for multiple columns. In this example the dataField is 'price'.
- data[ ... ] - This accesses the data for the specific field in the item. In this example it will be the price column.
- Number( ... ) - This casts the value to a Number because the format() function requires a Number parameter.
- cfmt.format( ... ) - This formats the value as a currency.
Summary
Use whatever makes you comfortable when implementing itemRenderers. Some people only work in ActionScript which is great when you've got experience with Flex and ActionScript. MXML makes quick work of simple itemRenderers, too.
In a future article we'll look at making more efficient itemRenderers, which are ActionScript classes, but they extend UIComponent. In the next article I'll discuss more communication between itemRenderers and the rest of the application.
Posted by pent at 01:41 PM | Comments (9)
March 03, 2008
itemRenderers: Part 1: inline renderers
I'm starting a new series of articles on itemRenderers. Our documentation team has great examples so please check that information out first. I'm giving you my distillation of it.
Recycling Renderers
One thing many people try to do is access an itemRenderer from outside of the list. For example, you might want to make the cell in the 4th column of the 5th row in a DataGrid turn green because you've just received new data from the server. Getting that itemRenderer instance and modifying it externally would be a huge breech of the Flex framework and component model.
To understand itemRenderers you have to understand why they are what they are and what our intentions were when we designed them. BTW - when I say 'we' I really mean the Adobe Flex engineering team - I had nothing to do with it. Anyway, suppose you have 1000 records you want to show. If you think the list control creates 1000 itemRenderers you are incorrect. If the list is showing only 10 rows, the list creates about 12 itemRenderers - enough to show every visible row plus a couple for buffering and performance reasons. The list initially shows rows 1 through 10. When the user scrolls the list it may now be showing rows 3 - 12. But those same 12 itemRenderers are still there - no new itemRenderers were created, even after the list scrolled.
Here's what we do. When the list is scrolled, those itemRenderers which will still be showing the same data (rows 3 - 10) are moved upward. Aside from being in a new location, they haven't changed. The itemRenderers that were showing the data for rows 1 and 2 are now moved below the itemRenderer for row 10. Then those itemRenderers are given the data for rows 11 and 12. In other words, unless you resize the list, those same itemRenderers are reused - recycled - to a new location and are now showing new data.
If you want to change the background color of the cell in the 4th column of the 5th row, be aware that the itemRenderer for that cell may now be showing the contents of the 21st row if the user has scrolled the list.
So how do you make changes like this?
The itemRenderers must change themselves based on the data they are given to show. If the itemRenderer for the list is supposed to change its color based on a value of the data, then it must look at the data it is given and change itself.
inline itemRenderers
In this article we'll look at the answer to this problem using inline itemRenderers. An inline itemRenderer is one which is written directly in the MXML file where the list control occurs. In the next article we'll look at writing external itemRenderers. The inline itemRenderers are the least complex and are generally used for very simple renderers or for prototyping a larger application. There's nothing wrong with inline itemRenderers, but when the code becomes complex it is better to extract it into its own class.
In all of the examples we'll use the same data: a collection of information about books: author, title, publication date, thumbnail image, and so forth. Each record is an XML node which looks like this:
<book> <author>Peter F. Hamilton</author> <title>Pandora's Star</title> <image>assets/pandoras_star_.jpg</image> <date>Dec 3, 2004</date> </book>
Let's start with a simple itemRenderer using a <mx:List> control. Here, the author is listed followed by the title of the book.
<mx:List x="29" y="67" dataProvider="{testData.book}" width="286" height="190"> <mx:itemRenderer> <mx:Component> <mx:Label text="{data.author}: {data.title}" /> </mx:Component> </mx:itemRenderer> </mx:List>
This itemRenderer is so simple that a labelFunction would probably have been better, but it at least lets you focus on the important parts. First, an inline itemRenderer uses the <mx:itemRenderer> tag to define it. Within this tag is the <mx:Component> tag. This tag must be here as it tells the Flex complier you are defining a component inline. We'll discuss what this really means in a bit.
Within the <mx:Component> tag you define your itemRenderer. For this example it is a single <mx:Label> with its text field set to a data-binding expression: {data.author}: {data.title}. This is very important. The list control gives each itemRenderer instance the record of the dataProvider by setting the itemRenderer's data property. Looking at the code above, it means that for any given row of the list, the itemRenderer instance of its inline itemRenderer will have its data property set to a <book> XML node (such as the one above). As you scroll through the list, the data property is being changed as the itemRenderers are recycled for new rows.
In other words, the itemRenderer instance for row 1 might have its data.author set to "Peter F. Hamilton" now, but when it scrolls out of view, the itemRenderer will be recycled and the data property - for that same itemRenderer - may now have its data.author set to "J.K. Rowling". All of this happens automatically as the list scrolls - you don't worry about it.
Here's a more complex inline itemRenderer using the <mx:List> control again:
<mx:List x="372" y="67" width="351" height="190" variableRowHeight="true" dataProvider="{testData.book}"> <mx:itemRenderer> <mx:Component> <mx:HBox > <mx:Image source="{data.image}" width="50" height="50" scaleContent="true" /> <mx:Label text="{data.author}" width="125" /> <mx:Text text="{data.title}" width="100%" /> </mx:HBox> </mx:Component> </mx:itemRenderer> </mx:List>
This really isn't much different. Instead of a <mx:Label> the itemRenderer is an <mx:HBox> with an <mx:Image>, <mx:Label>, and a <mx:Text> control. Data-binding still relates the visual with the record.
DataGrid
You can use inline itemRenderers on a DataGrid, too. Here's one applied to a column:
<mx:DataGrid x="29" y="303" width="694" height="190" dataProvider="{testData.book}" variableRowHeight="true"> <mx:columns> <mx:DataGridColumn headerText="Pub Date" dataField="date" width="85" /> <mx:DataGridColumn headerText="Author" dataField="author" width="125"/> <mx:DataGridColumn headerText="Title" dataField="title"> <mx:itemRenderer> <mx:Component> <mx:HBox paddingLeft="2"> <mx:Script> <![CDATA[ override public function set data( value:Object ) : void { super.data = value; var today:Number = (new Date()).time; var pubDate:Number = Date.parse(data.date); if( pubDate > today ) setStyle("backgroundColor",0xff99ff); else setStyle("backgroundColor",0xffffff); } ]]> </mx:Script> <mx:Image source="{data.image}" width="50" height="50" scaleContent="true" /> <mx:Text width="100%" text="{data.title}" /> </mx:HBox> </mx:Component> </mx:itemRenderer> </mx:DataGridColumn> </mx:columns> </mx:DataGrid>
As you can see, this is much more complex than the last two, but it has the same structure: <mx:itemRenderer> with <mx:Component> definition inside of it.
The purpose of <mx:Component> is to provide an MXML syntax for creating an ActionScript class right in the code. Picture the code that appears in the <mx:Component> block being cut out and put into a separate file and given a class name. When you look at the inline itemRenderer it does look like a complete MXML file, doesn't it? There's the root tag (<mx:HBox> in this case) and even a <mx:Script> block.
The purpose of the <mx:Script> block in this example is to override the set data function so the background color of the itemRenderer can be changed. In this case, the background is changed from white whenever the publication data for a book is in the future. Remember that itemRenderers are recycled, so the color must also be set back to white if the test fails. Otherwise all of the itemRenderers will eventually turn purple as the user scrolls through the list.
outerDocument
The scope has also changed. What I mean is, variables that you define from within a <mx:Component> are only scoped to that component/inline itemRenderer. Likewise, the content outside of the <mx:Component> is in a different scope, just as if this component were defined in a separate file. For instance, suppose you add a Button to this itemRenderer that allows the user to by the book from an online retailer. Buttons call functions on their click event, so you might define the button like this:
<mx:Button label="Buy" click="buyBook(data)" />
If the buyBook() function were defined in the <mx:Script> block of the file you would get an error saying that buyBook() is an undefined method. That's because buyBook() is defined in the scope of the file, not in the scope of the <mx:Component>. Since this is a typical use case there is a way around that using the outerDocument identifier:
<mx:Button label="Buy" click="outerDocument.buyBook(data)" />
The outerDocument identifier changes the scope to look into the file, or outer document, with reference to the <mx:Component>. Now beware: the function has to be a public function, not a protected or private one. Remember that <mx:Component> is treated as an externally defined class.
Bubbling Events
Let's look at another, even more complex example. This is a TileList using the same data.
<mx:TileList x="29" y="542" width="694" dataProvider="{testData.book}" height="232" columnWidth="275" rowHeight="135" > <mx:itemRenderer> <mx:Component> <mx:HBox verticalAlign="top"> <mx:Image source="{data.image}" /> <mx:VBox height="115" verticalAlign="top" verticalGap="0"> <mx:Text text="{data.title}" fontWeight="bold" width="100%"/> <mx:Spacer height="20" /> <mx:Label text="{data.author}" /> <mx:Label text="Available {data.date}" /> <mx:Spacer height="100%" /> <mx:HBox width="100%" horizontalAlign="right"> <mx:Button label="Buy" fillColors="[0x99ff99,0x99ff99]"> <mx:click> <![CDATA[ var e:BuyBookEvent = new BuyBookEvent(); e.bookData = data; dispatchEvent(e); ]]> </mx:click> </mx:Button> </mx:HBox> </mx:VBox> </mx:HBox> </mx:Component> </mx:itemRenderer> </mx:TileList>
The itemRenderer looks like this when the application is run:
This itemRenderer is pretty close to the one used in the DataGrid, but the Buy button's click event doesn't use outerDocument to call a function. In this case the click event creates a custom event which bubbles up out of the itemRenderer, through the TileList, and is received by some higher component in the visual chain.
This is a very common problem: you have an itemRenderer which has some interactive control in it, usually a Button, LinkButton, etc. that is supposed to cause some action to take place when clicked. Perhaps it is to delete the row or in this case, buy the book.
It is unreasonable to expect the itemRenderer to do the work. Afterall, the itemRenderer's job is to make the list look good - period. Event bubbling allows the itemRenderer to pass off the work to something else. A custom event is useful here because the event is related to the data on the row - so why not include that data in the event; the receiver of the event won't have to go hunt it down.
Summary
Using inline itemRenderers is a great and quick way to give your lists a custom look. Consider inline itemRenderers as separate ActionScript classes - afterall, they are scoped as if they were. If you must refer to functions or properties in the containing file, use the outerDocument identifier to change the scope. If you need to communicate information as the result of an interaction with the itemRenderer, use a custom, bubbling, event.
And remember: don't try to get hold of itemRenderers - they are recycled for a purpose. Make them responsible only to the data given to them.
In the next article I'll discuss external itemRenderers.
Posted by pent at 08:26 AM | Comments (19)
February 15, 2008
Revised Gauge Control
I've been looking over my past posts and thought it was time to update the old Gauge control I introduced way-back-when. In looking over that code I thought it was time for a re-write and a simplification.
This is the result:
This gauge control is simplified in a couple of ways:
- First, there are just two parts: the face and the needle. Both of these are skins which you can change.
- Second, the skins are directly 'attached' to the control. In the previous versions of this component I had created classes which then held the skins. I wanted to show you a more simplified approach and it works well here.
I also made adjustments for the placement of the needle and the label as well as the start angle of the needle and its sweep from minimum to maximum value. If you play with the example above you'll see that as you change the needle's position the start and sweep angles reset to favored positions. You can change them, of course. For example, if you set the needle position to "top" the start and sweep angles reset to 45 and -90 respectively. This puts the minimum value on the left side. If you want the minimum value to appear on the right side and have the maximum value on the left, then set the start angle to 135 and set the sweep angle to (positive) 90.
This set of properties along with custom skins can give you very flexible gauges.
I did not in this version of the gauge control include interaction. You can basically copy the code from the previous version and use it here. I recommend you extend this control and create an InteractiveGauge control or something like that.
I hope you find the control useful, but more important, I hope you find this example of writing a Flex control simple and easy to follow.
View and download the source code.
Posted by pent at 10:57 AM | Comments (8)
December 31, 2007
Scrolling Text Component
Here's another example of a Flex component. This one scrolls a message within a fixed area. The message can be scrolled vertically or horizontally. You can give it a try right here:
This component shows how to use custom properties, meta data to work with Flex Builder, and overriding functions.
Click here to download the source code. The download is a Flex Builder 3 Beta 3 project - if you do not have Flex Builder 3 Beta 3 from Adobe Labs, you can open the file as a regular archive and use the source code.
Posted by pent at 08:27 AM | Comments (19)
December 24, 2007
Component Pack from ILOG
I've been asked a number of times if there are more chart types, as well as other controls, available for Flex. Perhaps I'm late to the party, but I just came across this announcement (from October 2007) that Adobe and ILOG are teaming up to enhance Flex 3.
Here's a quick list of what's available in the ILOG ELIXR package. You can find out more on Adobe Labs:http://labs.adobe.com/wiki/index.php/Flex_3:ILOG
- Radar charts (also named spider charts)
- Full 3D charts including bar/column, area, line and pie
- Treemap component, for analyzing large data sets
- Scheduling component, allowing users to view and manipulate time series data
- Organizational charts
- Country maps for creating interactive reports or dashboards
Posted by pent at 11:07 AM | Comments (1)
December 20, 2007
Component Class - Part Five
The last article in this series showed how to write the CycleSelectButton from scratch. In this article I'll look at styling and skinning the component.
Skins versus Styles
One frequently asked question is "what's the difference between styles and skins?" This is a good question and it is confusing a bit because you specify a component's skins using specific styles on the component. For example, the upSkin style on the Button component.
Styles control the appearance of a component while skins are the appearence. Or put another way, skins use styles to present the component. Take the borderColor style. It's purpose is to specify the color of a component's edge. The component's skin can use that style to draw its border - which may be round or square, thick or thin (using the borderThickness style). The skin contains the look of the component.
The reason skins are specified as style is so you can build an entire look or theme using just style sheets (.CSS files). If skins were specified in ActionScript you would have to deliver a new SWF for each theme. Having the skins and other styles in CSS means you can change the theme of an application with a new style sheet.
There are two types of skins: graphical and programmatic. Graphical skins are bitmaps: GIFs, JPGs, PNGs, etc. In Flex 3 you can import graphical skins from Adobe Illustrator and Adobe Photoshop. You can use Adobe Flash CS3 to create animated skins (picture a button that pulses with color).
Programmatic skins are written in ActionScript. They are class files that usually extend mx.skins.ProgrammaticSkin which is a very lightweight class. The class uses its override of updateDisplayList() to render the skin using the drawing API (see flash.display.Graphics).
Each component has its own set of skins. For a Button there are 8 possible skins: upSkin (its normal state), overSkin (when the mouse hovers over it), downSkin (when the mouse is pressed over it), disabledSkin (when the Button's enabled property is false), selectedUpSkin (when the Button's toggle property is true), selectedOverSkin (toggle is true and mouse is hovering over the Button), selectedDownSkin (toggle is true and mouse is pressed over it), and selectedDisabledSkin (toggle is true and enabled is false). If you want to use graphical skins for a Button, you should supply 8 different image files. If your Button will never be a toggle, then you can supply just 4 skins.
Download Example
This is a zip file and contains a full Flex Builder 3 project. You will either need Flex Builder 3 from Adobe Labs or you can use Flex Builder 2 and import the sources into a project. This project contains the source from the previous articles as well.
If you decide to use a programmatic skin you can either make separate skin classes, or use a single class, or a combination. A programmatic skin can detect which skin style it is being used for and code within the programmatic skin class can adjust for it. If, for example, the skin class is being used for a Button's upSkin, overSkin, downSkin, and disabledSkin, the class can decide to draw a green-filled circle for the upSkin, a blue-filled circle for the overSkin, a blue-filled circle for the downSkin, and a gray-filled circle for the disabledSkin.
You decide what works best for the look you want. You can wind up with a collection of skins - both graphical and programmatic - that make your application look unique (or follow your company's user interface guidelines).
Applying Skins
Applying the skins is simple. I prefer to do it in a style-sheet to make them easier to change:
Button { upSkin: Embed('assets/BlueButtonUp.gif'); overSkin: Embed(source='assets/CompanyIcons.swf',symbol='GreenButton'); downSkin: ClassReference('com.mycompany.skins.StandardButtonSkin'); disabledSkin: ClassReference('com.mycompany.skins.StandardButtonSkin'); }
This pretty wild Button has a mixture of skins: one is a GIF, another is a symbol out of a SWF, and two come from the same ActionScript class.
Specifying a graphical skin uses the Embed directive. For a simple image file the Embed names the file using a path that is relative to the application's main file, or an absolute path within the project. The example above uses a relative path. When a Flash SWF is used, the skin can be the entire SWF file or a specific symbol within the SWF. If you chose to use a specific symbol, the Embed directive names the file and the symbol within it.
Specifying a programmatic skin uses the ClassReference directive. The full class name, including its package, is given for the reference. The compiler will find that class and pull it into the SWF.
The main advantage of programmatic skins over graphical skins is scaling. Because programmatic skins use the Flash Drawing API, the skins scale and rotate very well. Graphical skins can easily become distorted unless you scale9grid specifications in the Embed directive. The scale9grid specifications let you specify a grid overlay on the graphic that tells the Flash Player which parts of the graphic to scale. Think of a rectangle where you want the 4 corners to never scale, the top and bottom to scale only when the graphic is stretched horizontally, the left and right edges to scale when the graphic is stretched vertically, and the center to always scale.
Going back to the CycleSelectButton component, here is how the createChildren() function looks currently:
override protected function createChildren() : void { arrows = new Arrows(); arrows.width = 20; arrows.height= 20; addChild(arrows); linkButton = new LinkButton(); addChild(linkButton); // add a listener for the click on the LinkButton. linkButton.addEventListener(MouseEvent.CLICK, handleClick); super.createChildren(); }
To redo this component using skins, you have to think about which parts of the component should be skinable. It seems like a good idea for the circle of arrows to be a skin. Maybe you want to make your own arrows using Photoshop, for instance.
Here is the modified createChildren() function that introduces skins:
var skin:Class; skin = getStyle("arrowSkin"); if( skin == null ) skin = CycleSelectArrowSkin; _arrowSkin = new skin(); _arrowSkin.name = "arrowSkin"; if( _arrowSkin is ProgrammaticSkin ) (_arrowSkin as ProgrammaticSkin).styleName = this; _arrowSkin.width = 20; _arrowSkin.height= 20; addChild(_arrowSkin as DisplayObject);
This is a bit different. First, the value for the arrowSkin style is retrieved. The arrowSkin style is specified by metadata above the class declaration:
[Style(name="arrowSkin",type="Class",inherit="yes")]
Notice that the type of the style data is "Class" - you want to load the class definition for the skin, not just the name of the class. This works for Embed as well since a class is created from the embedded image data.
If no arrowSkin style has been specified, then the default class, CycleSelectArrowSkin, is given. Then the arrowSkin member variable is set with a new instance of whatever skin class was selected. This is the standard way to specify skins using styles.
Once the class instance has been created and arrowSkin is now set, you'll see it is given a name ("arrowSkin") and its style is set to this. What it means is that the skin will get all of the styles set on the component. For example, the CycleSelectArrowSkin uses a style called "arrowColor" to draw the arrow graphic. There isn't any way from outside of the CycleSelectButton code to associate this style with the arrow skin; the style is set on the component, along with the arrowSkin style shown above:
[Style(name="arrowColor",type="Number",format="Color",inherit="yes")] [Style(name="arrowSkin",type="Class",inherit="yes")]
With the skin inheriting the component's style, arrowColor among them, the skin code can draw the arrows.
Details
In this section I go through the steps in more detail . I'll use the sample CycleSelectButton available from the download with this article, but I will only show the skin for the arrows; the skins for the rest of the component work the same way and it will be less confusing to focus on one skin.
Step 1: Figure out what you want the skin to be used for. In this case, it is for the cycle of arrows and by making it a skin, gives a developer the chance to change the look of the component without re-writing the component.
Step 2: In the component class file (CycleSelectButton.as), define the style for the skin above the class definition:
[Style(name="arrowSkin",type="Class",inherit="yes")] public class CycleSelectButton extends UIComponent {
Make sure the type of the style is "Class". The name will be used in the style sheet or on the MXML tag for the component:
StyleSheet.css: CycleSelectButton { arrowSkin: ClassReference('com.adobe.examples.skins.CycleSelectArrowSkin'); or arrowSkin: Embed('assets/ArrowSkin.png'); } MXML: <buttons:CycleSelectButton arrowSkin="com.adobe.examples.skins.CycleSelectArrowSkin"... /> or <buttons:CycleSelectButton arrowSkin="@Embed('assets/ArrowSkin.png')" ... />
Step 3: Declare a member variable to hold the skin instance:
private var _arrowSkin:IFlexDisplayObject;
Notice that the type of the variable is IFlexDisplayObject - not CycleSelectArrow skin, not UIComponent, and not even ProgrammaticSkin. If you want your skin to be either a programmatic skin or a graphic skin, you need to use a data type that is common to both. IFlexDisplayObject fills that need. It is generic enough, but also allows you to position and size the skin.
Step 4: Create the skin. You can do this either in createChildren() or in commitProperties().
var skin:Class; skin = getStyle("arrowSkin"); if( skin == null ) skin = CycleSelectArrowSkin; _arrowSkin = new skin(); _arrowSkin.name = "arrowSkin"; if( _arrowSkin is ProgrammaticSkin ) (_arrowSkin as ProgrammaticSkin).styleName = this; _arrowSkin.width = 20; _arrowSkin.height= 20; addChild(_arrowSkin as DisplayObject);
The getStyle() function is used to get an alternative skin class (ProgrammaticSkin or graphic) from the styles for the component. This is how a custom skin can be used from a style sheet or MXML tag (from Step 2 above). If no skin was specified getStyle() returns null. In this case a default skin is used. It is important that when using skins you are consistent and create a default skin; creating a skin as a default is always a good idea and perhaps it too can be extended and customized.
Once the skin class is chosen, the arrowSkin member (from Step 3) is set with an instance of this class. Now it is either a graphic skin or a ProgrammaticSkin. If the latter you must set the styleName of the skin to be this (or some other object instance which holds the styles). If you don't do this, the ProgrammaticSkin will fail when it uses getStyle().
You can size the skin at this step IF you know the size. If your skin is going to occupy the entire component's space, you can set it within updateDisplayList() (see Step 5).
Finally you add the skin as a child of the component. Note that you have to cast the skin as a DisplayObject since addChild does not accept IFlexDisplayObject parameters.
Step 5: Position (and optionally, size) the skin in updateDisplayList():
override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void { super.updateDisplayList( unscaledWidth, unscaledHeight ); // position the arrowSkin _arrowSkin.move( 10,10 );
Here the arrowSkin is moved into position. If you were to have a skin that required it to be sized, then you can do that too using skin.setActualSize( width, height ) where the width and height might be unscaledWidth, unscaledHeight or some derivative of those values.
That's all you need to do to use a skin in your component. Notice that none of the component's look has been done by the actual component code - it is all done by the skin. This gives your component a tremendous amount of flexability in how it is presented, not in how it behaves.
The Skin Itself
The CycleSelectArrowSkin is one of the files available in the download with this article. Here are some of the highlights:
public class CycleSelectArrowSkin extends ProgrammaticSkin
The class extends mx.skins.ProgrammaticSkin which extends flash.display.Shape. That's because skins should be very light-weight and be limited to just presenting graphics. However, one of the most powerful properties of Flex and the Flash Player is its flexability. You do not have to make your skins extend ProgrammaticSkin. You can use any class which implements the IFlexDisplayObject interface.
Since skins are so lightweight there isn't much else they do except override updateDisplayList():
override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void { super.updateDisplayList( unscaledWidth, unscaledHeight ); var color:Number; switch( name ) { case "arrowSkin": color = getStyle("arrowColor"); if( isNaN(color) ) color = getStyle("themeColor"); break; case "arrowDisabledSkin": color = 0xAAAAAA; break; } drawArrows( graphics, color ); }
In this function, the skin's name is used to determine its color. If the skin is the "arrowSkin" then the color is extracted from the "arrowColor" style. If that was not defined, then the skin's color defaults to the "themeColor".
Using the themeColor as a default - whether it is the actual color or a darker or lighter version (see mx.utils.ColorUtil class) - is a good idea since that color flows nicely with the style sheet and theme idea.
Once the color is chosen the skin is drawn. The drawArrows function is the same as it was before:
private function drawArrows( g:Graphics, color:uint ) : void { g.clear(); g.lineStyle(0, color, 1); g.moveTo(-10,0); g.curveTo(-10,-10,0,-10); g.curveTo(2.5,-11,8,-8); g.moveTo(8,-8); g.lineTo(6.5,-13); g.moveTo(8,-8); g.lineTo(2.5,-6); g.moveTo(10,0); g.curveTo(10,10,0,10); g.curveTo(-2.5,11,-8,8); g.moveTo(-8,8); g.lineTo(-6.5,13); g.moveTo(-8,8); g.lineTo(-2.5,6); }
Summary
That's all there is to skinning components:
- Figure out what you want to skin,
- extract the drawing into a class based on ProgrammaticSkin,
- add the skin as a style to your component,
- create an instance of the skin, and position it.
You should now be able to make reusable components for all of your Flex projects.
Posted by pent at 02:35 AM | Comments (5)
November 01, 2007
Component Sources Modified
A couple of typos and a mistake have been brought to my attention regarding the ZIP files for these Component examples. I have made the fixes and replaced the files. If you have downloaded the code, please go back to the articles and download the files again.
.
Posted by pent at 10:38 AM
October 31, 2007
Component Class - Part Four
In the previous article in this series you saw how the Arrow part of the CycleSelectButton was created. In this article we'll write the CycleSelectButton from scratch by extending UIComponent. Watch how similar this component's construction is to the V2 and Arrows components.
Start by creating a new ActionScript class and call it CycleSelectButtonV3
. Have it extend UIComponent
:
public class CycleSelectButtonV3 extends UIComponent { /** * constructor function * * This is a good place to set inital styles */ public function CycleSelectButtonV3() { super(); }
To make this simple for you - and to drive home the point of how similar things are - copy the following items from the V2 component into this V3 component:
- The
[Event]
metadata; - the linkButton and arrows variable definitions (be sure to copy the import statements, too);
- the
createChildren()
function; - The
dataProvider
property set and get functions; - The
selectedIndex
property set and get functions; - The
commitProperties()
function; - The
handleClick()
event handler function.
So what's left? I'm not sure all of that will compile, but give it a try and if it does, put it into a Flex application and test it out. Not quite right, huh?
This zip file contains the source for this component and a sample application.
The V2 version of this component extends HBox which does a couple of things for you: it handles the placement or layout of the component. By using HBox you don't have to worry about how big things are and where they go. HBox always measures each child and sticks one after the other.
Since this V3 component extends UIComponent you don't have any of that help. You have to implement a couple of the Flex framework functions to make the component behave correctly.
measure()
Look back at the Arrows component and you'll see two things that are missing from this V3 component: the measure()
and updateDisplayList()
functions. Measure() is important because the Flex framework needs to know how big the component is in order to position it within a container. The updateDisplayList() function is important to position the arrows and linkButton - something HBox did for you.
override protected function measure() : void { super.measure(); measuredWidth = arrows.getExplicitOrMeasuredWidth() + linkButton.getExplicitOrMeasuredWidth(); measuredHeight= Math.max( arrows.getExplicitOrMeasuredHeight(), linkButton.getExplicitOrMeasuredHeight() ); }
The measure() function must set the measuredWidth
and measuredHeight
properties. Since the component's design is to be horizontal with the arrows followed by the linkButton, the width is then the sum of each child's width. The height is the largest of the two.
If your component is also given an explicit width and height, then this measure() method will not be called.
Noticed the call to getExplicitOrMeasuredWidth
(and getExplicitOrMeasuredHeight
). Since the arrows child has been given a size of 20x20, these functions return the explicit size of 20. The linkButton however, was not given a size, so it has to be measured.
updateDisplayList()
Once the child components have been measured and an overall size for the component has been determined, the Flex framework calls the updateDisplayList() function.
Just as with the Arrows component, updateDisplayList's purpose is to position and size the child components to make this component look the way it is supposed to look.
override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void { super.updateDisplayList( unscaledWidth, unscaledHeight ); arrows.move(0,0); linkButton.move(arrows.width,0); linkButton.setActualSize(unscaledWidth-arrows.width,unscaledHeight); }
The arrows are moved to the (0,0) position. Then the linkButton is positioned immediately after it. The linkButton is also given a size. Here, unscaledWidth and unscaledHeight will be whatever measure() determined or they will be the explicit sizes given to your component.
And that's all there is. The download source file has it all put together for you along with a sample test program. But take a look at the V2 and V3 components; compare them with the Arrows component. In all cases the Flex framework operates consistently. Because V2 was based on HBox, the measure and updateDisplayList functions were not necessary. When writing a component from scratch you have to do these things yourself.
Now you can write components from scratch - either based on existing components or from scratch. The next article in this series looks at skinning and styling components.
Posted by pent at 11:36 AM | Comments (6)
October 24, 2007
Component Class - Part Three
In the previous article you saw how to create a component in ActionScript and how that mimics a component written in MXML:
MXML | ActionScript |
---|---|
Root tag | class extends |
Metadata tag | [Metadata above class definition] |
Child component tags | override createChildren(), using new operator and addChild() function. |
Properties | Set and Get functions; override commitProperties() |
Events | Use addEventHandler and specify an event argument to the handler function. |
In this article we'll look at how to make that circle of arrows rotate.
This is a link to the same file download in the previous article; nothing has changed.
Arrow Class
In the first article the cycle arrows is a GIF. Easy to place using the Image component (either in MXML or in ActionScript). That would be enough, except we want to rotate the arrows as the user clicks on the link in the component.
So what's wrong with that? Rotation in Flash is pretty easy, you just set the rotation property to an angle and the object rotates - about its (0,0) point. That's the catch - in Flex a component has (0,0) as its upper-left corner. When you rotate a Flex component by changing its rotation property, it pivots on this corner - it does not rotate about its center.
There are ways around this using a translation matrix, but I think this alternative will prove educational and help you out when you have some awkwardthings to do in Flex.
Principle
Keep in mind that (0,0) is the upper-left corner of a Flex component and to make life very easy and simple in the Flex framework, the Arrow component is going to keep it this way. The difference is that inside of the Arrow component, the circle of arrows will appear and it will rotate and not the Arrow component itself.
Here's the Arrow component in its entirety, but broken into sections. I think it will be easier to explain this way.
package com.adobe.examples.skins { import mx.core.UIComponent; import flash.display.Graphics; import flash.display.Shape;
public class Arrows extends UIComponent {
There really isn't any existing Flex component to extend so the Arrow class extends UIComponent - the base class for every Flex component. UIComponent is what every Flex component inherits from.
createChildren
private var canvas:Shape; /** * createChildren (override) * * Creates the shape in which the arrows appear. This shape can then * be rotated. */ override protected function createChildren():void { canvas = new Shape(); // after drawing the arrows below, I realized they were too big, but my // calculations for the lines and curves were already figured out. So I // just scaled the graphic a bit to make it look better. canvas.scaleX = 0.6; canvas.scaleY = 0.6; addChild(canvas); }
This component has a single child, a flash.display.Shape
, where the arrows will appear. This is the part that actually rotates. The Shape class is a very basic, lightweight Flash class for drawing. It has very little overhead and is ideal for this purpose.
measure
/** * measure (override) * * Return the default width and height */ override protected function measure():void { measuredWidth = measuredMinWidth = 20; measuredHeight = measuredMinHeight = 20; }
So far you've seen createChildren() and commitProperties() - functions which you override to make your component. Here is another - measure()
. This function is critical to making components behave properly with the Flex framework's layout manager. You don't need to have measure() in the CycleSelectButton components because the HBox does this for you - a benefit of using a Container as a basis for your own components.
The measure function is called only when the Flex framework does not know how large the component should be. If you supply an explicit width and height to a component, measure() is never called because the layout manager knows how big it is. Keep this in mind and do not write anything in this function that is critical since it is not always called.
The measure() function's job is to set the measuredWidth
and measuredHeight
properties (and optionally, like here, the measuredMinWidth
andmeasuredMinHeight
properties). Sometimes measure() can be complex if you have lots of children to the component - you have to measure all of them and then figure out how large the overall component is.
In this case, I create the circle of arrows to occupy a 20x20 area. So that's what I set measuredWidth and measuredHeight to.
updateDisplayList
/** * updateDisplayList (override) * * Position the canvas containing the arrows in the middle of the component. Then * draw the arrows. */ override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void { super.updateDisplayList( unscaledWidth, unscaledHeight ); canvas.x = unscaledWidth/2; canvas.y = unscaledHeight/2; drawArrows( canvas.graphics ); }
The updateDisplayList()
is another function in the Flex framework called in the life cycle of a component. The updateDisplayList() function is perhaps the most fun - it is were you actually make things happens. In this case two things are done: the canvas Shape with the arrows is positioned and the arrows are drawn.
Remember that the arrows are being drawn within the canvas at (0,0) - so we need to place the canvas Shape where we want its (0,0) to be - and that's in the middle of the component which is (unscaledWidth/2, unscaledHeight/2). If this is confusing, change it to (0,0) and see what happens.
I moved the arrow-drawing into a separate function to make it clearer (this function appears at the end of the class):
/** * drawArrows * * Draws the circle of arrows in the given graphic. The circle is centered * at (0,0) to make it easy to rotate */ private function drawArrows( g:Graphics ) : void { g.clear(); g.lineStyle(0, 0x0000CC, 1); g.moveTo(-10,0); g.curveTo(-10,-10,0,-10); g.curveTo(2.5,-11,8,-8); g.moveTo(8,-8); g.lineTo(6.5,-13); g.moveTo(8,-8); g.lineTo(2.5,-6); g.moveTo(10,0); g.curveTo(10,10,0,10); g.curveTo(-2.5,11,-8,8); g.moveTo(-8,8); g.lineTo(-6.5,13); g.moveTo(-8,8); g.lineTo(-2.5,6); }
The flash.display.Graphics
(g) is the Flash entity which places instructions into the Flash Player's display list. Ultimately every component does this. The first thing is almost always clear() so you don't add more graphics - you usually want to replace them. You can read more about the Drawing API in the Flex documentation. You can see that the arrows are a series of lineTo
and curveTo
functions about (0,0).
The rotation Property
/** * rotation - apply rotation to the canvas shape, not to this component */ override public function set rotation(value:Number):void { canvas.rotation = value; } override public function get rotation():Number { return canvas.rotation; }
Here's a trick you may not expect: overriding a very basic property like rotation. If you were to do something like arrows.rotation = 45
you would and should expect the component to rotate to 45 degrees. But if that happens, the component rotates about its upper-left corner. What we want to do is rotate the Shape instead. By overriding the component's rotate property, we re-direct the value down to the Shape and prevent the component itself from being rotated.
Another benefit of overriding the property is that it will make sense to the developer who uses the component. Telling them that they can rotate the arrows by changing the rotation property is what they expect. Trying to explain they should not use rotation but instead use another set of functions is awkward.
Use in CycleSelectButtonV2
Now open the CycleSelectButtonV2 code and replace the Image component with an instance of the new Arrows component. The code provided in the download already does this; you use the Arrows component like anything else:
- You have to create an instance of it using new Arrows().
- You can add it to the CycleSelectButtonV2 component with addChild().
override protected function createChildren() : void { arrows = new Arrows(); arrows.width = 20; arrows.height= 20; addChild(arrows); linkButton = new LinkButton(); addChild(linkButton); // add a listener for the click on the LinkButton. linkButton.addEventListener(MouseEvent.CLICK, handleClick); super.createChildren(); }
- You make it spin by changing its rotation property in the LinkButton click event handler:
arrows.rotation = arrows.rotation + 45;
What's Next
In the next article in this series we'll write this component one more time, but completely from scratch by extending UIComponent.
Posted by pent at 07:22 PM | Comments (4)
October 22, 2007
Component Class - Part Two
In the previous article I showed you how to make a component based on an existing MXML component, the HBox. In this article we'll look at writing that same component in ActionScript. I think this is a worthwhile exercise because you'll see how similar MXML and ActionScript components are.
One good thing to try is to add
-keep-generated-actionscript=true
to your Flex compiler options. MXML files are first translated into ActionScript classes, then compiled into the SWF file. The-keep-generated-actionscript
switch will leave those translations and you can compare what we're about to do with what the compiler has generated.
Take a look at the V1 MXML component. There are four parts to it: the root tag (the HBox), the Event metadata, the Script block, and the child components (LinkButton and Image). You'll see how these elements are reflected when you write the ActionScript version of the component.
This is the source code for this example in a package structure. The file for the icon is included.
Create a new ActionScript class called CycleSelectButtonV2 and have it extend HBox. Use the Flex Builder wizard if you can. Otherwise create the class using the following code:
package com.adobe.examples.buttons { import mx.containers.HBox; public class CycleSelectButtonV2 extends HBox { public function CycleSelectButtonV2() { super(); } } }
One of the four elements has already been taken care of: the class extends HBox.
The MXML component also includes event metadata. Add that to the ActionScript class just above the class definition:
package com.adobe.examples.buttons { import mx.containers.HBox; [Event(name="change",type="flash.events.Event")] public class CycleSelectButtonV2 extends HBox { ...
The child components, Image and LinkButton, require ActionScript code. In the first article of this series you read how the Flex framework operates with properties being set, with commitProperties() being called, and so forth. Creating child components is also part of that framework.
When a component is being created the Flex framework invokes its createChildren()
function. This is as true for Buttons as it is for Container classes like HBox. To create the LinkButton and Image components, you'll need to add your own override of createChildren().
Below the import statement for HBox in the V2 ActionScript file, add:
import mx.controls.LinkButton; import mx.controls.Image;
The MXML tags for Image and LinkButton created two members of the class. You'll do the same here, below the class constructor function:
private var linkButton:LinkButton; private var image:Image;
Component children in MXML files are public. I like to make them private, but you can use protected or public if you like.
You also need to bring in the cycle button graphic. In the MXML file it was embedded directly in the Image tag. There is no ActionScript equivalent for that. Instead, add this code below the variable declarations:
[Embed(source="../assets/cycle_component.gif")] private var cycleIcon:Class;
The embed metadata tells the compiler to grab the bytes from the file and generate a class to make an image out of them. The data type of the cycleIcon variable is Class
because you don't want to have duplicates of the image (takes too much memory) but rather have shared instances that refer to those bytes.
Now add the createChildren() function:
override protected function createChildren() : void { image = new Image(); image.width = 20; image.height = 20; image.source = cycleIcon; addChild(image);
linkButton = new LinkButton(); addChild(linkButton); super.createChildren(); }
An instance of the Image class is created, then its width and height are set as is its source property. The LinkButton is also created. Notice how both components are added as children to the component using addChild()
. This puts the children onto the component's display list. If you do not do this the children will not appear. Creating the component children with new
and using addChild()
is equivalent to the MXML tags.
The createChildren function is called before commitProperties() so that you can apply properties to the children. You should now copy the commitProperties() function, along with the setter and getter property functions from the V1 class.
With the LinkButton and Image child components created and set, you've knocked off another MXML component equivalent.
In the previous article, you read that part of the Flex framework includes the measurement of the components. One of the big things the MXML component does for you is figure out the size of everything and where to place it. You'll have to do that yourself with an ActionScript component.
Because we are using HBox as the base class for the component, the sizing and positioning of the children are taken care of. Later, when we write this component from scratch, you'll see how to do that for yourself.
At this point the only thing missing is the click event handler on the LinkButton. To add that, go back into the createChildren() method and add the following line after the linkButton has been created:
linkButton.addEventListener(MouseEvent.CLICK, handleClick);
You'll need to include an import statement for flash.events.MouseEvent
with the other import statements.
Using addEventListener() is the equivalent of putting the event handler in the MXML tag.
Now copy the handleClick() function from the V1 component and make the change shown:
private function handleClick( event:MouseEvent ) : void { selectedIndex = selectedIndex + 1; // wrap back to zero if more than the # of items in the dataProvider. if( selectedIndex >= dataProvider.length ) selectedIndex = 0; dispatchEvent( new Event(Event.CHANGE) ); }
When you are using MXML tags you have the luxury of deciding what, if any, arguments to pass to an event handler. When using addEventListener the event handler function must take a single argument of some event type. If you read the documentation on the LinkButton and look at the click event, you'll see it dispatches a MouseEvent, so that's the type of data that will be passed.
Open the main Flex application and change the tag from CycleSelectButtonV1 to CycleSelectButtonV2 and run it. The component should look and behave the same way.
Rotating the Arrows
I said in the previous article that, in addition to writing this component in ActionScript, we'd also rotate the arrow cycle each time the LinkButton was clicked. I've included the code to do that in the source download, replacing the Image. Take a look and it will be the topic of the next article in this series.
Posted by pent at 04:50 PM | Comments (6)
October 16, 2007
Component Class - Part One
This is a topic I visit from time to time - writing components. In this example I'll show you how to write a component from the ground-up. It will take several articles, but in the end you should be able to build your own components.
The component I've chosen I call a cycle-select button. This component shows a single entry from a dataProvider along with two arrows in a circle. When you select the button the arrow rotates a bit and the next value in the dataProvider is displayed. Think of it as a ComboBox with no drop-list.
Try clicking on the label and you'll see how it cycles through the values.
Flex Framework
In order to build a Flex component you must understand the Flex framework. Building a component is done in stages as the Flex framework makes multiple passes through the component hierarchy to determine layout.For example, consider an Application with two VBoxes. One VBox has a bunch of Buttons while the other has a bunch of labels. To create these components, the Flex framework has to first create the Application itself, then the VBoxes, and then the children of those VBoxes. That's one pass.
If neither VBox has been given a width or height you'd expect the VBoxes to be large enough to enclose all of its children, right? To figure out how big to make the VBoxes, the Flex framework has to determine how big each of its children are. Buttons and Labels do not typically get explicit sizes, so the Flex framework has to determine their sizes, too. That's another pass.
Once all of the measurements are done, the Flex framework has to get the components sized property and positioned. That's another pass.
As you can see, creating components isn't a simple task, but it also isn't that hard, once you get the hang of it.
To facilitate component creation, the Flex framework calls upon certain methods within each components. By implementing these methods, your component can fit nicely and easily into the framework.
There are two ways to begin a component: by extending a component that already exists which does most of what you want to do or by creating a component "from scratch" which is to say, from the base class all components extend.
Extending an existing component is the most common and it is what you do all the time when writing a Flex application. When you create your main application file with a root tag of <mx:Application> you've created a component (extending Application). When you make a new MXML file a root tag of any other component, that's doing the same thing; whether you extend a component through MXML or ActionScript.
First Draft
We'll start by modifying the HBox component but ultimately we'll want to extend UIComponent. Using HBox can provide a good proof of concept.Download File
This is the source code in a package structure. It includes the icon for the graphic as well.
Taking a look at the component's design, there are two obvious top-level pieces: the cycle button and the label. But since we want to select the label to cycle through the choices, using a Button seems more practical since a Button also provides some feedback as the mouse rolls over it and is pressed. But a Button doesn't look anything like the control above, so perhaps a LinkButton is closer in appearance.
Create a new MXML component using HBox as the root tag and call it CycleSelectButtonV1.mxml (for version 1). To that add two children: an Image and a LinkButton (set its id to "linkButton"). Set the Image to be 20x20 and set the HBox's verticalAlign property to "middle". If you used FlexBuilder to make the component, erase any pre-set width and height on the HBox.
Change the Image tag to this:
<mx:Image source="@Embed('../assets/cycle_component.gif')" width="20" height="20" />Picture how you'd use this new component. Perhaps something like this:
<CycleSelectButtonV1 dataProvider="{choices}" change="handleCycleChange(event)" />The HBox component doesn't have a dataProvider property nor does it have a change event. This is part of the customization you'll need to do.
Events and Properties
The component will dispatch achange
event, so you'll need to tell the Flex compiler that the component will be dispatching the event. Add this below the HBox root tag:<mx:Metadata> Event(name="change",type="flash.events.Event")] </mx:Metadata>The Event metadata tells the Flex compiler that including change="..." on the MXML tag is OK. The class given as the data type of the event is the default, but I like to be explicit so there's no question about what event class is going to be expected in the event handling function.
For the dataProvider, which is a property, you'll need to write some ActionScript. Add a Script block below the Metadata tag:
<mx:Script> <![CDATA[ ]]> </mx:Script>
The <![CDATA and ]]> syntax is a way of telling XML that everything within those brackets is to be taken as-is with no XML parsing. You don't *need* to have the CDATA block inside the Script tags, but if you use < for example, the XML parser will think you are trying to start a new tag!Add this code within the CDATA block:
import mx.collection.ArrayCollection; private var _dataProvider:ArrayCollection; public function set dataProvider( value:ArrayCollection ) : void { _dataProvider = value; } public function get dataProvider() : ArrayCollection { return _dataProvider; }This is a standard way of writing a property - using set and get functions with the actual value the same name as the set and get functions, but prefixed with an underscore. You'll sometimes see this referred to as a backing variable.
At this point you can test the component, even including dataProvider and change in the component tag; they don't do anything yet.
When I thought up this component I envisioned the data being provided the same way you'd do for ComboBox. Here's an example:
[ {label:"Apples", value:1}, {label:"Oranges", value:2}, etc. ]What you would like to see are the labels displayed. The trick is to make that happen.
commitProperties
Nothing in the component so far shows how to display the data in the LinkButton. For that we wil need some more ActionScript. Add this code within the Script block:override protected function commitProperties() : void { super.commitProperties(); // we'll fill this in below }Part of the Flex framework cycle includes a call to commitProperties(). This function is called after all of a component's properties have been set. This is important because within any given property set function you don't know if another set function has already been called. If setting a third property depends on the value of two others, for example, the only logical place to set that third property is in commitProperties().
We're going to use commitProperties() to set the label of the LinkButton to a value in the dataProvider. For this first example it will be the first item. Add this below super.commitProperties();
linkButton.label = dataProvider[0].label;There are a lot of things that can go wrong here: the dataProvider property might never have been set or it might not have a label property in the first item. For now, believe everything is set.
Running the Flex app again and you should see "Apples" as the LinkButton's label.
What do you think should happen if you wrote some ActionScript code in your main file that changed the CycleSelectButtonV1's dataProvider? What happens if you change the dataProvider of a ComboBox or DataGrid? They change to show those new values, right? You'll want the CycleSelectButton to do the same. Making this happen will further illustrate the Flex framework.
If you do: cycleButton.dataProvider = newValue
this calls the component's set function for the dataProvider property:
public function set dataProvider( value:ArrayCollection ) : void { _dataProvider = value; }Big deal: the _dataProvider internal member changes. But that won't change the label to the LinkButton. You could try adding
linkButton.label=value[0].label
right into this set function, but I hope you can see there are problems with that. For one, it just "feels" wrong. But really, the problem is that when the component is first being created and the properties are being set, the LinkButton's label property may not be able to accept the value. It will later, of course, but many components won't be able to be changed so easily during the property setting phase.Which of course, leads to the commitProperties() function. The next logical thing to do then is call commitProperties right from the set function. Again, doesn't "feel" right and besides, all that does is just do the same thing with the same problem.
What you want to do is notify the Flex framework that commitProperties() needs to be called. To that you call invalidateProperties() from the set function. This sets a flag in the Flex framework. What is good about this is that you can set a hundred properties and call invalidateProperties() a hundred times, but commitProperties will be called just once. Much more efficient. Change the set function to this:
public function set dataProvider( value:ArrayCollection ) : void { _dataProvider = value; invalidateProperties(); }
Cycling Through the Labels
That little exercise was the set up for the next thing you should do: change the label on the LinkButton to the next item in the dataProvider.What's the "next" item if zero is hard-coded in commitProperties()? Obviously we'll need a variable to hold this. Hmm, selectedIndex sounds like a good choice and is consistent with the ComboBox and many other Flex controls. Set up a set and get function for selectedIndex, too:
private var _selectedIndex:int = 0; public function set selectedIndex( value:int ) : void { _selectedIndex = value; invalidateProperties(); } public function get selectedIndex() : int { return _selectedIndex; }You'll also need to change a line in commitProperties to use selectedIndex:
linkButton.label = dataProvider[selectedIndex].label;And make the click event on the LinkButton call a function to bump up the index:
private function handleClick() : void { selectedIndex = selectedIndex + 1; if( selectedIndex >= dataProvider.length ) selectedIndex = 0; }Notice that the linkButton's label was not explicitly set in the handleClick() function. Because the selectedIndex property is set, and because that set function calls invalidateProperties(), the linkButton's label is changed in commitProperties(). As a rule of thumb, try to modify components in only one place.
Don't forget to hook up the handleClick() function to click event on the linkButton:
<mx:LinkButton label="linkButton" click=handleClick()" />
Now run the application and click the LinkButton in the component. It should cycle through all of the labels in the dataProvider. When the LinkButton is clicked, the handleClick() function is called. That increments the selectedIndex which sets a flag for commitProperties to be called. When commitProperties is called, it displays the label assoicated with item in the dataProvider at the selectedIndex index. Pretty cool, huh?
Dispatching Events
The last thing to do is dispatch a change event when the selectedIndex changes. You could put this into the selectedIndex set function. But that means any change via ActionScript will dispatch the event. That's not a normal Flex workflow. Instead, dispatch the event right from the LinkButton's click handler:private function handleClick() : void { selectedIndex = selectedIndex + 1; if( selectedIndex >= dataProvider.length ) selectedIndex = 0; dispatchEvent( new Event(Event.CHANGE) ); }In the main application you can handle the change event and use the component's selectedIndex property to find out what was selected.
Wrap Up
That's it for this article which is already long for a blog entry. You should be able to write components using any Flex container (eg, HBox, VBox, Canvas) as a base. Just toss in the child components as MXML tags into the file, create a Script block to add any properties (with set and get functions), and override commitProperties() to apply those properties.In the next episode of this series we'll look at writing this same component in ActionScript and rotate that circle image with each click.