Wednesday, July 27, 2011

Spark Data Grid Tab Focus Control

After spending so many months customizing the AdvancedDataGrid, I'm now interested to see if I can migrate many of those customizations to the Spark DataGrid. One of my first challenges is to see if I can improve my focus control - specifically tab navigation.

To review, here is the use case I'm trying to achieve (BTW it works in spark, and only partially works in ADG). My data is a product, and I have two occasionally editable fields related to this product: Quantity and Price (my role is a sales associate - so I can negotiate on prices). The product as a whole is only editable if it is in stock; I cannot change quantity or price if it is out of stock. Price is only editable if and handful of business rules apply (margin, vendor, etc). I want to be able to tab to the editable fields.

The problem with the ADG was that in it's "findNextEditableItemRenderer" loop, it only checked the data (ie, inStock). There was no way to tell what column it was looking at.

I was quite pleased with the spark DataGrid and its nice separation of concerns. I love that there is a separate DataGridEditor. And I was happy to discover that there was a simple PUBLIC method on DataGrid that I could tap into to do everything that I wanted (So far).

That public method is startItemEditorSession(rowIndex:int, columnIndex:int):Boolean. SWEET!

That problem is resolved in the spark dataGrid.
You can see the example here. In this example, the top grid is the regular one and the bottom grid is the modified one. I'm using the alpha property instead of enabled to prove that focus is indeed under my control (MUH HA HA HA - <evil laugh>).

