Sunday, October 30, 2011

Flex4: Hide/Show AdvancedDatagrid Columns in Flex 4: a design pattern

In this post I will point out how a usually disliked programming technique (recursion) is going to help to write clean and manageable code. In my point of view, recursion is to prefer to iteration when we can estimate in advance the depth of the recursive calls, or we are reasonably sure about an upper bound. The following tutorial shows a general purpose drop-in routine that can be reused for any advancedDatagrid and provides the following benefits:
  1. Populate a menu based switcher with grid's headerText.
  2. Manage hiding/showing columns.
To better understand the code, let see the advancedDatagrid declaration in mxml:
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
<mx:AdvancedDataGrid id="grid"  selectionMode="singleRow" wordWrap="true" 
  headerWordWrap="true" 
  defaultLeafIcon="{null}" alternatingItemColors="[#ffc4c4, #FFFFFF]" 
  height="300" width="800" x="0" y="46">
  <mx:groupedColumns>
    <mx:AdvancedDataGridColumnGroup headerText="Equipo" >
      <mx:AdvancedDataGridColumn headerText="Descripcion" />
      <mx:AdvancedDataGridColumn headerText="Marca" />
      <mx:AdvancedDataGridColumn headerText="Modelo" />
      <mx:AdvancedDataGridColumn headerText="Serie" />
      <mx:AdvancedDataGridColumn headerText="Ubicacion" />
    </mx:AdvancedDataGridColumnGroup>
    <mx:AdvancedDataGridColumnGroup headerText="Empresa" >
      <mx:AdvancedDataGridColumn headerText="Nombre" />
    </mx:AdvancedDataGridColumnGroup>          
    <mx:AdvancedDataGridColumn headerText="Fecha" />
    <mx:AdvancedDataGridColumn headerText="Asignado"/>
    <mx:AdvancedDataGridColumn headerText="Terminado"/>
    <mx:AdvancedDataGridColumn headerText="Puntos"/>
    <mx:AdvancedDataGridColumn headerText="Monto"/>
    <mx:AdvancedDataGridColumn headerText="Factura"/>
    <mx:AdvancedDataGridColumn headerText="Monto Factura"/>
    <mx:AdvancedDataGridColumn headerText="Pagado"/>
  </mx:groupedColumns>
</mx:AdvancedDataGrid>


We are going to hide/show columns using headerText values instead of id's for the following reasons:
  1. It's easier to populate a menu based switcher with grid's headerTexts in actionscript than having to manually write the dataProvider content in mxml.
  2. We can write a general algorithm not requiring a support function acting as a dispatcher.
  3. We can reasonably assume that no two columns have the same headerText (Note that this is a mandatory requirement).
Let define a PopUpMenuButton and its dataProvider:
1
2
3
4
5
<fx:Declarations>
  <!-- Place non-visual elements (e.g., services, value objects) here -->
  <s:ArrayCollection id="arr"/>
</fx:Declarations>
<mx:PopUpMenuButton x="10" y="10" label="Hide/Show Columns" dataProvider="arr"/>
Basically, we want now to define a general purpose traversal function in actionscript which can be used for both menu population and event handling when hiding/showing columns. We will use some more or less advanced coding technique in OO programming leading to the implementation of a design patter (I'm sure it has a name but I don't known which one in this moment). First of all, we create a new Interface in as3 called "LeafHandler.as":
1
2
3
4
5
6
7
8
9
package
{
  import mx.controls.advancedDataGridClasses.AdvancedDataGridColumn;
 
  public interface LeafHandler
  {
    function handleLeaf(leaf:AdvancedDataGridColumn):void;
  }
}
The function handleLeaf() must implement the logic of both variants: menu population and hiding/showing support. So, we can write the first implementation "MenuLeafHandler.as":
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
28
29
package
{
  import mx.collections.ArrayCollection;
  import mx.controls.advancedDataGridClasses.AdvancedDataGridColumn;
 
  public class MenuLeafHandler implements LeafHandler
  {
 
    private var arr:ArrayCollection;
 
    public function MenuLeafHandler()
    {
      arr = new ArrayCollection();
    }
 
    public function handleLeaf(leaf:AdvancedDataGridColumn):void
    {
      var obj:Object = new Object();
      obj.label = leaf.headerText;
      obj.type = "check";
      obj.toggled = "true";
      arr.addItem(obj);
    }
 
    public function getArray():ArrayCollection {
      return arr;
    }
  }
}
For each leaf of the columns tree of the AdvancedDatagrid, we create a an Object containing the description of a CheckBox, and add it to the array. Next, we provide the implementation for columns hiding/showing logic in the file "HideShowHandler.as":
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
package
{
  import mx.controls.advancedDataGridClasses.AdvancedDataGridColumn;
 
  public class HideShowHandler implements LeafHandler
  {
    private var item:AdvancedDataGridColumn = null;
    private var headerToLookFor:String;    
 
 
    public function HideShowHandler(headerToLookFor:String)
    {
      this.headerToLookFor = headerToLookFor;
    }
 
    public function handleLeaf(leaf:AdvancedDataGridColumn):void
    {
      if (item != null) return;
      if (leaf.headerText == headerToLookFor) item = leaf;
    }
 
    public function getLeaf():AdvancedDataGridColumn {
      return item;
    }
  }
}
 In this case, we just want to save in memory a reference to the AdvancedDatagridColumn having the headerText value equal to the label of the CheckBox generating the click event. Note that the constructor of this class takes a String as a parameter, which must be set to the label value of the MenuItem generating the click event.
