Archive for September, 2008

Binding Flex TextInput UI Controls to a DataProvider

In Flex when you bind data to a UI control like a DataGrid, the grid cells refresh every time the dataSource changes. The reverse is also true if the DataGrid is enabled for editing. That is, the dataSource is also updated when you edit a cell.

The TextInput can be bound as well so that when the dataSource changes, the value of the TextInput will automatically update. However, unlike the DataGrid, changing the text of the TextInput will not automatically update the dataSource. Take the following code for example (assuming “source” is a String variable):

<mx:TextInput id="example" text="{source}" />

When the TextInput is changed, the value of source remains the same. It’s only bound one-way. If you want the value of source to be updated when TextInput changes, it’s actually easy, but there are at least five (5) different ways to do it of which I know. For the most straight-forward two-way binding, you could update the TextInput code like so:

<mx:TextInput id="example" text="{source}" valueCommit="{source = example.text;}" />

Technically source is not bound to the TextInput, but it does produce the desired result. source is updated manually whenever the valueCommit event fires. The valueCommit event fires when the TextInput text has been changed onBlur (ie when when TextInput loses focus). If you prefer source to be updated on every key stroke, you can change valueCommit to change instead and the update will occur on every keyUp. If I’m updating a database or making a service call, I prefer valueCommit so the back-end code only fires once after the user is finished updating the field. If the TextInput is an ajax-style auto complete or lookup, the change event might be more desirable so the application can react after each key stroke.

As I mentioned there are five methods to do this. You can bind controls using Flex’s binding features in either MX code or ActionScript. Depending on your application one may be better than the rest as far as keeping your code clean and consistent. For the most part they all achieve the same result. Attached is source code that demonstrates all five techniques:

Download TextInputBinding.zip

If you know of any other ways to bind data to UI Controls, please post a comment.

Handling data updates with the Flex AS3 DataGrid

The Flex DataGrid is an incredibly useful component however there is one feature that is strangely missing in AS3 which is a cellEdit event.  One would assume that a listener could be attached to the DataGrid which gets fired when a cell is edited – which is true.  You can listen to the itemEndEdit event, unfortunately itemEditEnd is fired before any of the controls or dataProvider are updated. You have access to the original cell value, but the newly entered value is difficult to obtain.

An article by Paul Robertson from Adobe explains the sequence of DataGrid events and provides two workarounds that allow you to get the old and new values when a cell is edited.  You can actually put his solution to work right away and get on with your day.

I wasn’t fully satisfied with this approach only because it seems weird that Adobe would make us jump through all these hoops just to get a common bit of data. Paul suggests that that DataProvider would be a good place to listen, but he didn’t find any suitable events. As it turns out, this is exactly the way to do it and I’ll provide example source code below. It actually makes more sense because the DataGrid is just a view and you could have multiple views of this same data throughout your application.  When changes occur, they’re handled centrally by the model. This also fits nicely with the current trend of using a model locator singleton and binding UI controls to it. If your UI is bound to the model and the model is listening for changes, the whole thing just works and you don’t really need any code to handle data updates.

The relevant classes in the Flex API are the CollectionEvent.COLLECTION_CHANGE event, which is fired on every change.  Updates will contain one or more PropertyChangeEvents which have properties “oldValue,” “newValue” and several other useful properties.

Example Code:

Download DataGridExample.zip

If you don’t want to download the project, the working parts of the code look something like this (note this won’t compile by itself):

<mx:Script>
<!--[CDATA[
 
[Bindable]
private var gridData:ArrayCollection = new ArrayCollection();
 
/** 
 * initialize the data source (fired by creationComplete in WindowedApplication)
 */
private function onCreationComplete():void
{
  // the listener will detect all changes made to the collection
  gridData.addEventListener(CollectionEvent.COLLECTION_CHANGE,onCollectionChange);		
 
  // the source of an ArrayCollection is just an array.  we can replace it with
  // whatever we want .  note that this does not cause the change event to fire
  gridData.source = Widget.getDummyData();
}
 
/** 
  *This is the event handler when any gridData data has been 
  * changed, regardless of what UI Control changed it 
  */
private function onCollectionChange(event:CollectionEvent):void
{
  if (event.kind == CollectionEventKind.UPDATE)
  {
    var propChangeEvent:PropertyChangeEvent = event.items[0] as PropertyChangeEvent;
    var existingItem:Widget = propChangeEvent.source as Widget;
    // TODO: update the database, web service, etc
  }
  else if (event.kind == CollectionEventKind.ADD)
  {
    var newItem:Widget = event.items[0] as Widget;
    // TODO: insert into the database, web service, etc.
  }
}
]]>
</mx:Script>
 
