Jacks_Depression

Jacks_Flex-able DataGrid

Posted: 2009-10-30 07:29:41

When it comes to UI programming and business, nothing it more scared than the DataGrid. Back in the dark days when I used to use .Net, if you did not know everything about the DataGrid, you would be ostracized. And thus I ate many lunches alone. When I was learning the language, I never really needed to use a DataGrid. I never really needed to use one, and the consent seems so simple. Why would I waste my time on it when I could probably just figure it out as I go? So when the time came that I needed to use one in a Flex application, I approached it like it where nothing. I gave time estimates that sounded reasonable.

But then I got to thinking to my self. This is flex. Its pretty much the cream of the crop right now for visually appealing UI. Why not make the data more appealing? Why not go above and beyond? So, I decide to display percentages in bar graph form and boolean values as images. Not only that, but I was determined to put a button at the end of the column to remove it.

When I got into the code though, I realized it was not going to be as easy as I had hoped. It turns out that the only thing the DataGrid was built to display was text. However, I have challenged my self and I will be victorious. We will start by demonstrating the problem.


index.mxml

<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:local="*" applicationComplete="init()">
    <mx:Script>
    <![CDATA
       
        import mx.collections.ArrayCollection;
       
        public var gridData:ArrayCollection = new ArrayCollection();
       
        public function init():void
        {
            gridData.addItem(new SomeObject("Item 1", 75.0));
            gridData.addItem(new SomeObject("Item B", 60.0));
            gridData.addItem(new SomeObject("C-Item", 15.0));
           
            grid.dataProvider = gridData;
        }
       
    ]>

    </mx:Script>
    <mx:Panel label="DataGrid" percentWidth="100" percentHeight="100">
        <mx:DataGrid id="grid" percentWidth="100" percentHeight="100">
            <mx:columns>
                <mx:DataGridColumn dataField="name" headerText="SomeObject Name"/>
                <mx:DataGridColumn dataField="percent" headerText="Number Value"/>
                <local:BarColumn dataField="percent" headerText="Bar"/>
            </mx:columns>
        </mx:DataGrid>
    </mx:Panel>
</mx:Application>

SomeObject.as

package
{
    public class SomeObject extends Object
    {
        public var name:String;
        [Bindable]
        public var percent:Number;
       
        public function SomeObject(newName:String = "Blank", newPercent:Number = 50.0)
        {
            this.name = newName;
            this.percent = newPercent;
        }
    }
}

Here we have a basic DataGrid. The dataProvider is comprised of a simple object. Three of this object to be exact. And for starters, we have three columns. Two of them are the provided "DataGridColumn" class. And the 3rd is a custom column.

There are no problems with the first two columns. They work out of the box as advertised. Lets focus on the custom column.

BarColumn.as

package
{
    import mx.controls.dataGridClasses.DataGridColumn;

    public class BarColumn extends DataGridColumn
    {
        public function BarColumn()
        {
            super();
        }
    }
}

So the question is: how do we convert this number value into code execution? The column uses a factory pattern to create each cell on the grid. It is stored as "itemRenderer" as an IFactory type. A little more digging finds "ClassFactory" to be the only implantation of the IFactory interface. So, lets add this to our code.

BarColumn.as

package
{
    import mx.controls.dataGridClasses.DataGridColumn;
    import mx.core.ClassFactory;

    public class BarColumn extends DataGridColumn
    {
        public function BarColumn()
        {
            super();
           
            this.itemRenderer = new ClassFactory(BarRenderer);
        }
    }
}

BarRenderer.as

package
{
    import mx.controls.dataGridClasses.DataGridItemRenderer;
   
    public class BarRenderer extends DataGridItemRenderer
    {
        public function BarRenderer()
        {
            super();
        }
    }
}

Now we have the basics needed to really start making ground. But there is a bit of bad news. If you look at the inheritance of the "itemRenderer", you will see that its only contains text based functions. It has no "graphics" property, and you can't add children to it. So now I must wonder, why did Adobe go this path? Why are they making it so hard to add anything in the DataGrid but text? Not only that, but it is still uncertain where how to redraw when the number changes.

Good thing you came to my blog though, because I have done all the research for you. Just implement the "IdataRenderer" interface and your pretty much golden. I chose to simply make my "Render" a "Box", and it all works.

BarRenderer.as

package
{
    import mx.containers.Box;
    import mx.controls.dataGridClasses.DataGridItemRenderer;
    import mx.core.IDataRenderer;
    import mx.events.FlexEvent;
   
   
    public class BarRenderer extends Box implements IDataRenderer
    {
       
        [Bindable]
        protected var _data:Number = 0.0;
       
        public function BarRenderer()
        {
            super();
        }
       
        [Bindable("dataChange")]
        override public function get data():Object
        {
            return this._data as Object;
        }
       
        override public function set data(value:Object):void
        {
            var num:Number = value.percent;
           
            if(value != num)
            {
                this._data = num;
                dispatchEvent(new FlexEvent(FlexEvent.DATA_CHANGE));
            }
        }
    }
}

It is important to note that the object that is "set" is the whole "SomeObject" object. You'll have to tell it specifically what property to read or store. Now, lets turn it into a our bar render.

BarRenderer.as

package
{
    import mx.containers.Box;
    import mx.core.IDataRenderer;
    import mx.events.FlexEvent;
    import mx.events.ResizeEvent;
   
   
    public class BarRenderer extends Box implements IDataRenderer
    {
       
        [Bindable]
        protected var _data:Number = 0.0;
       
        public function BarRenderer()
        {
            super();
            this.addEventListener(mx.events.ResizeEvent.RESIZE, this.resize)
        }
       
        [Bindable("dataChange")]
        override public function get data():Object
        {
            return this._data as Object;
        }
       
        override public function set data(value:Object):void
        {
            var num:Number = value.percent;
           
            if(value != num)
            {
                this._data = num;
                dispatchEvent(new FlexEvent(FlexEvent.DATA_CHANGE));
            }
        }
       
        public function resize(e:ResizeEvent):void
        {
            this.drawBar(this._data as Number);
        }
       
        public function drawBar(percent:Number):void
        {
            var newPercent:Number = percent / 100.0;
            var offest:Number = 5;
            var horzArea:Number = this.width - (offest*2);
            var vertArea:Number = this.height - (offest*2);

            this.graphics.clear();
           
            this.graphics.beginFill(0x008866);
            this.graphics.drawRect(offest, offest, horzArea * newPercent, vertArea);
            this.graphics.endFill();
           
            this.graphics.lineStyle(2);
            this.graphics.drawRect(offest, offest, horzArea, vertArea);
        }
    }
}

There is your solution in a nut shell.