The last step is to define the actual traversing function. We will make use of the polymorphism nature of as3. The code for "Traversal.as" follows:
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
28
29
30
package
{
  import mx.controls.advancedDataGridClasses.AdvancedDataGridColumn;
  import mx.controls.advancedDataGridClasses.AdvancedDataGridColumnGroup;
 
  public class Traversal
  {
    private var handler:LeafHandler;
 
    public function Traversal(handler:LeafHandler)
    {
      this.handler = handler;
    }
 
    public function traverseAndGetHandler(clms:Array):LeafHandler {
      traverse(clms);
      return handler;
    }
 
    private function traverse(clms:Array):void {
      for (var i:int = 0; i < clms.length; i++) {
        if (clms[i] is AdvancedDataGridColumnGroup) {
          traverse(AdvancedDataGridColumnGroup(clms[i]).children);
        } else {
          handler.handleLeaf(AdvancedDataGridColumn(clms[i]));
        }
      }
    }
  }
}

This is the function using recursion as anticipated before. Note that we actually wrote really few lines of code. I have another example where we achieve the same result defining the same elements of the UI but hardwiring the values. The result is obscene and you actually lose a considerable amount of time on changing everytime all the Strings references for a particular implementation. Note also that if you are creating the grid structure runtime, and you want to provide a columns hide/show functionality, you have no choice but to use the depicted method.
We can finish this tutorial by providing the source code of our main application. I don't explain all the event handling as you should know about that, the final result is a clean, reusable and understandable code. You can see the final result following this link.
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" 
         xmlns:s="library://ns.adobe.com/flex/spark" 
         xmlns:mx="library://ns.adobe.com/flex/mx" minWidth="800" minHeight="400" 
         width="800" height="400"
         applicationComplete="init()">
  <fx:Declarations>
    <!-- Place non-visual elements (e.g., services, value objects) here -->
    <s:ArrayCollection id="arr"/>
  </fx:Declarations>
  <mx:AdvancedDataGrid id="grid"  selectionMode="singleRow" wordWrap="true" 
             headerWordWrap="true" 
             defaultLeafIcon="{null}" alternatingItemColors="[#ffc4c4, #FFFFFF]" 
             height="354" width="800" x="0" y="46" >
    <mx:groupedColumns>
      <mx:AdvancedDataGridColumnGroup headerText="Equipo" >
        <mx:AdvancedDataGridColumn headerText="Descripcion" />
        <mx:AdvancedDataGridColumn headerText="Marca" />
        <mx:AdvancedDataGridColumn headerText="Modelo" />
        <mx:AdvancedDataGridColumn headerText="Serie" />
        <mx:AdvancedDataGridColumn headerText="Ubicacion" />
      </mx:AdvancedDataGridColumnGroup>
      <mx:AdvancedDataGridColumnGroup headerText="Empresa" >
        <mx:AdvancedDataGridColumn headerText="Nombre" />
      </mx:AdvancedDataGridColumnGroup>          
      <mx:AdvancedDataGridColumn headerText="Fecha" />
      <mx:AdvancedDataGridColumn headerText="Asignado"/>
      <mx:AdvancedDataGridColumn headerText="Terminado"/>
      <mx:AdvancedDataGridColumn headerText="Puntos"/>
      <mx:AdvancedDataGridColumn headerText="Monto"/>
      <mx:AdvancedDataGridColumn headerText="Factura"/>
      <mx:AdvancedDataGridColumn headerText="Monto Factura"/>
      <mx:AdvancedDataGridColumn headerText="Pagado"/>
    </mx:groupedColumns>
  </mx:AdvancedDataGrid>
  <mx:PopUpMenuButton id="popupmenubutton1" x="10" y="10" label="Hide/Show Columns" 
                      dataProvider="{arr}" itemClick="menu_change(event)"/>
 
  <fx:Script>
    <![CDATA[
      import mx.events.MenuEvent;
 
      private function init():void {
        var lh:MenuLeafHandler = new MenuLeafHandler();
        var t:Traversal = new Traversal(lh);
        t.traverseAndGetHandler(grid.groupedColumns);
        arr = lh.getArray();
      }
 
      private function menu_change(evt:MenuEvent):void {
        var lh:HideShowHandler = new HideShowHandler(evt.item.label);
        var t:Traversal = new Traversal(lh);
        t.traverseAndGetHandler(grid.groupedColumns);
        lh.getLeaf().visible = popupmenubutton1.dataDescriptor.isToggled(evt.item);
      }
    ]]>
  </fx:Script>
 
</s:Application>

No comments :

Post a Comment