<!-- ###  Notice there is no listener attached to the grid ### -->
<mx:DataGrid id="dg1" editable="true" dataProvider="{gridData}" width="100%" height="100%">
</mx:DataGrid>

Thanks to Paul for getting my gears spinning. I’d appreciate any comments or links to similar example code.

Working with dates in Flex AIR and SQLite

UPDATE 3/25/09: Paul Robertson from the AIR team stopped by and writes that declaring your SQLite column affinity (ie column type) as “DATE” will instruct AIR to handle all date conversions for you automatically.  The problems I experienced mainly surfaced in a DataGrid when using a DateField and I have not had a chance to poke around with that yet.  Another approach is to extend DateField and override the “data” setter which is how the DataGrid supplies item editors with their value.  Then you can deal with casting issues manually, however that may be more of a hack.  The article below still has some good information that is still helpful for dealing with Dates in AIR/SQLite.

Working with SQLite and Flex/AIR Date values can be tricky and various caveats are not particularly well documented. The confusion (for me) is that ActionScript is loaded with UTC functions, and SQLite will happily insert them into DATETIME columns. Everything appears fine, however SQLite does not actually recognize this format as a Date and treats it as plain text. You have no way to see this happened until you try to apply some date formatting functions and notice SQLite returning NULL. SQLite is so lax about data integrity that you can insert anything into any column type and will never receive any warnings. AIR, though, will attempt to cast values behind the scenes based on column types and so you will run into ‘Invalid Date’ errors and weird glitches when attempting to update data.

The magic solution is the Julian Date Format which both SQLite and AIR recognize as a date value. This is somewhat surprising as ActionScript has no built-in support for outputting Julian dates. If you’re like me, you may have already hacked up workarounds using int fields with timestamps, however your matching ActionScript class properties have to be hacked to match, and the hacking can trickle down throughout your code. This also prevents you from using the SQLStatement.itemClass functionality, which is nice when using Cairngorm, DAOs, value objects, etc.

To avoid the pain follow these rules when working with dates:

1. If you want a strongly typed Date field in AIR, the relevant SQLite column must be defined as DATETIME.  The interesting thing about this is that DATETIME is not technically a recognized SQLite column type and according to the SQLite docs it will be considered numeric.  But AIR is obviously looking at the column definition somewhere in the framework because it will refuse to automatically cast any value where the column type is not DATETIME.

2. Whenever inserting or updating DATETIME fields, you must store it in Julian format (or NULL).  SQLite will happily accept many common date formats. However AIR will behave inconsistently. Here is how to insert Julian dates in a variety of ways:

Inserting a Julian date manually via SQL:

To do this, simply have SQLite format your date value to Julian format using ‘%J’

UPDATE my_table SET my_column = STRFTIME('%J','2008-01-02 03:04:05')

Inserting a Julian date via AIR (with parameters):

Parameters are the best way to build SQL statements as you can use strongly typed Date variables and AIR will deal with the formatting for you.

statement.text = "UPDATE my_table SET my_column = :my_value";
statement.parameters[":my_value"] = new Date(2008,0,2,3,4,5); // Jan 02, 2008 03:04:05

Inserting a Julian date via AIR (without parameters):

If you are not using parameters, you have to pre-format the date into something that SQLite can parse. This is surprisingly obnoxious and requires you to write a couple of helper functions. (Note – if you know of an easier way to do this, please post a comment.)

public function lpad(original:Object, length:int, pad:String):String
{
var padded:String = original == null ? "" : original.toString();
while (padded.length &lt; length) padded = pad + padded;
return padded;
}
 
public function toSqlDate(dateVal:Date):String
{
return dateVal == null ? null : dateVal.fullYear
+ "-" + lpad(dateVal.month + 1,2,'0')  // month is zero-based
+ "-" + lpad(dateVal.date,2,'0')
+ " " + lpad(dateVal.hours,2,'0')
+ ":" + lpad(dateVal.minutes,2,'0')
+ ":" + lpad(dateVal.seconds,2,'0')
;
}
 
