Tuesday, November 9, 2010

GWT CellTable with sorting

UPDATE: There is new article about the sorting here.

The latest version of GWT (version 2.1) brings some new widget possibilities for building UI. One of those components is the Date Presentation Widgets which gives you a set of powerful and high performance components like lists, table, trees and browsers. In this blog I would like to show you hot to implement sorting in CellTable Data Presentation Widget. CellTable basically renders row values in columns but at the current version does not support sorting out-of-box. I was looking after how to implement this functionality and realize that there is already very good example in the Expenses demo application which you will find in the GWT Installation Samples folder. I tried to get out only the sorting functionality and implement it in my own CellTable and here I would like to give you my experience.

First you need to implement header which handles click operation. In my example I’ve just got the one implemented into the Expenses example.

1 /*
2 * Copyright 2010 Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16 package com.mvp.client.ui;
17
18 import com.google.gwt.cell.client.ClickableTextCell;
19 import com.google.gwt.core.client.GWT;
20 import com.google.gwt.resources.client.ClientBundle;
21 import com.google.gwt.resources.client.ImageResource;
22 import com.google.gwt.safehtml.client.SafeHtmlTemplates;
23 import com.google.gwt.safehtml.shared.SafeHtml;
24 import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
25 import com.google.gwt.safehtml.shared.SafeHtmlUtils;
26 import com.google.gwt.user.cellview.client.Header;
27 import com.google.gwt.user.client.ui.AbstractImagePrototype;
28
29 public class SortableHeader extends Header<String> {
30
31 interface Template extends SafeHtmlTemplates {
32 @Template("<div style=\"position:relative;cursor:hand;cursor:pointer;"
33 + "padding-right:{0}px;\">{1}<div>{2}</div></div>")
34 SafeHtml sorted(int imageWidth, SafeHtml arrow, String text);
35
36 @Template("<div style=\"position:relative;cursor:hand;cursor:pointer;"
37 + "padding-right:{0}px;\"><div style=\"position:absolute;display:none;"
38 + "\"></div><div>{1}</div></div>")
39 SafeHtml unsorted(int imageWidth, String text);
40 }
41
42 private static Template template;
43
44 /**
45 * Image resources.
46 */
47 public static interface Resources extends ClientBundle {
48
49 ImageResource downArrow();
50
51 ImageResource upArrow();
52 }
53
54 private static final Resources RESOURCES = GWT.create(Resources.class);
55 private static final int IMAGE_WIDTH = 16;
56 private static final SafeHtml DOWN_ARROW = makeImage(RESOURCES.downArrow());
57 private static final SafeHtml UP_ARROW = makeImage(RESOURCES.upArrow());
58
59 private static SafeHtml makeImage(ImageResource resource) {
60 AbstractImagePrototype proto = AbstractImagePrototype.create(resource);
61 String html = proto.getHTML().replace("style='",
62 "style='position:absolute;right:0px;top:0px;");
63 return SafeHtmlUtils.fromTrustedString(html);
64 }
65
66 private boolean reverseSort = false;
67 private boolean sorted = false;
68 private String text;
69
70 public SortableHeader(String text) {
71 super(new ClickableTextCell());
72 if (template == null) {
73 template = GWT.create(Template.class);
74 }
75 this.text = text;
76 }
77
78 public boolean getReverseSort() {
79 return reverseSort;
80 }
81
82 @Override
83 public String getValue() {
84 return text;
85 }
86
87 @Override
88 public void render(SafeHtmlBuilder sb) {
89 if (sorted) {
90 sb.append(template.sorted(IMAGE_WIDTH, reverseSort ? DOWN_ARROW
91 : UP_ARROW, text));
92 } else {
93 sb.append(template.unsorted(IMAGE_WIDTH, text));
94 }
95 }
96
97 public void setReverseSort(boolean reverseSort) {
98 this.reverseSort = reverseSort;
99 }
100
101 public void setSorted(boolean sorted) {
102 this.sorted = sorted;
103 }
104
105 public void toggleReverseSort() {
106 this.reverseSort = !this.reverseSort;
107 }
108 }
109

