Subscribe to Latest Posts

21 Mar 2010

Flex 4 is gold (build 4.0.0.14159)

Posted by TomeK. No Comments

Hopefully we’ll see an official Flex SDK, Flash Builder & Player 10.1 release this week.

This is the single most exciting month in Flex history :) So many major Adobe products coming out in a single months time.

Flash Builder 4 (incl. SDK)

http://www.adobe.com/devnet/flex/

Standalone SDK:
http://opensource.adobe.com/wiki/display/flexsdk/Download+Flex+4

18 Mar 2010

Creating an Enterprise Flex Grid – Dynamic dataProvider properties (part 2)

Posted by TomeK. 4 Comments

In Part 1 – Dynamic DataGrid Columns of this Creating an enterprise Flex Grid series we quickly set up a ActionScript based grid that would let us programmatically set an arbitrary number of columns. In this part we will actually hook this grid to a data provider that can accommodate dynamic columns and seamlessly enable all DataGrid features to ‘just work’.

There is no good enterprise application without objects (if possible of course), so before we get start, some housekeeping first.

Step 1) Convert loose XML or Object to strongly typed

[Bindable] private var rows:ArrayCollection =
	new ArrayCollection([
		new {product: 'Ski',   price0: 10, price1: 20... },
		new {product: 'Sword', price0: 0,  price1: 0, ...},
		new {product: 'Beer', price0: 5,  price1: 5,...}]);

To use this strongly typed object:

public class ProductPrices
{
	public function ProductPrices() {}

	public var product:String;
	public var price0:Number;
	public var price1:Number;
	public var price2:Number;
	public var price3:Number;
	public var price4:Number;
	public var price5:Number;
}

Step 2) Convert strong object to use internal Array within each row

We now have type safety on our columns and a somewhat structured data provider object that will represent each of the rows in the grid. What we are missing is the ability to have an arbitrary number of prices across time. Like our DataGridColumn problem this is easily fixed by converting to an array.

public class ProductPrices
{
	public function ProductPrices() {}

	public var product:String;
	public var pricesAccrossTime:Array;
}

Problem – DataGrid only supports a flat dataProvider

Great except now how do we hook a row (ProductPrices) that has an internal Array (of prices) into our grid? This is not supported out of the box otherwise this article wouldn’t exist. i.e. You cannot have DataGridColumn.dataField = [ProductPrices.] “pricesAccrossTime[0]“, it needs to be [ProductPrices.] “price2″

We have 2 possibilities:

  1. Use a column labelFunction to return the value of pricesAccrossTime[columnIndex]
  2. Make our change not at the logical View but Model (data class ProductPrices) (think Model View Controller). This change will make our internal dynamic data representation totally transparent to our grid or chart (if we chose to expose this as a graph). This is the preferred solution here but I’d like to discuss why labelFunction is not enough to solve this problem

Why is labelFunction not enough?

Blurb: labelFunctions, sortComparers, groupingFunctions etc are great hooks provided by the Flex framework. While they will let you totally bypass some limitations of your Model they are really not always intended to replace it. Take labelFunction, some people use it to fetch some data (in the View) but really the fetching of data belongs to another layer (Model). Best use of label function would be for formatting reasons and not as an absolute replacement. We’ll talk more about this later.

Solution – labelFunction (kind of) to the rescue

Let’s go ahead and create the labelFunction solution now.

First we will need to create a custom DataGridColumn which will contain some extra information for us so we can easily map a column to an index in our pricesAccrossTime Array.

Add new ActionScript class CustomPriceColumn.as

package
{

import mx.controls.dataGridClasses.DataGridColumn;

public class CustomPriceColumn extends DataGridColumn
{
	public function CustomPriceColumn(columnName:String=null)
	{
		super(columnName);
	}

	// This will let us track which price to use for each column
	// think of this as a columnIndex for our prices
	public var priceIndex:int;
}
}

Next we will create our labelFunction that will fetch the correct data at an index, we do this in main application.

/**
 * Function that will return the price for each time period
 */
private function getPricesLabel(item:ProductPrices, column:CustomPriceColumn) : String
{
	return item.pricesAccrossTime[column.priceIndex].toString();
}

Here is the complete labelFunction Solution, have a look at Source View to find a few other minor changes. We basically added a few helper methods to help us create our dynamic data provider and modified the column creation ever so slightly.

labelFunction Solution (click to run and view code):

Everything looks great doesn’t it? Seems like we are done. We now have dynamic columns tied to dynamic dataProvider. Well that’s only ½ true. Want to break the grid? Click on a header to sort, CRASH. This is only one way to now crash the grid, it’s mainly caused by the fact that we did not provide an explicit dataField for a column. The Flex DataGrid & more so AdvancedDataGrid is an incredibly powerful control, as is the underlying ArrayCollection>ListCollection infrastructure but the absolute key for all this to behave correctly is dataField.