var myDate:Date = new Date(2008,0,2,3,4,5); // Jan 02, 2008 03:04:05
statement.text = "UPDATE my_table SET my_column = strftime('%J','" + toSqlDate(myDate) + "')";

Fudging data to work around AIRs validation

If you absolutely refuse to change your schema (for example you insist on using timestamps, or you have to maintain compatibility with other clients) you can get AIR to play along during READ operations by altering your select statement like so:

SELECT STRFTIME('%J',my_column) AS my_column FROM my_table

This does assume that the data is in a format that SQLite recognizes as a date. If SQLite can’t parse the date value, then it will just return NULL. For hilarity sake, you can also use this ridiculous date format which surprisingly works with AIR. A word of warning about this workaround is that, even though you will be able to read data, you may not be able to update data via SQLCommand parameters if your column types are DATETIME because AIR will complain about an invalid date (see errors below). You will have either have to write your own SQL statements without parameters or else change your column types to int or varchar.

Formatting a Julian date manually in SQL so you can read it:

Julian values are great and all that, but it’s pretty much impossible to eyeball them when you’re working at the command line. SQLite recognizes Julian formatting as a valid date, so you can use the STRFTIME function to format and output it any way you like. Below is a simple example that is easier to read:

SELECT STRFTIME('%Y-%m-%d %H:%M:%S',my_column) AS my_column_formatted FROM my_table

Common errors that occur while working with dates:

Invalid Date

You may see this in a DataGrid instead of the expected date value. This is because you have a DATETIME column in SQLite, however the value is not in Julian format. Even though SQLite may recognize it as a date value, AIR does not. The solution is to clean your data so that all dates are Julian format, or alternatively change the column type to VARCHAR.

‘Error #3115: SQL Error.’, details:’could not convert string value to date’

This error occurs when you try to update a record that has one or more DATETIME columns that do not have the date stored in Julian format. The weird part is that even if you are not touching that specific column in your insert/update statement – AIR will still validate the Date and throw this error. The solution is to clean your data so that all dates are Julian format, or alternatively change the column type to VARCHAR.

If you have any tips or corrections please post a comment and I’ll incorporate it into the article.

Ridiculous date formatting in SQLite

The following code outputs a SQLite date into a standard date format with month abbreviations that, believe it or not, Flex/AIR will accept as a Date value (Though SQLite itself doesn’t recognize it as a valid DATETIME!). I created this atrocious code while trying to figure out the mysterious interaction between AIR and SQLite date values. Julian time format is what you should use, though, for AIR applications. I decided to post this code up just for curiosity sake and future reference:

SELECT
   CASE WHEN (  strftime('%m',my_column)   = '01') THEN ('Jan') ELSE '' END
|| CASE WHEN (  strftime('%m',my_column)   = '02') THEN ('Feb') ELSE '' END
|| CASE WHEN (  strftime('%m',my_column)   = '03') THEN ('Mar') ELSE '' END
|| CASE WHEN (  strftime('%m',my_column)   = '04') THEN ('Apr') ELSE '' END
|| CASE WHEN (  strftime('%m',my_column)   = '05') THEN ('May') ELSE '' END
|| CASE WHEN (  strftime('%m',my_column)   = '06') THEN ('Jun') ELSE '' END
|| CASE WHEN (  strftime('%m',my_column)   = '07') THEN ('Jul') ELSE '' END
|| CASE WHEN (  strftime('%m',my_column)   = '08') THEN ('Aug') ELSE '' END
|| CASE WHEN (  strftime('%m',my_column)   = '09') THEN ('Sep') ELSE '' END
|| CASE WHEN (  strftime('%m',my_column)   = '10') THEN ('Oct') ELSE '' END
|| CASE WHEN (  strftime('%m',my_column)   = '11') THEN ('Nov') ELSE '' END
|| CASE WHEN (  strftime('%m',my_column)   = '12') THEN ('Dec') ELSE '' END
|| STRFTIME(' %d %H:%M:%S GMT-0600 %Y', my_column)
FROM my_table
Return top