首先说明,StucturedViewer的所有实现类都有这种陷阱问题,包括:
- JFace的TableViewer, TreeVewer, ListViewer,ComboViewer。
- Nebula Grid的GridTableViewer和GridTreeViewer。
1. 问题描述
StructueredViewer用于结构化显示数据,例如表格显示,树形显示等。数据显示是以行为单位的,一行显示一个对象的相关数据。例如,使用TableViewer显示Person对象,Person只有name和gender两个field。
class Person { private String name = ""; private String gender = ""; public Person(String name, String gender) { this.name = name; this.gender = gender; } @Override public int hashCode() { return Objects.hash(name, gender); } @Override public boolean equals(Object obj) { if (obj == null || obj.getClass() != this.getClass()) { return false; } return Objects.equals(name, ((Person) obj).name) && Objects.equals(gender, ((Person) obj).gender); } }
注意:这里我们重写了hashCode方法和equals方法,某些情况下我们需要这么做。
1.1 打开TableViewer
1.2 修改第二行Jerry为Tom
发现没有修改成功,同时第一行变为选中状态(按理说是第二行为选中状态)。
1.3 再次修改第二行Name为其它值
编辑状态时,内容正确(上次修改的Tom),再次修改为Tom Brother然后保存,发现刷新的仍然是第一行(第一行为选中状态),第二行还是没有修改成功。
2. 问题分析
从选中状态上看,每次编辑后,第一行变为选中状态,暗示说明刷新的是第一行,debug后,发现其果然刷新的是第一行。原因是因为,编辑后调用的是StructuredViewer—>refresh(element)方法,此方法仅仅刷新该element对应的行。而StructuredViewer内部是遍历所有行元素,使用equals方法来判断哪一行需要刷新。上面的例子中,我们把Jerry改成Tom,导致第二行和第一行完全一行,equals比较时,发现第一行就已经相等了,所以刷新的是第一行。
说明:该问题仅仅是UI刷新出现问题了,其实数据写入是正确的,如果有保存的话,再次打开数据是正确的。
3. 解决方法
3.1 去掉Person对象的equals方法
当去掉Person的equals方法是,每一个对象实例都不相等,这样就可以保证刷新是正确的。但是有些情况下,确实需要重写hashCode和equals方法,这样贸然拿掉,可能会导致其它bug。
3.2 设置IElementComparer
查看StructuredViewer->equals(Object, Object)方法,发现其仅在没有comparer的情况才使用element本身的equals方法。所以我们可以调用StructuredViewer->setComparer(IElementComparer)设置comparer,来修改比较行为。
/** * Compares two elements for equality. Uses the element comparer if one has * been set, otherwise uses the default <code>equals</code> method on the * elements themselves. * * @param elementA * the first element * @param elementB * the second element * @return whether elementA is equal to elementB */ protected boolean equals(Object elementA, Object elementB) { if (comparer == null) { return elementA == null ? elementB == null : elementA.equals(elementB); } else { return elementA == null ? elementB == null : comparer.equals(elementA, elementB); } }
IElementComparer接口定义,其同时修改了hashCode方法和equals方法。
package org.eclipse.jface.viewers; /** * This interface is used to compare elements in a viewer for equality, * and to provide the hash code for an element. * This allows the client of the viewer to specify different equality criteria * and a different hash code implementation than the * <code>equals</code> and <code>hashCode</code> implementations of the * elements themselves. * * @see StructuredViewer#setComparer */ public interface IElementComparer { /** * Compares two elements for equality * * @param a the first element * @param b the second element * @return whether a is equal to b */ boolean equals(Object a, Object b); /** * Returns the hash code for the given element. * @param element the element the hash code is calculated for * * @return the hash code for the given element */ int hashCode(Object element); }
安全的解决方法是,设置comparer,比较其引用是不是同一个对象实例。
tableViewer.setComparer(new IElementComparer() { @Override public boolean equals(Object elementA, Object elementB) { return elementA == null || elementB == null ? false : elementA == elementB; } @Override public int hashCode(Object element) { return element != null ? element.hashCode() : 0; } });
3.3 refresh整个viewer
调用StructueredViewer->refresh()方法,直接refresh整个viewer,就不会有equals的问题了。对于数据不是很多的viewer,可以这么做,数据很多时就有效率问题。