Groups Model"

From Documentation
 
(28 intermediate revisions by 8 users not shown)
Line 4: Line 4:
 
  {{ZK PE and EE}}
 
  {{ZK PE and EE}}
  
Here we describe how to implement a groups model (<javadoc type="interface">org.zkoss.zul.GroupsModel</javadoc>). For the concept about component, model and render, please refer to [[ZK_Developer's_Reference/MVC/Model/List_Model#Model-driven_Display|the Model-driven Display section]].
+
Here we describe how to implement a groups model (<javadoc type="interface">org.zkoss.zul.GroupsModel</javadoc>). For the concept of component, model and render, please refer to [[ZK_Developer's_Reference/MVC/Model/List_Model#Model-driven_Display|the Model-driven Display section]].
  
A groups model is used to drive components that support groups of data. The groups of data is a two-level tree of data: a list of grouped data and each grouped data is a list of elements to display. Here is [http://www.zkoss.org/zkdemo/grid/grouping a live demo]. Currently, <javadoc>org.zkoss.zul.Listbox</javadoc> and <javadoc>org.zkoss.zul.Grid</javadoc> both support a list of grouped data.
+
A groups model is used to drive components that support groups of data. The groups of data is a two-level tree of data: a list of grouped data and each grouped data is a list of elements to display. Here is [http://www.zkoss.org/zkdemo/grid/grouping a live demo]. Currently, both <javadoc>org.zkoss.zul.Listbox</javadoc> and <javadoc>org.zkoss.zul.Grid</javadoc> support a list of grouped data.
  
 
Instead of implementing <javadoc type="interface">org.zkoss.zul.GroupsModel</javadoc>, it is suggested to extend from <javadoc>org.zkoss.zul.AbstractGroupsModel</javadoc>, or to use one of the default implementations as following:  
 
Instead of implementing <javadoc type="interface">org.zkoss.zul.GroupsModel</javadoc>, it is suggested to extend from <javadoc>org.zkoss.zul.AbstractGroupsModel</javadoc>, or to use one of the default implementations as following:  
  
{| border='1px' | width="100%"
+
{| class='wikitable' | width="100%"
 
!  !! <javadoc>org.zkoss.zul.SimpleGroupsModel</javadoc> !! <javadoc>org.zkoss.zul.GroupsModelArray</javadoc>
 
!  !! <javadoc>org.zkoss.zul.SimpleGroupsModel</javadoc> !! <javadoc>org.zkoss.zul.GroupsModelArray</javadoc>
 
|-
 
|-
 
| Usage
 
| Usage
| The grouping is immutable, i.e., re-grouping is not allowed
+
| The grouping is '''immutable''', i.e., re-grouping is not allowed
 
| Grouping is based on a comparator (java.util.Comparator)
 
| Grouping is based on a comparator (java.util.Comparator)
 
|-
 
|-
Line 24: Line 24:
 
|
 
|
 
Since 3.5.0
 
Since 3.5.0
| Since 5.0.5; For 5.0.4 or prior, please use <javadoc>org.zkoss.zul.ArrayGroupsModel</javadoc> (the same).
+
| Since 5.0.5; For 5.0.4 or prior, please use [http://www.zkoss.org/javadoc/5.0.4/zk/org/zkoss/zul/ArrayGroupsModel.html org.zkoss.zul.ArrayGroupsModel] (the same).
 
|}
 
|}
  
Line 68: Line 68:
 
=Sorting and Regrouping=
 
=Sorting and Regrouping=
  
If your groups model allows the end user to sort and/or to re-group (i.e., grouping data based on different criteria), you have to implement <javadoc type="interface">org.zkoss.zul.GroupsModelExt</javadoc> too. Then, <javadoc method="group(java.util.Comparator, boolean, int)" type="interface">org.zkoss.zul.GroupsModelExt</javadoc> will be called if the user requests to sort the data based on particular column. And, <javadoc method="sort(java.util.Comparator, boolean, int)" type="interface">org.zkoss.zul.GroupsModelExt</javadoc> will be called if the user requests to re-group the data based on particular column.
+
If your groups model allows the end user to sort and/or to re-group (i.e., grouping data based on different criteria), you have to implement <javadoc type="interface">org.zkoss.zul.ext.GroupsSortableModel</javadoc> too. Then, <javadoc method="group(java.util.Comparator,boolean,int)" type="interface">org.zkoss.zul.ext.GroupsSortableModel</javadoc> will be called if the user requests to re-group the data based on a particular column. And, <javadoc method="sort(java.util.Comparator,boolean,int)" type="interface">org.zkoss.zul.ext.GroupsSortableModel</javadoc> will be called if the user requests to sort the data based on a particular column.
  
<javadoc>org.zkoss.zul.GroupsModelArray</javadoc> support both sorting and re-grouping as described below:
+
<javadoc>org.zkoss.zul.GroupsModelArray</javadoc> supports both sorting and re-grouping as described below:
  
 
* Sorting: <javadoc>org.zkoss.zul.GroupsModelArray</javadoc> sorts each group separately by using the specified comparator (java.util.Comparator).
 
* Sorting: <javadoc>org.zkoss.zul.GroupsModelArray</javadoc> sorts each group separately by using the specified comparator (java.util.Comparator).
Line 78: Line 78:
 
[[Image:Grouping_model_explain.png]]
 
[[Image:Grouping_model_explain.png]]
  
=Example: Grouping Tabular Data=
+
=Grouping 2-dimensional Array=
 
Suppose you have the data in a two-dimensional array (see below), and you want to allow the user to group them based on a field selected by the user (such as food's name or food's calories).  
 
Suppose you have the data in a two-dimensional array (see below), and you want to allow the user to group them based on a field selected by the user (such as food's name or food's calories).  
  
Line 115: Line 115:
 
</source>
 
</source>
  
Then, we can make it a groups model by extending from <javadoc>org.zkoss.zul.GroupsModelArray</javadoc>:
+
Then, we can turn it into a GroupsModel by extending from <javadoc>org.zkoss.zul.GroupsModelArray</javadoc>:
  
 
<source lang="java">
 
<source lang="java">
Line 126: Line 126:
 
     }
 
     }
 
     protected Object createGroupHead(Object[] groupdata, int index, int col) {
 
     protected Object createGroupHead(Object[] groupdata, int index, int col) {
         return groupdata[0][col];
+
         return ((Object[])groupdata[0])[col];
 
         //groupdata is one of groups after GroupsModelArray groups _foods
 
         //groupdata is one of groups after GroupsModelArray groups _foods
         ///here we pick the first element and use the col-th column as the group head
+
         //here we pick the first element and use the col-th column as the group head
 
     }
 
     }
 
     private static Object[][] _foods = new Object[][] {
 
     private static Object[][] _foods = new Object[][] {
         ...tabular data as shown above
+
         //...tabular data as shown above
 
     };
 
     };
 
};
 
};
 
</source>
 
</source>
  
In additions, we have to implement a comparator to group the data based on the given column as follows.
+
In addition, we have to implement a comparator to group the data based on the given column as follows.
  
 
<source lang="java">
 
<source lang="java">
Line 143: Line 143:
 
int _col;
 
int _col;
 
boolean _asc;
 
boolean _asc;
public FoodComparator(int col, boolean asc) {
+
public FoodComparator(long col, boolean asc) {
    _col = col; //which column to compare
+
            _col = (int) col; //which column to compare
 
    _asc = asc; //ascending or descending
 
    _asc = asc; //ascending or descending
 
}
 
}
Line 150: Line 150:
 
             Object[] data = (Object[]) o1;
 
             Object[] data = (Object[]) o1;
 
             Object[] data2 = (Object[]) o2;
 
             Object[] data2 = (Object[]) o2;
             int v = data[_col].compareTo(data2[_col]);
+
             int v = ((Comparable)data[_col]).compareTo(data2[_col]);
 
return _asc ? v: -v;
 
return _asc ? v: -v;
 
}
 
}
Line 156: Line 156:
 
</source>
 
</source>
  
Since the data will be displayed in a multiple column, we have to implement a renderer. Here is an example.
+
Since the data will be displayed in multiple columns, we have to implement a renderer. Here is an example.
  
 
<source lang="java">
 
<source lang="java">
 
public class FoodGroupRenderer implements RowRenderer {
 
public class FoodGroupRenderer implements RowRenderer {
     public void render(Row row, java.lang.Object obj) {
+
     public void render(Row row, java.lang.Object obj, int index) {
 
         if (row instanceof Group) {
 
         if (row instanceof Group) {
 
             //display the group head
 
             //display the group head
             row.appendChild(new Label(data.toString()));
+
             row.appendChild(new Label(obj.toString()));
 
         } else {
 
         } else {
 
             //display an element
 
             //display an element
Line 177: Line 177:
 
</source>
 
</source>
  
Finally we could them together in a ZUML document as follows.
+
Finally we could group them together in a ZUML document as follows.
  
 
<source lang="xml">
 
<source lang="xml">
Line 187: Line 187:
 
         <column label="Category"
 
         <column label="Category"
 
         sortAscending="${c:new2('foo.FoodComparator', 0, true)}"
 
         sortAscending="${c:new2('foo.FoodComparator', 0, true)}"
         sortDescending="${c:new2('foo.FoodComparator', 0, false)}"/>
+
         sortDescending="${c:new2('foo.FoodComparator', 0, false)}"
 +
        sortDirection="ascending"/> <!-- since it is initialized as sorted -->
 
         <column label="Name"
 
         <column label="Name"
 
         sortAscending="${c:new2('foo.FoodComparator', 1, true)}"
 
         sortAscending="${c:new2('foo.FoodComparator', 1, true)}"
Line 213: Line 214:
 
Second, <javadoc>org.zkoss.zul.ArrayComparator</javadoc> was introduced, so <code>foo.FoodComparator</code> is not required in the above example.
 
Second, <javadoc>org.zkoss.zul.ArrayComparator</javadoc> was introduced, so <code>foo.FoodComparator</code> is not required in the above example.
  
Third, <javadoc method="setSort(java.lang.String)">org.zkoss.zul.Column</javadoc> supports <tt>auto(0)</tt>, <tt>auto(1)</tt>, etc.
+
Third, <javadoc method="setSort(java.lang.String)">org.zkoss.zul.Column</javadoc> supports <code>auto(0)</code>, <code>auto(1)</code>, etc.
  
 
Thus, we can simplify the above example as follows.
 
Thus, we can simplify the above example as follows.
Line 219: Line 220:
 
<source lang="xml">
 
<source lang="xml">
 
<grid apply="foo.FoodComposer">
 
<grid apply="foo.FoodComposer">
  <!-- Initially, we group data on 1st column in ascending order -->
 
 
     <columns menupopup="auto"> <!-- turn on column's menupopup -->
 
     <columns menupopup="auto"> <!-- turn on column's menupopup -->
         <column label="Category" sort="auto(0)"/>
+
         <column label="Category" sort="auto(0)"
 +
        sortDirection="ascending"/> <!-- since it is initialized as sorted -->
 
         <column label="Name" sort="auto(1)"/>
 
         <column label="Name" sort="auto(1)"/>
 
         <column label="Top Nutrients" sort="auto(2)"/>
 
         <column label="Top Nutrients" sort="auto(2)"/>
Line 241: Line 242:
 
         Grid grid = (Grid)comp;
 
         Grid grid = (Grid)comp;
 
         grid.setModel(new GroupsModelArray(_foods, new ArrayComparator(0, true)));
 
         grid.setModel(new GroupsModelArray(_foods, new ArrayComparator(0, true)));
 +
            //Initially, we group data on 1st column in ascending order
 
         grid.setRowRenderer(new FoodGroupRenderer());
 
         grid.setRowRenderer(new FoodGroupRenderer());
 
     }
 
     }
Line 247: Line 249:
  
 
=Example: Grouping Array of JavaBean =
 
=Example: Grouping Array of JavaBean =
 +
 +
Suppose you have a collection of JavaBean objects (i.e., with the proper getter methods) as follows.
 +
 +
<source lang="java">
 +
public class Food {
 +
    String _category,  _name, _nutrients;
 +
    int _percentageOfDaily, _calories;
 +
 +
    public Food(String cat, String nm, String nutr, int pod, int cal) {
 +
        _category = cat;
 +
        _name = nm;
 +
        _nutrients = nutr;
 +
        _percentageOfDaily = pod;
 +
        _calories = cal;
 +
    }
 +
    public String getCategory() {
 +
        return _category;
 +
    }
 +
    public String getName() {
 +
        return _name;
 +
    }
 +
    public String getNutrients() {
 +
        return _nutrients;
 +
    }
 +
    public int getPercentageOfDaily() {
 +
        return _percentageOfDaily;
 +
    }
 +
    public int getCalories() {
 +
        return _calories;
 +
    }
 +
}
 +
</source>
 +
 +
Assume you want to use the value of the field that the user uses to group the data, then you could override <javadoc>org.zkoss.zul.GroupsModelArray</javadoc> as follows.
 +
 +
<source lang="java">
 +
public class FoodGroupsModel extends GroupsModelArray {
 +
    public FoodGroupsModel(Food[] foods) {
 +
        super(foods, new FieldComparator("category", true));
 +
    }
 +
    protected Object createGroupHead(Object[] groupdata,int index,int col) {
 +
    return new Object[] {groupdata[0], new Integer(col)};
 +
    }
 +
};
 +
</source>
 +
 +
where
 +
* We use <javadoc>org.zkoss.zul.FieldComparator</javadoc> to initialize the groups at the <code>category</code> field.
 +
* We use an object array as the group head that carries the first element of the given group (<code>Food[]</code>), and the index of the column that causes the grouping. We will use the index later to retrieve the field's value
 +
 +
We also need a custom renderer:
 +
 +
<source lang="java">
 +
package foo;
 +
import org.zkoss.lang.reflect.Fields;
 +
import org.zkoss.zk.ui.*;
 +
import org.zkoss.zul.*;
 +
public class FoodGroupRenderer implements RowRenderer {
 +
    public void render(Row row, java.lang.Object obj, int index) {
 +
        if (row instanceof Group) {
 +
            Object[] data = (Object[])obj; //prepared by createGroupHead()
 +
            row.appendChild(new Label(getGroupHead(row, (Food)data[0], (Integer)data[1])));
 +
        } else {
 +
            Food food = (Food) obj;
 +
            row.appendChild(new Label(food.getCategory()));
 +
            row.appendChild(new Label(food.getName()));
 +
            row.appendChild(new Label(food.getNutrients()));
 +
            row.appendChild(new Label(food.getPercentageOfDaily() + ""));
 +
            row.appendChild(new Label(food.getCalories() + ""));
 +
        }
 +
    }
 +
    private String getGroupHead(Row row, Food food, int index) {
 +
        Column column = (Column)row.getGrid().getColumns().getChildren().get(index);
 +
        String orderBy = ((FieldComparator)column.getSortAscending()).getOrderBy();
 +
        int j = orderBy.indexOf("name="),
 +
            k = orderBy.indexOf(' ');
 +
        try {
 +
            return Fields.get(food, orderBy.substring(j+1, k>0 ? k: orderBy.length())).toString();
 +
        } catch (NoSuchMethodException ex) {
 +
            throw UiException.Aide.wrap(ex);
 +
        }
 +
    }
 +
};
 +
</source>
 +
 +
The retrieval of the field's value is a bit tricky: since we will use <code>auto(fieldName)</code> to group and sort data for a given column (see the ZUML content listed below), we could retrieve the field's name by use of <javadoc method="getOrderBy()">org.zkoss.zul.FieldComparator</javadoc>, which returns something like "name=category ASC". Then, use <javadoc method="get(java.lang.Object, java.lang.String)">org.zkoss.lang.reflect.Fields</javadoc> to retrieve it. If the field name is in a compound format, such as <code>something.yet.another</code>, you could use <javadoc method="getByCompound(java.lang.Object, java.lang.String)">org.zkoss.lang.reflect.Fields</javadoc>
 +
 +
<blockquote>
 +
----
 +
For 5.0.6 or later, you could use <javadoc method="getRawOrderBy()">org.zkoss.zul.FieldComparator</javadoc> instead, which returns the field name you passed to <javadoc method="setSort(java.lang.String)">org.zkoss.zul.Column</javadoc>, i.e., "category".
 +
<source lang="java">
 +
        Column column = (Column)row.getGrid().getColumns().getChildren().get(index);
 +
        String field = ((FieldComparator)column.getSortAscending()).getRawOrderBy();
 +
        return Fields.get(food, field).toString();
 +
</source>
 +
</blockquote>
 +
 +
Then, you could have the ZUML document as follows.
 +
 +
<source lang="xml">
 +
<grid apply="foo.FoodComposer">
 +
    <columns menupopup="auto">
 +
        <column label="Category" sort="auto(category)" sortDirection="ascending"/>
 +
        <column label="Name" sort="auto(name)"/>
 +
        <column label="Top Nutrients" sort="auto(nutrients)"/>
 +
        <column label="% of Daily" sort="auto(percentageOfDaily)"/>
 +
        <column label="Calories" sort="auto(calories)"/>
 +
    </columns>
 +
</grid>
 +
</source>
 +
 +
And, the composer is as follows.
 +
 +
<source lang="java">
 +
package foo;
 +
import org.zkoss.zk.ui.Component;
 +
import org.zkoss.zk.ui.util.Composer;
 +
import org.zkoss.zul.*;
 +
public class FoodComposer implements Composer {
 +
    Food[] _foods = new Food[] { //assume we have a collection of foods
 +
        new Food("Vegetables", "Asparagus", "Vitamin K", 115, 43),
 +
        new Food("Vegetables", "Beets", "Folate", 33, 74)
 +
        //...more
 +
    };
 +
 +
    public void doAfterCompose(Component comp) throws Exception {
 +
        Grid grid = (Grid)comp;
 +
        grid.setModel(new FoodGroupsModel(_foods));
 +
            //Initially, we group data on 1st column in ascending order
 +
        grid.setRowRenderer(new FoodGroupRenderer());
 +
    }
 +
}
 +
</source>
  
 
=Group Foot =
 
=Group Foot =
  
 
If the groups model supports a foot (such as a summary of all data in the same group), you could return an object to represent the footer when <javadoc method="getGroupfoot(int)" type="interface">org.zkoss.zul.GroupsModel</javadoc> is called (similar to <javadoc method="getGroup(int)" type="interface">org.zkoss.zul.GroupsModel</javadoc> shall return an object representing the group).
 
If the groups model supports a foot (such as a summary of all data in the same group), you could return an object to represent the footer when <javadoc method="getGroupfoot(int)" type="interface">org.zkoss.zul.GroupsModel</javadoc> is called (similar to <javadoc method="getGroup(int)" type="interface">org.zkoss.zul.GroupsModel</javadoc> shall return an object representing the group).
 +
 +
If you use <javadoc>org.zkoss.zul.GroupsModelArray</javadoc>, you could override <javadoc method="createGroupFoot(java.lang.Object[], int, int)">org.zkoss.zul.GroupsModelArray</javadoc>. For example,
 +
 +
<source lang="java">
 +
public class FoodGroupsModel extends GroupsModelArray {
 +
    protected Object createGroupFoot(Object[] groupdata, int index, int col) {
 +
        return "Total " + groupdata.length + " items";
 +
    }
 +
...
 +
</source>
  
 
=Version History=
 
=Version History=
{{LastUpdated}}
+
 
{| border='1px' | width="100%"
+
{| class='wikitable' | width="100%"
 
! Version !! Date !! Content
 
! Version !! Date !! Content
 
|-
 
|-
 
| 5.0.6
 
| 5.0.6
 
| December 2010
 
| December 2010
| Enhanced the support of tabular data as described in [[#5.0.6 and later]].
+
| Enhanced the support of tabular data as described in [[#5.0.6 and Later]].
 
|}
 
|}
  
 
{{ZKDevelopersReferencePageFooter}}
 
{{ZKDevelopersReferencePageFooter}}

Latest revision as of 04:36, 5 June 2024

  • Available for ZK:
  • http://www.zkoss.org/product/zkhttp://www.zkoss.org/whyzk/zkeeVersion pe-ee.png

Here we describe how to implement a groups model (GroupsModel). For the concept of component, model and render, please refer to the Model-driven Display section.

A groups model is used to drive components that support groups of data. The groups of data is a two-level tree of data: a list of grouped data and each grouped data is a list of elements to display. Here is a live demo. Currently, both Listbox and Grid support a list of grouped data.

Instead of implementing GroupsModel, it is suggested to extend from AbstractGroupsModel, or to use one of the default implementations as following:

SimpleGroupsModel GroupsModelArray
Usage The grouping is immutable, i.e., re-grouping is not allowed Grouping is based on a comparator (java.util.Comparator)
Constructor The data must be grouped, i.e., data[0] is the first group, data[1] the second, etc. The data is not grouped, i.e., data[0] is the first element. The constructor requires a comparator that will be used to group them.
Version

Since 3.5.0

Since 5.0.5; For 5.0.4 or prior, please use org.zkoss.zul.ArrayGroupsModel (the same).

Example: Immutable Grouping Data

If your data is already grouped and the grouping won't be changed, then you could use SimpleGroupsModel as follows:

<zk>
    <zscript>
        String[][] datas = new String[][] {
            new String[] { //group 1
                // Today
                "RE: Bandbox Autocomplete Problem",
                "RE: It's not possible to navigate a listbox' ite",
                "RE: FileUpload"
            },
            new String[] { //group 2
                // Yesterday
                "RE: Opening more than one new browser window",
                "RE: SelectedItemConverter Question"
            },
            new String[] { //group 3
                "RE: Times_Series Chart help",
                "RE: SelectedItemConverter Question"
            }            
        };
        GroupsModel model = new SimpleGroupsModel(datas,
            new String[]{"Date: Today", "Date: Yesterday", "Date: Last Week"});
            //the 2nd argument is a list of group head
    </zscript>
    <grid model="${model}">
        <columns sizable="true">
            <column label="Subject"/>
        </columns>
    </grid>
</zk>

Then, the result

DrGroupsModel.png

Sorting and Regrouping

If your groups model allows the end user to sort and/or to re-group (i.e., grouping data based on different criteria), you have to implement GroupsSortableModel too. Then, GroupsSortableModel.group(Comparator, boolean, int) will be called if the user requests to re-group the data based on a particular column. And, GroupsSortableModel.sort(Comparator, boolean, int) will be called if the user requests to sort the data based on a particular column.

GroupsModelArray supports both sorting and re-grouping as described below:

Grouping model explain.png

Grouping 2-dimensional Array

Suppose you have the data in a two-dimensional array (see below), and you want to allow the user to group them based on a field selected by the user (such as food's name or food's calories).

DrGroupsModelArray.png

//Data
Object[][] _foods = new Object[][] { //Note: the order does not matter
    new Object[] { "Vegetables", "Asparagus", "Vitamin K", 115, 43},
    new Object[] { "Vegetables", "Beets", "Folate", 33, 74},
    new Object[] { "Vegetables", "Bell peppers", "Vitamin C", 291, 24},
    new Object[] { "Vegetables", "Cauliflower", "Vitamin C", 92, 28},
    new Object[] { "Vegetables", "Eggplant", "Dietary Fiber", 10, 27},
    new Object[] { "Vegetables", "Onions", "Chromium", 21, 60},
    new Object[] { "Vegetables", "Potatoes", "Vitamin C", 26, 132},
    new Object[] { "Vegetables", "Spinach", "Vitamin K", 1110, 41},
    new Object[] { "Vegetables", "Tomatoes", "Vitamin C", 57, 37},
    new Object[] { "Seafood", "Salmon", "Tryptophan", 103, 261},
    new Object[] { "Seafood", "Shrimp", "Tryptophan", 103, 112},
    new Object[] { "Seafood", "Scallops", "Tryptophan", 81, 151},
    new Object[] { "Seafood", "Cod", "Tryptophan", 90, 119},
    new Object[] { "Fruits", "Apples", "Manganese", 33, 61},
    new Object[] { "Fruits", "Cantaloupe", "Vitamin C", 112, 56},
    new Object[] { "Fruits", "Grapes", "Manganese", 33, 61},
    new Object[] { "Fruits", "Pineapple", "Manganese", 128, 75},
    new Object[] { "Fruits", "Strawberries", "Vitamin C", 24, 48},
    new Object[] { "Fruits", "Watermelon", "Vitamin C", 24, 48},
    new Object[] { "Poultry & Lean Meats", "Beef, lean organic", "Tryptophan", 112, 240},
    new Object[] { "Poultry & Lean Meats", "Lamb", "Tryptophan", 109, 229},
    new Object[] { "Poultry & Lean Meats", "Chicken", "Tryptophan", 121, 223},
    new Object[] { "Poultry & Lean Meats", "Venison ", "Protein", 69, 179},
    new Object[] { "Grains", "Corn ", "Vatamin B1", 24, 177},
    new Object[] { "Grains", "Oats ", "Manganese", 69, 147},
    new Object[] { "Grains", "Barley ", "Dietary Fiber", 54, 270}
};

Then, we can turn it into a GroupsModel by extending from GroupsModelArray:

//GroupsModel
package foo;
public class FoodGroupsModel extends GroupsModelArray {
    public FoodGroupsModel(java.util.Comparator cmpr) {
        super(_foods, cmpr); //assume we
        //cmpr is used to group 
    }
    protected Object createGroupHead(Object[] groupdata, int index, int col) {
        return ((Object[])groupdata[0])[col];
        //groupdata is one of groups after GroupsModelArray groups _foods
        //here we pick the first element and use the col-th column as the group head
    }
    private static Object[][] _foods = new Object[][] {
        //...tabular data as shown above
    };
};

In addition, we have to implement a comparator to group the data based on the given column as follows.

package foo;
public class FoodComparator implements java.util.Comparator {
	int _col;
	boolean _asc;
	 public FoodComparator(long col, boolean asc) {
            _col = (int) col; //which column to compare
	    _asc = asc; //ascending or descending
	}
	public int compare(Object o1, Object o2) {
            Object[] data = (Object[]) o1;
            Object[] data2 = (Object[]) o2;
            int v = ((Comparable)data[_col]).compareTo(data2[_col]);
		return _asc ? v: -v;
	}
}

Since the data will be displayed in multiple columns, we have to implement a renderer. Here is an example.

public class FoodGroupRenderer implements RowRenderer {
    public void render(Row row, java.lang.Object obj, int index) {
        if (row instanceof Group) {
            //display the group head
            row.appendChild(new Label(obj.toString()));
        } else {
            //display an element
            Object[] data = (Object[]) obj;
            row.appendChild(new Label(data[0].toString()));
            row.appendChild(new Label(data[1].toString()));
            row.appendChild(new Label(data[2].toString()));
            row.appendChild(new Label(data[3].toString()));
            row.appendChild(new Label(data[4].toString()));
        }
    }
};

Finally we could group them together in a ZUML document as follows.

<?taglib uri="http://www.zkoss.org/dsp/web/core" prefix="c" ?>
<grid rowRenderer="${c:new('foo.FoodGroupRenderer')}"
   model="${c:new1('foo.FoodGroupsModel', c:new2('foo.FoodComparator', 0, true))}">
   <!-- Initially, we group data on 1st column in ascending order -->
    <columns menupopup="auto"> <!-- turn on column's menupopup -->
        <column label="Category"
         sortAscending="${c:new2('foo.FoodComparator', 0, true)}"
         sortDescending="${c:new2('foo.FoodComparator', 0, false)}"
         sortDirection="ascending"/> <!-- since it is initialized as sorted -->
        <column label="Name"
         sortAscending="${c:new2('foo.FoodComparator', 1, true)}"
         sortDescending="${c:new2('foo.FoodComparator', 1, false)}"/>
        <column label="Top Nutrients"
         sortAscending="${c:new2('foo.FoodComparator', 2, true)}"
         sortDescending="${c:new2('foo.FoodComparator', 2, false)}"/>
        <column label="% of Daily"
         sortAscending="${c:new2('foo.FoodComparator', 3, true)}"
         sortDescending="${c:new2('foo.FoodComparator', 3, false)}"/>
        <column label="Calories"
         sortAscending="${c:new2('foo.FoodComparator', 4, true)}"
         sortDescending="${c:new2('foo.FoodComparator', 4, false)}"/>
    </columns>
</grid>

If it is not the behavior you want, you could override GroupsModelArray.sortGroupData(Object, Object[], Comparator, boolean, int). Of course, you could extend from AbstractGroupsModel to have total control.

5.0.6 and Later

Since 5.0.6, it is much easier to handle tabular data:

First, GroupsModelArray.createGroupHead(Object[], int, int) will return the correct element, so you don't have to override it as shown above.

Second, ArrayComparator was introduced, so foo.FoodComparator is not required in the above example.

Third, Column.setSort(String) supports auto(0), auto(1), etc.

Thus, we can simplify the above example as follows.

<grid apply="foo.FoodComposer">
    <columns menupopup="auto"> <!-- turn on column's menupopup -->
        <column label="Category" sort="auto(0)"
         sortDirection="ascending"/> <!-- since it is initialized as sorted -->
        <column label="Name" sort="auto(1)"/>
        <column label="Top Nutrients" sort="auto(2)"/>
        <column label="% of Daily" sort="auto(3)"/>
        <column label="Calories" sort="auto(4)"/>
    </columns>
</grid>

And, the composer is as follows.

package foo;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.util.Composer;
import org.zkoss.zul.*;
public class FoodComposer implements Composer {
    public void doAfterCompose(Component comp) throws Exception {
        Grid grid = (Grid)comp;
        grid.setModel(new GroupsModelArray(_foods, new ArrayComparator(0, true)));
             //Initially, we group data on 1st column in ascending order
        grid.setRowRenderer(new FoodGroupRenderer());
    }
}

Example: Grouping Array of JavaBean

Suppose you have a collection of JavaBean objects (i.e., with the proper getter methods) as follows.

public class Food {
    String _category,  _name, _nutrients;
    int _percentageOfDaily, _calories;

    public Food(String cat, String nm, String nutr, int pod, int cal) {
        _category = cat;
        _name = nm;
        _nutrients = nutr;
        _percentageOfDaily = pod;
        _calories = cal;
    }
    public String getCategory() {
        return _category;
    }
    public String getName() {
        return _name;
    }
    public String getNutrients() {
        return _nutrients;
    }
    public int getPercentageOfDaily() {
        return _percentageOfDaily;
    }
    public int getCalories() {
        return _calories;
    }
}

Assume you want to use the value of the field that the user uses to group the data, then you could override GroupsModelArray as follows.

public class FoodGroupsModel extends GroupsModelArray {
    public FoodGroupsModel(Food[] foods) {
        super(foods, new FieldComparator("category", true));
    }
    protected Object createGroupHead(Object[] groupdata,int index,int col) {
    	return new Object[] {groupdata[0], new Integer(col)};
    }
};

where

  • We use FieldComparator to initialize the groups at the category field.
  • We use an object array as the group head that carries the first element of the given group (Food[]), and the index of the column that causes the grouping. We will use the index later to retrieve the field's value

We also need a custom renderer:

package foo;
import org.zkoss.lang.reflect.Fields;
import org.zkoss.zk.ui.*;
import org.zkoss.zul.*;
public class FoodGroupRenderer implements RowRenderer {
    public void render(Row row, java.lang.Object obj, int index) {
        if (row instanceof Group) {
            Object[] data = (Object[])obj; //prepared by createGroupHead()
            row.appendChild(new Label(getGroupHead(row, (Food)data[0], (Integer)data[1])));
        } else {
            Food food = (Food) obj;
            row.appendChild(new Label(food.getCategory()));
            row.appendChild(new Label(food.getName()));
            row.appendChild(new Label(food.getNutrients()));
            row.appendChild(new Label(food.getPercentageOfDaily() + ""));
            row.appendChild(new Label(food.getCalories() + ""));
        }
    }
    private String getGroupHead(Row row, Food food, int index) {
        Column column = (Column)row.getGrid().getColumns().getChildren().get(index);
        String orderBy = ((FieldComparator)column.getSortAscending()).getOrderBy();
        int j = orderBy.indexOf("name="),
            k = orderBy.indexOf(' ');
        try {
            return Fields.get(food, orderBy.substring(j+1, k>0 ? k: orderBy.length())).toString();
        } catch (NoSuchMethodException ex) {
            throw UiException.Aide.wrap(ex);
        }
    }
};

The retrieval of the field's value is a bit tricky: since we will use auto(fieldName) to group and sort data for a given column (see the ZUML content listed below), we could retrieve the field's name by use of FieldComparator.getOrderBy(), which returns something like "name=category ASC". Then, use Fields.get(Object, String) to retrieve it. If the field name is in a compound format, such as something.yet.another, you could use Fields.getByCompound(Object, String)


For 5.0.6 or later, you could use FieldComparator.getRawOrderBy() instead, which returns the field name you passed to Column.setSort(String), i.e., "category".

        Column column = (Column)row.getGrid().getColumns().getChildren().get(index);
        String field = ((FieldComparator)column.getSortAscending()).getRawOrderBy();
        return Fields.get(food, field).toString();

Then, you could have the ZUML document as follows.

<grid apply="foo.FoodComposer">
    <columns menupopup="auto">
        <column label="Category" sort="auto(category)" sortDirection="ascending"/>
        <column label="Name" sort="auto(name)"/>
        <column label="Top Nutrients" sort="auto(nutrients)"/>
        <column label="% of Daily" sort="auto(percentageOfDaily)"/>
        <column label="Calories" sort="auto(calories)"/>
    </columns>
</grid>

And, the composer is as follows.

package foo;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.util.Composer;
import org.zkoss.zul.*;
public class FoodComposer implements Composer {
    Food[] _foods = new Food[] { //assume we have a collection of foods
        new Food("Vegetables", "Asparagus", "Vitamin K", 115, 43),
        new Food("Vegetables", "Beets", "Folate", 33, 74)
        //...more
    };

    public void doAfterCompose(Component comp) throws Exception {
        Grid grid = (Grid)comp;
        grid.setModel(new FoodGroupsModel(_foods));
             //Initially, we group data on 1st column in ascending order
        grid.setRowRenderer(new FoodGroupRenderer());
    }
}

Group Foot

If the groups model supports a foot (such as a summary of all data in the same group), you could return an object to represent the footer when GroupsModel.getGroupfoot(int) is called (similar to GroupsModel.getGroup(int) shall return an object representing the group).

If you use GroupsModelArray, you could override GroupsModelArray.createGroupFoot(Object[], int, int). For example,

public class FoodGroupsModel extends GroupsModelArray {
    protected Object createGroupFoot(Object[] groupdata, int index, int col) {
        return "Total " + groupdata.length + " items";
    }
...

Version History

Version Date Content
5.0.6 December 2010 Enhanced the support of tabular data as described in #5.0.6 and Later.



Last Update : 2024/06/05

Copyright © Potix Corporation. This article is licensed under GNU Free Documentation License.