Data binding and ActionScript 3

Scris de in data de 24 January 2009
3 comentarii

Data binding is really easy to do in MXML. But what do you do if you need to bind some dynamic objects at runtime using ActionScript? Here’s how I solved this particular problem in the following localization example.

Assume you want to add localization to a MenuBar component and the data provider is an Object. Let’s assume, for the sake of this example, that the Object that will act as dataProvider for the MenuBar component looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private var menu:Array = [
    { label: "FILE", children: [
        { label: "NEW" },
        { label: "OPEN" },
        { label: "IMPORT", children: [
            { label: "PROJECT" },
            { label: "OTHER" } ] },
        { type: "separator" },
        { label: "EXIT" } ] },
    { label: "EDIT", children: [
        { label: "UNDO" },
        { label: "REDO" },
        { type: "separator" },
        { label: "CUT" },
        { label: "COPY" },
        { label: "PASTE" } ] },
    { label: "PROJECT", children: [
        { label: "BUILD" },
        { label: "CLEAN" },
        { type: "separator" },
        { label: "EXPORT" } ] },
    { label: "LANGUAGE", children: [
        { label: "ENGLISH", children: [
            { label: "AMERICAN" },
            { label: "BRITISH" } ] },
        { label: "ROMANIAN" } ] }
];


For this object, the labels are actually resource names in the resource bundle. When the application is initialized, the init() function kicks in (defined below) and the MenuBar‘s dataProvider is set to the return value of the translateItems() function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private function init():void {
    mainMenu.dataProvider = translateItems(menu);
}
 
private function translateItems(menuItems:Array):Array {
    for (var p:String in menuItems) {
        var menuItem:ObjectProxy = menuItems[p] = new ObjectProxy(menuItems[p]);
        menuItem.labelLocale = menuItem.label;
        (function(item:ObjectProxy):void {
            BindingUtils.bindSetter(function(value:Object):void {
                item.label = value as String;
            }, ResourceManager.getInstance(), {
                name: "getString",
                getter: function(resMan:IResourceManager):String {
                    return resMan.getString(BUNDLE_NAME, item.labelLocale);
                }
            });
        })(menuItem);
        if (menuItem.children) {
            menuItem.children = translateItems(menuItem.children);
        }
    }
    return menuItems;
}

Now let’s see what the magic behind the translateItems() function is.

7
        var menuItem:ObjectProxy = menuItems[p] = new ObjectProxy(menuItems[p]);

First of all, each Object in the menu Array must be converted to an ObjectProxy. This is necessary because for the localization to work (that is, for the menu labels to change), we need to be able to track changes made to each Object in the data provider. When a change is made to an object managed by a proxy, the dataProvider is notified and the label is updated.

8
        menuItem.labelLocale = menuItem.label;

After we set the ObjectProxy, we save the current label property for later use in the labelLocale property. We’ll need the labelLocale property to fetch the resource name when the locale changes.

9
10
11
12
13
14
15
16
17
18
        (function(item:ObjectProxy):void {
            BindingUtils.bindSetter(function(value:Object):void {
                item.label = value as String;
            }, ResourceManager.getInstance(), {
                name: "getString",
                getter: function(resMan:IResourceManager):String {
                    return resMan.getString(BUNDLE_NAME, item.labelLocale);
                }
            });
        })(menuItem);

The next part is a little tricky. This section of code is wrapped inside an anonymous function that is executed immediately, with the current menuItem passed as its parameter. This is important because we need to keep a local reference to this menuItem for use in the binding functions.
Next up, is the BindingUtils utility class which provides two static methods for performing data binding from ActionScript. Here, I used the bindSetter method, which has these parameters:

  • setter:Function — Setter method to invoke with an argument of the current value of chain when that value changes.
  • host:Object — The host of the property. See the bindProperty() method for more information.
  • chain:Object — The name of the property, or property chain. See the bindProperty() method for more information.
  • commitOnly:Boolean (default = false) — Set to true if the handler should be called only on committing change events. See the bindProperty() method for more information.

I won’t go into much detail about each of these parameters, as I think the docs do a pretty good job explaining what each of them does. You can find more information here. What is important in this brief example is the chain parameter, to which I have passed an Object in the form: { name: property name, getter: function(host) { return something; } }. This Object must contain the name of, and a getter function for, a public bindable property of the host object. In our case, the host is the ResourceManager instance and the bindable property is the getString() function.

19
20
21
        if (menuItem.children) {
            menuItem.children = translateItems(menuItem.children);
        }

The last bit is self explanatory – if the current menu item has any child submenus, we apply the translateItems() function recursively.

Enough talk! Now let’s see this code in action!
[kml_flashembed movie="http://blogu.lu/mrm/files/2009/01/bindingdemo.swf" width="400" height="180" /]
And the Flex Builder Project source code


3 Comments comment feed
Claudio

hey,
about your adobe air application, can you make it bigger?
thanks!

Matthew Hall

Take a look at my data binding framework on Google Code:

http://code.google.com/p/bindage-tools

BindageTools is an alternative to BindingUtils. It has a more intuitive API and comes with several features generally useful in data binding, like validation, explicit conversion, two-way bindings, delayed response (i.e. let the user stop typing before you start some expensive operation).

mrm

Matthew, your framework looks sweet! I’ll play around with it in the weekend and maybe write a blog post about it.