AS3: DataGrid with custom ItemRenderers
One of the big advantages of Flex framework is that you can use a lot of ready components. The DataGrid is really helpful. Especially when you want to list big amount of data and provide ability to edit the information directly in the grid. These days I finished a test task which uses DataGrid and decided to share my experience.
There are tons of examples out there about DataGrid implementation, but most of them are using MXML, which I'm not a big fan of. I prefer to use pure AS3 instead of mixing both things. The code of the example could be found here and the result is shown below. This is DataGrid with three columns. The first one displays just plain text. The second one displays date and when the user clicks on it he is able to change it directly in the grid. Similar feature is implemented for the third column. There is a ComboBox for all the values. The columns are sortable.
var swf = new FlashObject( "http://krasimirtsonev.com/files/datagridsorting/bin/swf/Project.swf", "animationSwf", "500", "400", "9", "#FFFFFF" ); window.onload = function() { swf.write("animation"); };
1. Initialize the grid and add it to the stage
_grid = new DataGrid();
_grid.percentWidth = 100;
_grid.percentHeight = 100;
_grid.columns = _columns; // an array of DataGridColumn objects
_grid.dataProvider = _dp; // data provider
addChild(_grid);
2. Initialize the data grid columns
_columns array should contain objects of type DataGridColumn. The first column is a simple one so I used directly DataGridColumn class. The others are more complex, that's why I extended DataGridColumn and added aditional functionalities.
_columns = [];
var column1:DataGridColumn = new DataGridColumn();
column1.headerText = "Name:";
column1.dataField = "name";
_columns.push(column1);
var column2:DateColumn = new DateColumn("date");
column2.headerText = "Date:";
_columns.push(column2);
var column3:RatingColumn = new RatingColumn("rating");
column3.headerText = "Rating:";
_columns.push(column3);
headerText property contains the name of the column - the string that will be shown in the header of the data grid. dataField is the name of the property in the data provider object. For the first column the data field is assigned directly, but for the other two custom classes I decided to set the values in the costructor of the class.
3. Initialize the data provider
The data provider is nothing more than an array of objects. Have in mind that the properties of these objects should have the same names as the data fields of the columns.
_dp = [
{name: "Steve", rating: "2", date:"10.12.2008"},
{name: "Martin", rating: "2", date:"05.01.2009"},
{name: "John", rating: "4", date:"04.04.2008"},
{name: "Peter", rating: "7", date:"12.04.2010"},
{name: "Ivan", rating: "3", date:"23.05.2010"},
{name: "Walt", rating: "8", date:"18.05.2010"},
{name: "Jessy", rating: "10", date:"05.06.2011"},
{name: "Smit", rating: "6", date:"01.07.2001"},
{name: "Dave", rating: "9", date:"10.12.2011"}
];
Every row of the data provider repesents a row from the data grid. And each of these objects will be passed to the custom ItemRenderer of the column. That means that you can add as property whatever you want and you will have access to it later. I needed to know when the user changes something in the grid's data. Instead of listening for events I decided to add a callback function in the data provider's objects. I had two custom ItemRenderers so I needed to add two callbacks. The following helper function helps me to do it:
private function setOnChangeCallback(dataField:String, callback:Function):void {
var numOfRows:int = _dp.length;
for(var i:int=0; i<numOfRows; i++) {
_dp[i][dataField + "Callback"] = callback;
}
}
And the usage:
setOnChangeCallback("rating", onRatingChange);
setOnChangeCallback("date", onDateChange);
The callbacks look like that:
private function onRatingChange(row:Object):void {
trace("rating change - " + row.name + ": " + row.rating);
}
private function onDateChange(row:Object):void {
trace("date change - " + row.name + ": " + row.date);
}
4. Date column class
package lib {
import lib.helpers.DateFormater;
import mx.controls.dataGridClasses.DataGridColumn;
import mx.core.ClassFactory;
public class DateColumn extends DataGridColumn {
public function DateColumn(dataFieldParam:String) {
dataField = dataFieldParam; // setting the property name from the data provider
var c:ClassFactory = new ClassFactory(); //
c.properties = {dataField: dataFieldParam}; // setting the custom
c.generator = DateItemRenderer; // item renderer
itemRenderer = c; //
sortCompareFunction = sortDate; // setting the custom sort function
rendererIsEditor = true;
}
private function sortDate(obj1:Object, obj2:Object):int {
var d1:Number = DateFormater.str2Date(obj1.date as String).getTime();
var d2:Number = DateFormater.str2Date(obj2.date as String).getTime();
if(d1 < d2) {
return -1;
} else if(d1 == d2) {
return 0;
} else {
return 1;
}
}
}
}
DateFormater class is a helper class which converts String to a Date object and vise versa. sortDate compares two row's objects and returns -1 if obj1 should appear before obj2 in ascending order, 0 if obj1 = obj2 or 1 if obj1 should appear after obj2 in ascending order.
5. Date column custom item renderer
The idea of the custom renderer is to change the content of the grid's cell. For example for the date column I decided to use a Label (just a simple text holder) and DateField (text holder with date selection). Firstly I'm showing the label and after the user clicks on it I'm hiding it and showing the date field.
package lib {
import flash.events.Event;
import flash.events.MouseEvent;
import flash.utils.setTimeout;
import lib.helpers.DateFormater;
import mx.containers.Canvas;
import mx.controls.DateField;
import mx.controls.Label;
import mx.core.ScrollPolicy;
import mx.events.CalendarLayoutChangeEvent;
import mx.events.DropdownEvent;
import mx.events.FlexEvent;
public class DateItemRenderer extends Canvas {
private var _label:Label;
private var _dateField:DateField;
public var dataField:String;
public function DateItemRenderer() {
verticalScrollPolicy = ScrollPolicy.OFF;
horizontalScrollPolicy = ScrollPolicy.OFF;
_label = new Label();
_label.percentWidth = 100;
_label.percentHeight = 100;
_label.addEventListener(MouseEvent.CLICK, onLabelClick);
addChild(_label);
_dateField = new DateField();
_dateField.formatString = "DD.MM.YYYY";
_dateField.width = 150;
_dateField.height = 20;
_dateField.addEventListener(CalendarLayoutChangeEvent.CHANGE, onCalendarClose);
_dateField.addEventListener(DropdownEvent.CLOSE, onCalendarClose);
addEventListener(FlexEvent.DATA_CHANGE, onDataChanged);
}
override protected function commitProperties():void {
_label.text = data[dataField];
}
private function onLabelClick(e:MouseEvent):void {
if(contains(_label)) {
removeChild(_label);
}
addChild(_dateField);
_dateField.selectedDate = DateFormater.str2Date(data[dataField]);
setTimeout(openCalendar, 200);
}
private function openCalendar():void {
_dateField.open();
}
private function onCalendarClose(e:Event):void {
if(contains(_dateField)) {
removeChild(_dateField);
}
data[dataField] = DateFormater.date2Str(_dateField.selectedDate);
data[dataField + "Callback"](data);
_label.text = data[dataField];
addChild(_label);
}
private function onDataChanged(e:FlexEvent):void {
invalidateProperties();
}
}
}
In line 62 I called the callback, which I was talking about before. Of course I also sent the changed row data. The code of the third column and its renderer is really similar to the classes above. You can see them here: RatingColumn.as RatingItemRenderer.as