Take a look at the render method. This methods is called every time when the CellTable renders the headers. In the example above you will see that an arrow image will be rendered every time together with the text depending on the sorting (ASC or DESC). Also in the constructor you will see that our header is based on a ClickableTextCell. This cell implementation calls the ValueUpdater implementation every time you click on the cell. Make sure when you copy this code to your project to get also the two images downArrow.jpg and upArrow.jpg which are in the same folder together with the SortableHeader class in Expenses example.


Next step is to get the generic interface which says how the cell value will be returned. The interface is very simple and self explained. You will pass to the cell an object T and return object C.


1 public interface GetValue<T, C> {
2 C getValue(T object);
3 }
4 private final List<SortableHeader> allHeaders = new ArrayList<SortableHeader>();

The interfaces does not need to know the type of the values now. You can do it even more complicated, for example the object T could implement specific interface. The variable allHeaders contains all sortable headers. We will used this like shown Expenses example to modify the value of the sort before we re-render the headers, if you remember the render method from the SortableHeaders class.


As next I used the object ContactInfo from the CellTable example, which you can find here. This calls could be any shared object you want to transport via RPC. Don’t forget to implement interface Comparable which you will need for the sorting later. Here the ContactInfo from the ContactDatabase.java from the CellTable example:


1 public static class Category {
2
3 private final String displayName;
4
5 private Category(String displayName) {
6 this.displayName = displayName;
7 }
8
9 public String getDisplayName() {
10 return displayName;
11 }
12 }
13
14 public static class ContactInfo implements Comparable<ContactInfo> {
15
16 /**
17 * The key provider that provides the unique ID of a contact.
18 */
19 public static final ProvidesKey<ContactDatabase.ContactInfo> KEY_PROVIDER = new ProvidesKey<ContactInfo>() {
20 public Object getKey(ContactInfo item) {
21 return item == null ? null : item.getId();
22 }
23 };
24
25 private static int nextId = 0;
26
27 private String address;
28 private Date birthday;
29 private Category category;
30 private String firstName;
31 private final int id;
32 private String lastName;
33
34 public ContactInfo(Category category) {
35 this.id = nextId;
36 nextId++;
37 setCategory(category);
38 }
39
40 @Override
41 public int compareTo(ContactInfo o) {
42 return (o == null || o.firstName == null) ? 0 : o.firstName
43 .compareTo(firstName);
44 }
45
46 @Override
47 public boolean equals(Object o) {
48 if (o instanceof ContactInfo) {
49 return id == ((ContactInfo) o).id;
50 }
51 return false;
52 }
53
54 /**
55 * @return the contact's address
56 */
57 public String getAddress() {
58 return address;
59 }
60
61 /**
62 * @return the contact's birthday
63 */
64 public Date getBirthday() {
65 return birthday;
66 }
67
68 /**
69 * @return the category of the conteact
70 */
71 public Category getCategory() {
72 return category;
73 }
74
75 /**
76 * @return the contact's firstName
77 */
78 public String getFirstName() {
79 return firstName;
80 }
81
82 /**
83 * @return the contact's full name
84 */
85 public final String getFullName() {
86 return firstName + " " + lastName;
87 }
88
89 /**
90 * @return the unique ID of the contact
91 */
92 public int getId() {
93 return this.id;
94 }
95
96 /**
97 * @return the contact's lastName
98 */
99 public String getLastName() {
100 return lastName;
101 }
102
103 @Override
104 public int hashCode() {
105 return id;
106 }
107
108 /**
109 * Set the contact's address.
110 *
111 * @param address
112 * the address
113 */
114 public void setAddress(String address) {
115 this.address = address;
116 }
117
118 /**
119 * Set the contact's birthday.
120 *
121 * @param birthday
122 * the birthday
123 */
124 public void setBirthday(Date birthday) {
125 this.birthday = birthday;
126 }
127
128 /**
129 * Set the contact's category.
130 *
131 * @param category
132 * the category to set
133 */
134 public void setCategory(Category category) {
135 assert category != null : "category cannot be null";
136 this.category = category;
137 }
138
139 /**
140 * Set the contact's first name.
141 *
142 * @param firstName
143 * the firstName to set
144 */
145 public void setFirstName(String firstName) {
146 this.firstName = firstName;
147 }
148
149 /**
150 * Set the contact's last name.
151 *
152 * @param lastName
153 * the lastName to set
154 */
155 public void setLastName(String lastName) {
156 this.lastName = lastName;
157 }
158 }

