JFace StructuredViewer::refresh(element)陷阱

By | 7月 26, 2018

首先说明,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

image

1.2 修改第二行Jerry为Tom

发现没有修改成功,同时第一行变为选中状态(按理说是第二行为选中状态)。

image

1.3 再次修改第二行Name为其它值

编辑状态时,内容正确(上次修改的Tom),再次修改为Tom Brother然后保存,发现刷新的仍然是第一行(第一行为选中状态),第二行还是没有修改成功。

Edit Name

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,可以这么做,数据很多时就有效率问题。