12 November 2007
This article is geared toward intermediate to advanced developers creating applications using Flash CS3 Professional components—specifically, the DataGrid component. Some experience working with components in Flash is necessary, as well as experience with ActionScript 3.0 event handling. At its most advanced, this article briefly discusses applying these techniques when using custom cell renderers, so knowledge of creating a custom cell renderer will be necessary to apply that technique.
Intermediate
Suppose you are creating a user interface with Flash CS3 Professional using the new lightweight ActionScript 3.0 components. You're using the DataGrid component to display—what else?—a table of data. You want users to be able to edit the data, and you also want to know when the user changes the data so that you can update the application.
For example, suppose you're building something like a simple spreadsheet application (see the interactive example in Figure 1). Naturally, when the user edits a value in the Quantity column, you'd expect the Total value to automatically update itself. In order to make that happen, you need a way to determine when the user has actually made a change to a value in the Quantity column.
Figure 1. Sample inventory where Total should be updated when Quantity data changes
This sounds like something that should be built in to the DataGrid component, doesn't it? Alas, it's not—and getting it to work isn't as obvious as you'd think, as I discovered when I started down the road of trying to figure out how to make it work while building the Filter Workbench example for the Programming ActionScript 3.0 book in the Flash LiveDocs.
I was building an application using Flash CS3 Professional, which makes heavy use of the new ActionScript 3.0 components included with the authoring tool. In one part of the application, there is a DataGrid component that displays a table of values. In that particular case, the values define a set of colors that make up a gradient, so each row represents a single color and the columns have values for base color, transparency, and color distribution ratio.
In order to make it possible for users to alter these color settings and see different effects, I made the DataGrid component editable by setting its editable property to true. Consequently, by clicking in a cell, a user can change the value in the cell. In the code, the DataGrid component dispatches a few events related to editing in this order:
The key thing that's missing here is a way to determine when, as a result of the user's editing, the data actually changes. Unfortunately, under normal circumstances, when the itemEditEnd event is dispatched, the new value (if any) isn't available yet. For the rest of this article, I'll look at one approach that you might think would work but doesn't, and two approaches that do work, for determining if data actually changes when a user edits a cell of a DataGrid component.
As I dug through the documentation, I noticed that the fl.data.DataProvider class (which I was using as the data source for my DataGrid component) has some interesting events: dataChange and preDataChange. "Ah-ha!" I thought. I could just add a listener for those events, and when the DataGrid component changes the underlying data in DataProvider, it will dispatch those events and (if I needed it, which I didn't) I can grab a snapshot of the old data in preDataChange and get the new data in dataChange. (In my case, I didn't even need to know which data had changed; it was sufficient for my purposes to know that some data had changed, and then just recalculate filter values using all the data in DataGrid.)
Unfortunately, that's not how the DataProvider component works. In fact, its events are designed to handle the opposite use case: If something other than DataGrid changes the data in DataProvider, then DataProvider notifies DataGrid so that it can update itself. But if DataGrid changes DataProvider, then DataProvider assumes that DataGrid already knows about the change (and nobody else would want to), so it doesn't dispatch its events in that case.
So, strike 1 for trying to use the DataProvider component's events.
Since using the DataProvider component was not an option, I thought I'd take a look at the DataGrid component's events.
DataGrid exposes one event, itemEditEnd, that looks like it might be useful. However, there are a couple of problems:
itemEditEnd event is dispatched every time cell editing ends, not just when the cell's data changes. So the fact that the event is dispatched doesn't automatically tell you that the data has changed.itemEditBegin event, which has a corresponding itemEditBeginning event, the itemEditEnd event doesn't have a corresponding event you can use to get the value before it is edited. In fact, in most cases itemEditEnd gives you the value before it is changed, whether or not it actually changes.itemEditEnd event is a fl.events.DataGridEvent class. That class doesn't have any property such as dataChanged or oldValue and newValue, so I didn't see any obvious way to identify whether the data had changed. (The DataGridEvent class does have some properties that end up being useful—but nothing that can be used without some work.)Eventually, after stepping through the DataGrid source code several times as it processed editing events, I was able to figure out a couple of ways to identify when an edited item's value changes in an editable DataGrid component.
The premise behind both of these approaches is this: Normally, when you register a listener for the DataGrid component's itemEditEnd event, the underlying data in DataProvider still contains the old values, and hasn't been updated by DataGrid. In fact, DataGrid itself updates DataProvider by registering for its own itemEditEnd event, and then checking whether the original value is different from the new value and updating DataProvider if that's the case. The question is, if DataGrid is updating the underlying data as part of handling the itemEditEnd event, how can you get the value both before and after it changes, so that you can compare them and see if it's changed?
This is the approach I recommend, although it does require more code because you have to write two event listener functions. The trick behind this approach is the concept of event priority.
As I mentioned earlier, DataGrid itself uses its own itemEditEnd event to update its DataProvider component. If you consider the conceptual model of how events work in ActionScript 3.0, basically when a user ends the item editing process (probably by clicking or tabbing away from a particular cell), the DataGrid component loops through the list of objects that have registered as listeners for the itemEditEnd event, calling each function in turn. Because DataGrid uses itemEditEnd to update its DataProvider component, that could theoretically lead to inconsistent results. Depending on whether your particular listener function is called before or after the DataGrid component's internal itemEditEnd listener function, the value in DataProvider may or may not be updated with the new value.
In order to prevent this potential problem, DataGrid uses event priority to specify that (in the normal case) its own itemEditEnd listener should be called after other itemEditEnd listeners—which is the reason why (in the normal case) DataProvider contains the pre-edit data rather than the new value, if any. Specifically, when DataGrid registers as a listener of its own itemEditEnd event, it does so using a priority of −50.
Here is the actual code, from line 679 in the source code of the DataGrid class:
addEventListener(DataGridEvent.ITEM_EDIT_END,
itemEditorItemEditEndHandler, false, -50);
When you register an event listener in ActionScript, you can optionally specify a priority for your listener. (The default value that most listeners use is 0.) Listeners with a higher priority get called first; listeners with a lower priority get called later. Since the default (rarely changed) priority is 0, any listeners that are registered using the default are called before DataGrid's internal itemEditEnd listener—so they'll get access to the pre-change value.
However, there's no reason that your event listener cannot register with a different priority. For that matter, you can register two different listener functions for the same event using different priorities, which is exactly how this approach works.
You register two functions as listeners for the DataGrid component's itemEditEnd event. The first one should be called before DataGrid updates DataProvider, so it must be registered with a priority greater than −50 (I use 100 here):
// This variable will store a temporary value
// to check if the DataGrid's edited item changes
var tempValue:Number;
// register itemEditEnd listener that will be called before an item's value is updated
myGrid.addEventListener(DataGridEvent.ITEM_EDIT_END, itemEditPreEnd, false, 100);
function itemEditPreEnd(event:DataGridEvent):void
{
// get a reference to the datagrid
var grid:DataGrid = event.target as DataGrid;
// get a reference to the name of the property in the
// underlying object corresponding to the cell that's being edited
var field:String = event.dataField;
// get a reference to the row number (the index in the
// dataprovider of the row that's being edited)
var row:Number = Number(event.rowIndex);
if (grid != null)
{
// gets the value (pre-edit) from the grid's dataprovider
tempValue = grid.dataProvider.getItemAt(row)[field];
// you could also use this line to get the value
// directly from the cellrenderer that's showing the value
// in the datagrid -- it's the same value.
// That way you wouldn't need a reference to the DataGrid.
//tempValue = event.itemRenderer.data[field];
}
}
The second listener should be called after DataGrid updates its DataProvider component, so it must be registered with a priority less than −50 (I use −100 here):
// register itemEditEnd listener that will be called after an item's value is updated
myGrid.addEventListener(DataGridEvent.ITEM_EDIT_END, itemEditPostEnd, false, -100);
function itemEditPostEnd(event:DataGridEvent):void
{
var grid:DataGrid = event.target as DataGrid;
var field:String = event.dataField;
var row:Number = Number(event.rowIndex);
if (grid != null)
{
// gets the value (post-edit) from the grid's dataprovider
var newValue:Number = grid.dataProvider.getItemAt(row)[field];
// you could also use this line to get the value
// directly from the cellrenderer that's showing the value
// in the datagrid -- it's the same value.
// That way you wouldn't need a reference to the DataGrid.
//var newValue = event.itemRenderer.data[field];
// check if the value has changed
if (newValue != tempValue)
{
// do actions that should happen when the data changes
}
}
}
Notice that in the first listener function (itemEditPreEnd), the function stores the current value in a persistent variable, a variable that is declared outside the function scope. In the second listener function (itemEditPostEnd) that stored value is compared to the final value, and if they're different we know that the value of the edited item actually changed.
Figure 2 is a working example of this approach.
Although I recommend the other approach, if you don't like the idea of using two listeners for some reason, or if you're concerned about using two different priorities, there is an alternative approach that uses only one event listener.
In this approach, you create a single listener function and register it with a priority greater than −50 (the default 0 is fine). Let me repeat: Your listener must be triggered before DataGrid updates DataProvider, or you'll get a runtime error.
This approach works by pulling values from two different places. For the pre-change value, it looks in DataProvider (or optionally in the cellrenderer for the cell). For the post-change value, it looks at DataGrid's itemEditorInstance property, which is the actual display object that's used for the editing field, and pulls the raw data from there. Basically, it reads the text property of the TextField that's used in the default item editor.
Where it gets complicated is that it has to dynamically determine the name of the property that's used by the item editor. That way, if you create a DataGrid component that uses a custom item editor (for example, if you have numeric data in a column and use a NumericStepper component whenever that column is being edited, rather than the default text field) then you'll need to know the name of that item editor's property from which you should get the edited value.
Here's what the code looks like:
// register itemEditEnd listener to determine when an item is changed
myGrid.addEventListener(DataGridEvent.ITEM_EDIT_END, itemEditEndHandler);
function itemEditEndHandler(event:DataGridEvent):void
{
// get a reference to the datagrid
var grid:DataGrid = event.target as DataGrid;
// get a reference to the name of the property in the
// underlying object corresponding to the cell that's being edited
var field:String = event.dataField;
// get a reference to the row number (the index in the
// dataprovider of the row that's being edited)
var row:Number = Number(event.rowIndex);
// get a reference to the column number of
// the cell that's being edited
var col:int = event.columnIndex;
if (grid != null)
{
// gets the value (pre-edit) from the grid's dataprovider
var oldValue:Number = Number(grid.dataProvider.getItemAt(row)[field]);
// you could also use the following line to get the value
// directly from the cellrenderer that's showing the value
// in the datagrid -- it's the same value.
// That way you wouldn't need a reference to the DataGrid.
//var oldValue = event.itemRenderer.data[field];
// get the value (post-edit) from the item editor
var newValue:Number = Number(grid.itemEditorInstance[grid.columns[col].editorDataField]);
// check if the value has changed
if (newValue != oldValue)
{
// do actions that should happen when the data changes
// Note that in this case, the dataprovider
// hasn't been updated yet, so you can't do any
// actions that require the dataprovider to have
// the new data
}
}
}
In fact, if you already know for certain what type of object is being used as the item editor, you don't need to use such convoluted code. For example, with the default item editor, grid.columns[col].editorDataField will always be text (the .text property), so you can just use code like this to get the post-edit value:
var newValue:Number = Number(grid.itemEditorInstance.text);
Of course, if you're using some custom item editor, you can just hard-code the appropriate property name. For example, if you're using a NumericStepper component instead of the default item editor, you can use the value property instead of the text property:
var newValue:Number = grid.itemEditorInstance.value;
Note: The biggest drawback to this approach, as explained in the code comments is that the DataProvider component hasn't been updated by the DataGrid component yet, so if you want to do anything using DataProvider, this approach won't work unless you use some technique to delay your actions until after DataGrid has updated DataProvider. As I stated previously, you cannot use this technique after DataGrid has updated DataProvider; if you do, you will not be able to get the pre-edit value (because DataProvider will have already changed) and you'll get a runtime exception because the item editor will have already been destroyed, and you won't be able to access it anymore to get the new value.
Figure 3 is a working example of this approach. (It looks the same as Figure 2, of course.)
Using components can make building applications much easier. However, it occasionally comes with some complications. Nevertheless, I think the Flash CS3 Professional components constitute the best Flash component set yet because they're lightweight (they don't bloat your SWF's file size too much) and they were designed to be easy to skin, so you can make your components match the appearance of your application's design with greater flexibility and ease than ever before in Flash.
Another important feature of the Flash CS3 components is that the source code is included with Flash. Not only can you see the source code, but it is possible to create your own version of the source code and instruct Flash to use your version rather than the original, without jumping through many hoops, compared to how things worked in previous versions.
If you are feeling ambitious, you can look at the source code for the DataGrid component and add your own code to make it dispatch a new event (or some other mechanism), so that the component notifies you when a user's editing actually changes a cell's data. In the process of working out the solutions presented in this article, I looked through the source code in detail. I think it could be done with only a few lines of code, so if you're looking for a component-related challenge, then give it a shot.

This work is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 Unported License
| 04/23/2012 | Auto-Save and Auto-Recovery |
|---|---|
| 04/23/2012 | Open hyperlinks in new window/tab/pop-up ? |
| 04/21/2012 | PNG transparencies glitched |
| 04/01/2010 | Workaround for JSFL shape selection bug? |
| 02/13/2012 | Randomize an array |
|---|---|
| 02/11/2012 | How to create a Facebook fan page with Flash |
| 02/08/2012 | Digital Clock |
| 01/18/2012 | Recording webcam video & audio in a flv file on local drive |