I cannot stress this enough, unless you are writing your custom grid you must supply a dataField.

#1 rule of Flex

Always, always, always conform to default behaviour of a Flex control. In the DataGrid and Chart the default that controls almost everything is the column level dataField property. I’m not saying you can’t customize and hook into the controls, in fact you should since your logic can be best optimized by you, but be sure to respect the other 90% of the grid features you have not customized, they should still work!

Sure, in this example, the sort crashed because the grid did not know how to sort. You can supply a custom sortCompareFunction and the sort problem would be fixed. But I have to ask you: what else did you not do that might otherwise cause the grid to crash? How a about the Editor, making the columns editable would crash just the same, grouping using the Flex AdvancedDataGrid would also crash and this list goes on.

So let’s fix this default behaviour problem by supply the DataGrid with a dataField property, I know I know, we just had one then decided to remove it to make things ‘dynamic’, that’s true but read on Jedi. So how do we tell the grid this when our new object has an array and not a fixed property called price0, price1 etc?

The magic answer to this is a Flex class Proxy or ObjectProxy to be more specific. ObjectProxy lets the consumers of your typed object think they have a real property like price1 when it doesn’t really exist. For reasons of keeping this post from turning into a book I will let you Google Flex ObjectProxy and learn more about this topic but for now lets continue to the solution to our dynamic columns problem.

Flex ObjectProxy

Lets take a deep look at our ProductPrices class. We need to take public var pricesAccrossTime:Array; and expose it to the outside world as price0, price1 etc.

To accomplish this our ProductPrices cass needs to inherit from ObjectProxy. There are 3 main methods we need to override getProperty, setProperty & hasProperty. Think of these like this:

getProperty -  trace(myObject.price0)
// here the getProperty is called to return the value of price0
setProperty -  trace(myObject.price0 = 33)
// here the setProperty is called to set price0 to 33
(getProperty is also called here)
hasProperty - is used to ask our custom object
(ProductPrices) if it has some virtual property like price0.

We will add 2 custom methods that will help us wrap this logic

public function getPricesAccrossTimeAt(priceIndex:int) : Number
{
	return pricesAccrossTime[priceIndex];
}

public function setPricesAccrossTime(value:Number, priceIndex:int) : void
{
	pricesAccrossTime[priceIndex] = value
}

Now here is the magic (it’s not really magic and I hope in a few minutes you can realize this but for now it’s magic :) ), I have provided a few comments to clear things up but feel free to take all the time you need to absorb this.

ObjectProxy is probably the most powerful feature ActionScript gives us.

Once you master this you will never go back to hard coding dynamic properties, this comes with my own personal guarantee.
 
 
 
Ok, stop here
 
 
 
Breathe
 
 
 
Bathroom break
 
 
 
Coffee/tea/water
 
 
 
Ready?
 
 
 
Go start absorbing:
 
 
 

package
{
import flash.utils.flash_proxy;

import mx.utils.ObjectProxy;

// Need this to use dynamic properties
use namespace flash_proxy;

public class ProductPrices extends ObjectProxy
{
	public function ProductPrices() {}

	public var product:String;

	// keep this public so we are able to access this from our own custom
	// code it the View (application that has the DataGrid)
	public var pricesAccrossTime:Array;

	public function getPricesAccrossTimeAt(priceIndex:int) : Number
	{
		return pricesAccrossTime[priceIndex];
	}

	public function setPricesAccrossTimeAt(value:Number, priceIndex:int) : void
	{
		pricesAccrossTime[priceIndex] = value
	}

	/**
	 * Helps us figure out if we are a custom property that we want to explicitly handle
	 * .i.e. price0 is something we want to intercept but product or uid is not,
	 * we want Flex to handle this as it normaly would any property
	 */
	private function isPropertyPricesAccrossTime(propertyName:String) : Boolean
	{
		return (propertyName != null &&
			propertyName.substr(0, priceAccrossTimeKey.length) == priceAccrossTimeKey);
	}

	// help us strongly track
	// our new dataField format is same as before.
	// The word 'price' followed by some index, ie.e price2
	public static var priceAccrossTimeKey:String = "price";

	//--------------------------------------------------------------------------
	//
	// ObjectProxy methods
	//
	//--------------------------------------------------------------------------

	//////////////////////////////////////
	// Object Proxy section
	//////////////////////////////////////
	override flash_proxy function getProperty(name:*):*
	{
		var propertyName:String;
		if (name is QName) {
			propertyName = (name as QName).localName;
		}

		// Custom hook, this is where we intercept
		// the 'get' of something like price2
		if(isPropertyPricesAccrossTime(propertyName))
		{
			return getPricesAccrossTimeAt(
				parseInt(propertyName.replace(priceAccrossTimeKey, "")));
		}
		else
		{
			super.getProperty(name);
		}
	}

	override flash_proxy function setProperty(name:*, value:*):void    {

		var propertyName:String;

		if (name is QName) {
			propertyName = (name as QName).localName;
		}

		// Custom hook, this is where we intercept
		// the 'set' of something like price2
		if(isPropertyPricesAccrossTime(propertyName))
		{
			setPricesAccrossTimeAt(value,
				parseInt(propertyName.replace(priceAccrossTimeKey, "")));
		}
		else
		{
			super.setProperty(name, value);
		}
	}

	override flash_proxy function hasProperty(name:*):Boolean
	{
		// Custom hook, this is where we intercept
		// the 'has/contains' of something like price2
		return (isPropertyPricesAccrossTime(name) || super.hasProperty(name));
	}
}
}