The key is that within the startItemEditorSession, you
  • convert the rowindex to the data via the getItemAt on the dataprovider
  • convert the columnIndex to the column
  • validate the data as well as the data[column.datafield] properties
  • return the boolean if editing should happen.

    And here is the code

    public class FocusSparkDataGrid extends DataGrid 
     {
      private var _isDataEditableHelper:IIsDataEditable
      
      public function FocusSparkDataGrid()
      {
       super();
      }
      
    
      public function get isDataEditableHelper():IIsDataEditable
      {
       return _isDataEditableHelper;
      }
    
      public function set isDataEditableHelper(value:IIsDataEditable):void
      {
       _isDataEditableHelper = value;
      }
    
      override public function startItemEditorSession(rowIndex:int, columnIndex:int):Boolean
      {
       var dataIsEditable:Boolean = isDataEditable(rowIndex,columnIndex);
       if (dataIsEditable)
       {
        var editingStarted:Boolean = super.startItemEditorSession(rowIndex,columnIndex);
        return editingStarted
       }
       return false;
      }
    
      
      protected function isDataEditable(rowIndex:int, columnIndex:int):Boolean
      {
       if (isDataEditableHelper != null)
       {
        var dataItem:Object = dataProvider.getItemAt(rowIndex);
        var column:GridColumn = GridColumn(columns.getItemAt(columnIndex));
        var dataField:String = column.dataField;
        return isDataEditableHelper.isDataEditableForProperty(dataItem,dataField)
       }
       return false;
      }
    }
    
    

    public class IsProductVOEditable implements IIsDataEditable
     {
      public function IsProductVOEditable()
      {
      }
      
      public function isDataEditableForProperty(data:Object, property:String=""):Boolean
      {
       var vo:ProductVO = ProductVO(data);
       if (vo.inStock)
       {
        if (property == "price")
        {
         return vo.priceEditable;
        }
        return true;
       }
       return false;
      }
     }
  • Thursday, June 23, 2011

    Upgrading your Brain

    Here is the presentation that I've given based on the "Pragmatic Thinking & Learning" book by Andy Hunt.

    Upgrading your Brian.pptx

    Monday, June 20, 2011

    GroupingCollection2 not deleting an item while grouped

    So here was our issues, when the grid was grouped, an item wouldn't be visually deleted - although it did actually get deleted from the collection and on the server. Everything worked fine when the grid was not grouped.

    I discovered that the issue resides in the GroupingCollection2.getChildren() method. This method violates the SRP (Single Responsibility Principle) and not only does it return the children object, but also builds the parentMap.

    If you subclass GroupingCollection2, and redirect getChildren to your own method which just returns the children (as the name might suggest). The parentMap fails to get built.

    What this means, is the when you delete an item from the source collection, a parent for that item will not be found, and the grouped collection (one that is built internally ) doesn't respond to the delete and therefore the display doesn't need to update.

    The solution is in your subclass of GroupingCollection2 to call super.getChildren, even if you don't need use the return results.

    public override function getChildren(node:Object):Object // NO PMD
      {
       
       super.getChildren(node); 
       var rtn:Object  = helper.getChildren(node)
       
       /*
       Make sure this return value is not null. 
       A null value will cause the HierarchicalCollectionView to RTE on its internal refresh method
       */
       if (rtn == null)
       {
        rtn = new Object();
       }
       return rtn;
       
      }

    HierarchicalCollectionView updateLength discovery

    So, in another round of heavy debugging - I came across an an anomaly, which really is just a difference between what the developer thought would happen, and how I chose to implement a feature.

    In the HierarchicalCollectionView, on the collectionChange event the length of the collection gets updated for nearly all CollectionChangeKinds except for UPDATE. Now this makes sense, changing a property shouldn't result in a change of length (like adding or removing children).

    Unfortunately, the way that I a structured our application was that the items that needed to expand (ie have children) where flat, not hierarchical. So my solution was to toggle a property on the item to indicate that it had children. When I applied the same interface to the grouping collections (so that I could use the same framework for expanding and collapsing), I discovered that the length wasn't being recalculated and thus scrolling was throw off.

    Annoyingly, much of the calculate length features in HierarchicalCollectionView are private. So here is a solution that I put in place, but it isn't very pretty:

    class HierarchicalCollectionViewWithUpdateListeners extends HierarchicalCollectionView
    {
     
     private var myLength:int
     private const NO_LENGTH:int  = -1
     public function HierarchicalCollectionViewWithUpdateListeners(hierarchicalData:IHierarchicalData=null, argOpenNodes:Object=null)
     {
      super(hierarchicalData, argOpenNodes);
     }
     
     
      public override function collectionChangeHandler(event:CollectionEvent):void
      {
       super.collectionChangeHandler(event);
       myLength = NO_LENGTH;
       if (event.kind == CollectionEventKind.UPDATE)
       {
        myLength = calculateLength();
       }
       
      }
    
      
      public override function get length():int
      {
       var origLen:int = super.length;
       if (myLength != NO_LENGTH)
       {
        return myLength;
       }
       else
       {
        return origLen; 
       }
      }
    
    
    }
    

    HierarchicalData vs HierarchicalCollectionView as a dataprovider to the AdvancedDataGrid

    So I just discovered another issue with the ADG. When you switch dataProviders from a HierarchicalCollectionView to HierarchicalData, the collection doesn't get set properly.

    Here is a snippet from the set dataProvider method of the ADG.

    if (value is IHierarchicalData)
            {
                _rootModel = value as IHierarchicalData;
            }
            else if (value is IHierarchicalCollectionView)
            {
                // if the dataProvider is IHierarchicalCollectionView,
                // set it to super.dataProvider in commitProperties()
                _rootModel = IHierarchicalCollectionView(value).source;
                hierarchicalCollectionInstance = IHierarchicalCollectionView(value);
            }
            else
            {
                _rootModel = null;
                hierarchicalCollectionInstance = null;
                super.dataProvider = value;
            }

    Notice that if you pass in a IHierarchicalCollectionView a private flag called hierarchicalCollectionInstance gets set. It gets nullified on ALL other dataprovider types EXCEPT IHierarchicalData.

    This causes a IHierarchicalData following a IHierarchicalCollectionView to essentially not be saved, as the hierarchicalCollectionInstance gets referenced in commitProperties. The old IHierarchicalCollectionView will continue to be the datasource.

    The easiest resolution to this is to set the dataProvider to null, then set it to the new source

    Thursday, June 16, 2011

    Setting up Hudson on Windows Vista

    So, I ventured out and for the first time, set up Hudson on my own machine. I've used it many times on a client site, but this was a first time starting it from scratch. I discovered many "slap the head - I was stupid moments" while doing this and need to share.

    1) download the hudson.war file, and follow the instructions on installing it as a service under Windows. This means that you need to have java installed on your machine... but it works easily

    2) Under Manage Hudson -> Configure, be sure to tell Hudson where your JDK and ANT files are. Note: You can have it automatically install them (This was a critical step that I missed). I found that while it downloaded and tried to install both the JDK and ANT, that it never succeed, so I had to manually install them and point Hudson to the JAVA_HOME and ANT_HOME paths. Also note, that setting the PATH variables inside of windows didn't seem to have that much of an effect.

    3) For some reason I found that there is a timer on subversion... that if I committed a file, then immediately went to Hudson and started a build, that it wouldn't have the updated file. If I waited about 30 seconds, it would have the latest.

    4) If you are building a Flex library project, you will need to explicitly enumerate all of the classes that you want to include. Here is a snippet that will have ANT do that for you, including removing the .as and the basedir references:

    
      
      
      
      
      
      
       
        
        
       
      
      
      
      
       
    

    Thanks for all your help google!

    Friday, June 3, 2011

    Discovering Metrics about Parsley and creating its context

    Recently I had a performance tuning need to figure out how long Parsley was taking to instantiate and create its context. Thanks to Marty Pitt for the solution.

    First off within the main application have the following when you are defining your context


    <parsley:ContextBuilder complete="applicationContextBuilderComplete(event)">
       <debug:TimedObjectSupport />
       <parsley:ViewProcessor type="{ExtendedViewProcessor}" />
    </parsley:ContextBuilder>


    protected function applicationContextBuilderComplete(event:FlexContextEvent):void
       {
        trace ("Processing Parsley Context: stopTime > " + getTimer());
       }
    

    Where TimedObjectSupport.as is:

    import org.spicefactory.parsley.core.bootstrap.BootstrapConfig;
    import org.spicefactory.parsley.flex.tag.builder.BootstrapConfigProcessor;
    
    public class TimedObjectSupport implements BootstrapConfigProcessor
     {
      public function processConfig(config:BootstrapConfig):void
      {
       trace ("Processing Parsley Context: startTime > " + getTimer());
      }
     }
    

    and ExtendedViewProcessor.as is:

    import org.spicefactory.parsley.core.context.Context;
     import org.spicefactory.parsley.core.view.ViewConfiguration;
     import org.spicefactory.parsley.core.view.processor.DefaultViewProcessor;
     
     public class ExtendedViewProcessor extends DefaultViewProcessor
     {
      public override function init(config:ViewConfiguration, context:Context):void
      {
       trace("Parsley Wiring Start-> " +getTimer());
        super.init(config,context);
       trace("Parsley Wiring Stop-> " +getTimer());
      }
    
    
     }