Now we come to the more interesting part. The method which handles the logic with the adding the SortableHeaders into the CellTable. I’ve got this method from the Expenses example but I make it a little bit simpler and accommodate it to my needs, so here is it:


1 private <C> Column<ContactInfo, C> addColumn(final String text,
2 final Cell<C> cell, final GetValue<ContactInfo, C> getter,
3 final Comparator<ContactInfo> ascComparator,
4 final Comparator<ContactInfo> descComparator) {
5
6 // gets the cell value
7 final Column<ContactInfo, C> column = new Column<ContactInfo, C>(cell) {
8 @Override
9 public C getValue(ContactInfo object) {
10 return getter.getValue(object);
11 }
12 };
13
14 final SortableHeader header = new SortableHeader(text);
15 allHeaders.add(header);
16
17 // call this everytime headers is clicked
18 header.setUpdater(new ValueUpdater<String>() {
19 public void update(String value) {
20 header.setSorted(true);
21 header.toggleReverseSort();
22
23 for (SortableHeader otherHeader : allHeaders) {
24 if (otherHeader != header) {
25 otherHeader.setSorted(false);
26 otherHeader.setReverseSort(true);
27 }
28 }
29
30 // sort the clicked column
31 sortExpenses(ContactDatabase.get().getDataProvider().getList(),
32 header.getReverseSort() ? descComparator
33 : ascComparator);
34
35 cellTable.redrawHeaders();
36
37 // Go to the first page of the newly-sorted results, if wished
38 // pager.firstPage();
39 }
40 });
41 cellTable.addColumn(column, header);
42 return column;
43 }
44

What we have here is addColumn method which adds given a name and cell to the our CellTable object. For SortableHeader we set also ValueUpdater which will be called every time the header has been clicked and then inside we call the function sortExpenses which does the actual sorting. This function gets the comparator implementation and depending on it sorts the column. The comparator implementation looks like this:


1 private <C extends Comparable<C>> Comparator<ContactInfo> createColumnComparator(
2 final GetValue<ContactInfo, C> getter, final boolean descending) {
3 return new Comparator<ContactInfo>() {
4 public int compare(ContactInfo o1, ContactInfo o2) {
5 // Null check the row object.
6 if (o1 == null && o2 == null) {
7 return 0;
8 } else if (o1 == null) {
9 return descending ? 1 : -1;
10 } else if (o2 == null) {
11 return descending ? -1 : 1;
12 }
13
14 // Compare the column value.
15 C c1 = getter.getValue(o1);
16 C c2 = getter.getValue(o2);
17 if (c1 == null && c2 == null) {
18 return 0;
19 } else if (c1 == null) {
20 return descending ? 1 : -1;
21 } else if (c2 == null) {
22 return descending ? -1 : 1;
23 }
24 int comparison = c1.compareTo(c2);
25 return descending ? -comparison : comparison;
26 }
27 };
28 }

As you can see the comparator gets the GetValue implementation which could be any column. This allows you to pass any parameter from ContactInfo without the needs to implement new comparator for every column element.


Now you can add new columns to your CellTable like this:


1 addColumn("First name", new TextCell(),
2 new GetValue<ContactInfo, String>() {
3 public String getValue(ContactInfo object) {
4 return object.getFirstName();
5 }
6 });

If you want to check out the full example I share it here.

4 comments:

  1. Great article, thanks!

    I'm trying to add a sortable column with Integers but cannot figure out how.

    With this code:
    addColumn("Number column", new TextCell(), new GetValue() {
    @Override
    public Integer getValue(ContactInfo object) {
    return new Integer(Random.nextInt(999));
    }
    });

    I get this error:
    The method addColumn(String, Cell, CellTableViewImpl.GetValue) in the type CellTableViewImpl is not applicable for the arguments (String, TextCell, new CellTableViewImpl.GetValue(){})

    // Stephan

    ReplyDelete
  2. I will check on this later today, I actually extend the example and make it more generic, so you can easy add new type columns I will share it this or next week.

    ReplyDelete
  3. And the next week has not yet come!!!

    ReplyDelete
  4. You didn't see the beginning of article, I checked already a new example here:

    http://webcentersuite.blogspot.com/2010/12/gwt-celltable-with-sorting-extended.html

    ReplyDelete