Hopefully that was fun and it sunk in. Now the only additional changes we need to make in our Mxml is to re-add dataField property to column.

private function getPriceColumn(priceIndex:int, headerText:String) : DataGridColumn
{
	var column:CustomPriceColumn = new CustomPriceColumn();

	// we reintroduce this again since we now have an object that can supply us with this value
	// our new dataField format is same as before. The word 'price' followed by some index
	column.dataField = ProductPrices.priceAccrossTimeKey + name;

	// we leave this as its useful meta data
	column.priceIndex = priceIndex;

	column.headerText = headerText;

	//we keep labelFunction since we will want to actually use it for correct purpose later (like add $ symbol to price)
	column.labelFunction = getPricesLabel;

	return column;
}

Here is the complete solution for Part 2.

View Solution

In the next part of this series we will explore editing of this data and how to correctly capture changes that a user makes.

Until next time, keep your Flex sharp.

Part 3 Capturing changes in a dynamic dataProvider (Coming soon)

17 Mar 2010

Disable sort when values in Flex DataGrid change

Posted by TomeK. 8 Comments

In a sorted Flex DataGrid/AdvancedDataGrid, rows are automatically re-sorted when you update values in the grid. This is obviously not ideal behavior when a user is trying to edit some data and rows keep rearranging themselves. The root cause of this behavior issue is ListCollectionView. This is the base class & heart of the ArrayCollection and your entire Flex grid. The solution to prevent this reordering on an already sorted list is not trivial.

There are 2 options

  1. re-implement ListCollectionView as your own (i.e. CustomListCollectionView.as)
  2. monkey patch Flex SDK ListCollectionView

The place where all the magic happens is a private method so there is no clean way to extend the class.

You can choose to make your own ArrayCollection and ListCollectionView and modify them. To do this simply copy paste those classes from Flex SDK and use as your own (rename). This is a lot of work if you want to replace them correctly so might be easier to just monkey patch them (copy paste classes exactly into your own project and make a folder structure like {PROJECT}\src\mx\collections) Google Flex monkey patch for more info.

Here is what I did.

I introduced a new variable to ListCollectionView.as

/**
* Prevents collection from reordering itself when values change, list is only re-ordered on refresh()
*/
public var enablePostSortReorder:Boolean = true; //this is what framework does by default,
// set to false to do what you want but better to specify this in a custom ArrayCollection class so that you don't change the default behavior of existing controls(AC/LCV) (see below)

In

private function handlePropertyChangeEvents(events:Array):void

Change

if (sort || filterFunction != null)

to

if (enablePostSortReorder && (sort || filterFunction != null))

You will probably also want to extend ArrayCollection. i.e.

public class CustomArrayCollection extends ArrayCollection

public function CustomArrayCollection(source:Array = null)
{
super(source);
enablePostSortReorder = false;
}

This is the new ArrayCollection you should be binding your grid to. So myGrid.dataProvider = new CustomArrayCollection(someItems). Because you are extending the standard ArrayCollection (and AC extends ListCollectionView) everything will ‘just work’.

The usage of your new CustomArrayCollection is same as before. You still set sort on it myItems.sort = someSort. And you still call myItems.refresh() after. This change will not affect default sort behavior. Notice that the method we modified is only fired when propertyChange happens. If you trace it back up the stack you will see its not connected to the refresh() method called when you change sort (be it programatically or via clicking header and having Flex grid do it for you).

Hope this helps, I’ve been using this solution for a while now in a very big ADG.

17 Mar 2010

Creating an Enterprise Flex Grid – Dynamic Columns (Part 1)

Posted by TomeK. 2 Comments

Dynamic DataGrid Columns (Part 1)

