Monday, February 11, 2013

Flex GridColumn, complex objects, and default sorting

I recently discovered something about the spark GridColumn. You can pass in a complex type for the dataField.

Say for example that your dataProvider is a collection of Users:

public class User
{
    public var name:String;

    public var id:String;

    public var manager:User;

}


You can specify the gridColumn.datafield as "manager.name"
<s:GridColumn dataField="manager.name" />

This is really cool. Can you can see it in the source code down where the datafield property is set. There is an internal dataFieldPath variable.

Here is the problem that I ran into. I had several columns that had these nested values and I wanted to set up each column to have case insensitive sorting, which is not the default. Using a shared sortFunction was difficult, because I also didn't have direct acces to the dataFieldPath (the nested property), and it would have too much work (in my opinion) to build the object within the sort and then grab the string and then lowercase and compare. It was especially too much work, because it would have negated any label functions that were already in place for these columns.

So here is my solution:


package com.workbook.core.view.datagrid
{
    import mx.formatters.IFormatter;

    import spark.collections.SortField;
    import spark.components.gridClasses.GridColumn;
    import spark.components.gridClasses.GridSortField;

    public class CaseInsensitiveComplexDataPathGridColumn extends GridColumn
    {

        public function CaseInsensitiveComplexDataPathGridColumn()
        {
            super()
        }



        override public function get sortField():SortField
        {
            var sf:SortField = super.sortField;
            const isComplexDataField:Boolean = sf is GridSortField;
            var cF:Function = null;
            if (isComplexDataField && sortCompareFunction == null)
            {
                sf.compareFunction = dataFieldPathSortCompareCaseInsensitive;
            }
            return sf;
        }

        protected function dataFieldPathSortCompareCaseInsensitive(obj1:Object,
                                                                 obj2:Object):int
       {
            if (!obj1 && !obj2)
                return 0;

            if (!obj1)
                return 1;

            if (!obj2)
                return -1;

            const obj1String:String = itemToLabel(obj1).toLowerCase();
            const obj2String:String = itemToLabel(obj2).toLowerCase();

            if ( obj1String < obj2String )
                return -1;

            if ( obj1String > obj2String )
                return 1;

            return 0;
        }


    }

}

What this does, is that when you click on the column header, it grabs the sortfield from the gridColumn (that was the easiest public property to get at). Fortunately if the dataField is complex, it builds a GridSortField object instead of a SortField.

I check for the GridSortField and if there is not already a sortCompareFunction, I assign it to my case insensitive sort function.

Then within the sort function, I call itemToLabel, which fortunately does all of the heavy lifting to determine the nested object's label, calling any labelFunctions as needed.

Then I force to lowercase, and done.

To use, I replace my GridColumn in my dataGrid definition, with CaseInsensitiveComplexDataPathGridColumn.

5 comments:

  1. This complex dataField handling was in the later version of the MX grids as well.

    Couldn't you use a sortCompareFunction that matches the internal 'dataFieldPathSortCompare' (line 137), but do toLowerCase on the string returned?

    ReplyDelete
  2. @Tink. dataFieldPathSortCompare() is private, as is the itemToString() method that it is calling to compare internally. I was against recreating multiple methods (DRY) to get a simple sort in place.

    Thanks for the insight, I didn't realize that this feature was available in early releases of Flex.

    ReplyDelete
  3. Maybe I'm missing it, but I don't see the need to discriminate on ComplexDataPath since itemToLabel() is doing the real work and doesn't care.

    I wonder about the advantages of overriding the sortField's getter to force its compare function versus setting the sortCompareFunction to dataFieldPathSortCompareCaseInsensitive in the constructor. Well I guess a consumer of this class my feel bushwacked if they set it then later set it to null expecting the default behavior to return. I still don't like the idea of forcing it every time the sortField is accessed.

    ReplyDelete
  4. @CrashCodes. I agree that itemToLabel is doing the real work, but overriding that would have left all of the data formatted to lowercase, which is not what I wanted. I wanted the representation of the data to remain as is, with it variety of mixed case, but I did want it to sort case insensitive.

    The problem that I encountered while trying to set the sortCompareFunction on the grid column, was that you get the whole data item. From there you have to recreate the nested object from the datafield string. Then have to check the label function. Then perform the comparison. I felt that this would have been a significant duplication of code from the GridColumn, as most of the needed methods were private.

    Setting the sortCompareFunction to null, would indeed return it to the default behavior, which in this case is case insensitive sorting. If you wanted to go fully back to the case insensitive sorting, you would switch the GridColumn definition in the columns array of the datagrid.

    I agree that discriminating on ComplexDataPath might not be necessary. Thanks for pointing that out.

    ReplyDelete
  5. A note about a ComplexDataPath in the dataField. If you are using dynamic objects and you may have a field name that is not legal to declare as a variable such as a number you cannot use the [] notation as you normally would. Instead use . notation, the dataField parser doesn't care that it is an illegal variable. Example:

    var o:Object = {snarkyComment:{}};
    var id:int = 1234;
    o.snarkyComment[id] = "maybe I shoulda used a Dictionary";

    ...
    column.dataField = "snarkyComments[" + id + "]"; // this does not work
    column.dataField = "snarkyComments." + id; // this works

    ReplyDelete