Friday, June 25, 2010

Using flash.utils.Proxy objects inside a DataGrid

Today, I've learned something about Adobe Flex. That's not surprising, since I'm using Flex for the first time. So this is just a quick "note-to-self" kind of post. I was trying to use objects of a subclass of flash.utils.Proxy inside a mx.controls.DataGrid component. Displaying them worked as expected, but I could only select the last row in the list. After spending about half an hour stepping through DataGrid and related code from the Flex framework, I finally got it to work. Maybe this post will spare someone the dubious pleasure of wading through heaps and heaps of frameworky do-nothing-and-do-lots-of-it code. It turns out, that DataGrid uses a unique ID to keep track of the objects it is supposed to display. This ID is usually provided by the runtime. There is even documentation about it. Of course I only found that afterwards. Finding out whether reading this would have solved my problem might have taken just as long... Anyway, here is some code to illustrate the problem and the solution. Apologies for the FooBar nonsense. I couldn't come up with a simple example.

Let's say we have a model object called "FooBar" with two properties, "spam" and "eggs".


package flex_proxy_example
{
    import mx.collections.ArrayCollection;
    
    public class FooBar
    {
        public var spam:String;
        public var eggs:int;


        public function FooBar(spam:String, eggs:int)
        {
            this.spam = spam;
            this.eggs = eggs;
        }
        
        public static function getAll():ArrayCollection
        {
            var result:ArrayCollection = new ArrayCollection();
            result.addItem(new FooBar("foo", 5));
            result.addItem(new FooBar("bar", 23));
            result.addItem(new FooBar("baz", 42));
            result.addItem(new FooBar("spam", 1337));
            result.addItem(new FooBar("egs", 4711));
            return result;
        }
    }
}


We want to display a collection of these in a DataGrid. For some reason we want to massage the value of the "eggs" property before displaying it. This can be done by using a collection of proxy objects as the dataProvider of our DataGrid, instead of the model objects themselves. Flex 3 provides a class to create proxy objects, sensibly called "Proxy". The proper way to use this class is to subclass it and implement (at least) the methods getProperty and callProperty. Here's how our FooBarProxy class might look like:


package flex_proxy_example
{
    import flash.utils.Proxy;
    import flash.utils.flash_proxy;
    
    public class FooBarProxy extends Proxy
    {
        private var foobar:FooBar;
        
        public function FooBarProxy(foobar:FooBar)
        {
            this.foobar = foobar;   
        }
        
        override flash_proxy function getProperty(name:*):*
        {
            if (name.toString() == "eggs")
                return foobar.eggs + 12345;
            return foobar[name];
        }       
        
        override flash_proxy function callProperty(methodName:*, ... args):*
        {
            return foobar[methodName].apply(foobar, args);
        }
    }
}


And here is a minimal Flex application, using the above classes:


<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" creationComplete="creationComplete()">
    <mx:Script>
        <![CDATA[
            import flex_proxy_example.FooBarProxy;
            import flex_proxy_example.FooBar;
            import mx.collections.ArrayCollection;
            
            private var bunchOfFoobars:ArrayCollection;
            
            public function creationComplete():void
            {
                bunchOfFoobars = new ArrayCollection();
                
                for each (var fb:FooBar in FooBar.getAll())
                {
                    bunchOfFoobars.addItem(new FooBarProxy(fb));    
                }
                dg.dataProvider = bunchOfFoobars;
            }
        ]]>
    </mx:Script>
    <mx:DataGrid id="dg">
      <mx:columns>
         <mx:DataGridColumn dataField="spam" />
         <mx:DataGridColumn dataField="eggs" />
      </mx:columns>
     </mx:DataGrid>
</mx:Application>


There is just one little problem. Selecting anything but the last element of the DataGrid won't work. This is because the proxy objects all produce the same string as their UID. As the above linked documentation suggests, we can add our own UID property to the FooBarProxy class and everything will be peachy. This is how the fixed code looks:


package flex_proxy_example
{
    import flash.utils.Proxy;
    import flash.utils.flash_proxy;
    
    import mx.core.IUID;
    import mx.utils.UIDUtil;
    
    public class FooBarProxy extends Proxy implements IUID
    {
        private var foobar:FooBar;
        private var _uid:String;
                
        public function FooBarProxy(foobar:FooBar)
        {
            this.foobar = foobar;   
            this._uid = UIDUtil.createUID(); 
        }
        
        public function get uid():String
        {
            return _uid;
        }
        
        public function set uid(u:String):void
        {
            // ignored
        }
        
        override flash_proxy function getProperty(name:*):*
        {
            if (name.toString() == "eggs")
                return foobar.eggs + 12345;
            return foobar[name];
        }       
        
        override flash_proxy function callProperty(methodName:*, ... args):*
        {
            return foobar[methodName].apply(foobar, args);
        }
    }
}


I was too lazy to upload the example app somewhere and embed it into this post. It's pointless anyway. The code is available at http://hg.haikoschol.com/flex_proxy_example/src, simply because I have so much free space on Bitbucket. ;)