In this multi-part we will create a powerful Flex enterprise Excel like grid. It will by dynamic in every way possible. We will support an arbitrary number of columns hooked in to a strongly typed data provider that is capable of a arbitrary number of properties. Later we will even learn how to perform on the fly calculations between cells. We will start simple but get to advanced by end of Part 2.

Part 1 Goal

Create a dynamic column DataGrid/AdvancedDataGrid. In this example we need dynamic column because we are displaying prices across an arbitrary time range. The Product column is fixed but time columns horizontally scroll.

Here is our starting MXML/ActionScript code

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
				verticalAlign="middle" horizontalAlign="center" backgroundColor="white"
				layout="vertical" width="100%" height="100%">
<mx:Script>
	<![CDATA[

import mx.collections.ArrayCollection;
[Bindable] private var rows:ArrayCollection =
	new ArrayCollection([
		{product: 'Ski', price0: 10, price1: 20,
			price2: 30, price3: 40, price4: 50, price5: 60},
		{product: 'Sword', price0: 0, price1: 0,
			price2: 3, price3: 4, price4: 0, price5: 0},
		{product: 'Beer', price0: 5, price1: 5,
			price2: 6, price3: 4, price4: 4, price5: 5}]);

	]]>
</mx:Script>
<mx:DataGrid width="400" height="150" lockedColumnCount="1"
			 horizontalScrollPolicy="on" dataProvider="{rows}">
	<mx:columns>
		<mx:DataGridColumn headerText="Product" dataField="product" width="100" />
		<mx:DataGridColumn headerText="Jan 2010" dataField="price0" width="75"/>
		<mx:DataGridColumn headerText="Feb 2010" dataField="price1" width="75" />
		<mx:DataGridColumn headerText="Mar 2010" dataField="price2" width="75" />
		<mx:DataGridColumn headerText="Apr 2010" dataField="price3" width="75" />
		<mx:DataGridColumn headerText="May 2010" dataField="price4" width="75" />
		<mx:DataGridColumn headerText="June 2010" dataField="price5" width="75" />
	</mx:columns>
</mx:DataGrid>
</mx:Application>

Obvious problems

  1. Hard coded columns representing months of the year. This obviously doesn’t cut it if we wanted to display an arbitrary time range
  2. Our data provider is not yet strongly typed and has fixed properties for data in columns (price0, price1, price2 etc).

Notes: please note that the exact problem applies to standard DataGrid as well as AdvancedDataGrid.

Solution – Hard coded columns to dynamic columns.

Before we attempt to fix hard coded column definitions lets fix 1 tiny thing first. Since we are displaying an arbitrary number of columns we might not even have the labels for those headers, let’s create the grid dynamically in ActionScript instead.

Add event to Application

<mx:Application creationComplete="creationCompleteHandler(event)"
private function creationCompleteHandler(event:FlexEvent) : void
{
	var columns:Array = new Array();

	columns.push(getColumn("product", "Product"));
	columns.push(getColumn("price0", "Jan 2010"));
	columns.push(getColumn("price1", "Feb 2010"));
	columns.push(getColumn("price2", "Mar 2010"));
	columns.push(getColumn("price3", "Apr 2010"));
	columns.push(getColumn("price4", "June 2010"));

	var grid:DataGrid = new DataGrid();
	grid.columns = columns;
	grid.width = 400;
	grid.lockedColumnCount = 1;
	grid.horizontalScrollPolicy = ScrollPolicy.ON;

	grid.dataProvider = rows;

	//add grid to application, this can be wherever you want,
	// maybe some Box or Canvas placeholder
	this.addChild(grid);
}

/**
 * Helper method to get column
 */
private function getColumn(dataField:String, headerText:String) : DataGridColumn
{
	var column:DataGridColumn = new DataGridColumn(dataField);
	column.headerText = headerText;
	return column;
}

Now that we have this grid in ActionScript code we should be able to see something obvious: why not just put the column creation in a loop? Exactly.

In this example the Product column is something concrete so we’ll keep it fixed (lockedColumnCount=”1″) and focus on dynamic time periods.

var columns:Array = new Array();

columns.push(getColumn("product", "Product"));

// Add as many time periods as we want here (we will make this dynamic later)
var timePeriods:Array = ["Jan 2010", "Feb 2010", "Mar 2010",
							"Apr 2010", "June 2010"];

for(var i:int = 0; i < timePeriods.length; i++)
{
	columns.push(getColumn("price" + i, timePeriods[i]));
}

We have now essentially separated the column creation to be driven by some Array of time periods, everything else should take care of itself provided that our data provider has the necessary data, i.e. that it has enough “priceX” properties, price0 to priceN where N is number of columns.


View complete code

In part 2 we will discuss what is perhaps the hard part of this solution, dynamic strongly typed object properties

Part 2 Dynamic dataProvider properties