Groups Model
Here we describe how to implement a groups model (org.zkoss.zul.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 org.zkoss.zul.Listbox and org.zkoss.zul.Grid support a list of grouped data.
Instead of implementing org.zkoss.zul.GroupsModel, it is suggested to extend from org.zkoss.zul.AbstractGroupsModel, or to use one of the default implementations as following:
org.zkoss.zul.SimpleGroupsModel | org.zkoss.zul.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 org.zkoss.zul.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
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 org.zkoss.zul.ext.GroupsSortableModel too. Then, org.zkoss.zul.ext.GroupsSortableModel#group(java.util.Comparator,boolean,int) will be called if the user requests to re-group the data based on a particular column. And, org.zkoss.zul.ext.GroupsSortableModel#sort(java.util.Comparator,boolean,int) will be called if the user requests to sort the data based on a particular column.
org.zkoss.zul.GroupsModelArray supports both sorting and re-grouping as described below:
- Sorting: org.zkoss.zul.GroupsModelArray sorts each group separately by using the specified comparator (java.util.Comparator).
- Re-grouping: org.zkoss.zul.GroupsModelArray
re-groups by assuming two data belong to the same group if the
compared result is the same (i.e., the given java.util.Comparator
returns 0).
- For better control, you could implement org.zkoss.zul.GroupComparator, and pass an instance to, say, org.zkoss.zul.Column#setSortAscending(java.util.Comparator) and org.zkoss.zul.Column#setSortDescending(java.util.Comparator).
Example: Grouping Tabular Data
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).
//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 make it a groups model by extending from org.zkoss.zul.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 org.zkoss.zul.GroupsModelArray#sortGroupData(java.lang.Object, java.lang.Object[], java.util.Comparator, boolean, int). Of course, you could extend from org.zkoss.zul.AbstractGroupsModel to have total control.
5.0.6 and Later
Since 5.0.6, it is much easier to handle tabular data:
First, org.zkoss.zul.GroupsModelArray#createGroupHead(java.lang.Object[], int, int) will return the correct element, so you don’t have to override it as shown above.
Second, org.zkoss.zul.ArrayComparator was introduced,
so foo.FoodComparator
is not required in the above example.
Third,
org.zkoss.zul.Column#setSort(java.lang.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 org.zkoss.zul.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 org.zkoss.zul.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
org.zkoss.zul.FieldComparator#getOrderBy(),
which returns something like “name=category ASC”. Then, use
org.zkoss.lang.reflect.Fields#get(java.lang.Object, java.lang.String)
to retrieve it. If the field name is in a compound format, such as
something.yet.another
, you could use
org.zkoss.lang.reflect.Fields#getByCompound(java.lang.Object, java.lang.String)
For 5.0.6 or later, you could use org.zkoss.zul.FieldComparator#getRawOrderBy() instead, which returns the field name you passed to org.zkoss.zul.Column#setSort(java.lang.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 org.zkoss.zul.GroupsModel#getGroupfoot(int) is called (similar to org.zkoss.zul.GroupsModel#getGroup(int) shall return an object representing the group).
If you use org.zkoss.zul.GroupsModelArray, you could override org.zkoss.zul.GroupsModelArray#createGroupFoot(java.lang.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. |