GWT1.7(orz..)で作ったアプリの一部でバグが出たらしく調べてたら、バグが仕様かわからなかったのでメモしておきます。
個人的な意見としては、TreeItemの展開状態と子となるTreeItemを持っていることがイコールではないと思っているので、
バグだと思っていますが。
再現手順
どんな現象が発生したかと言うと、
- 階層を持つツリーを構成
- 1つのみの子要素を持つTreeItemを展開する
- 2.の子要素を削除する
- 2.及び3.以外のTreeItemをマウスで選択し、キーボードで2.を選択するように上下に移動する
- 2.が選択できずにエラーとなる。
という感じ。
サンプルコードは以下。ちなみにGWT2.4でサンプル作ってますが同様に発生する模様。
package com.wordpress.prepro.gwt.sample.tree.client; import com.google.gwt.core.client.EntryPoint; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.logical.shared.SelectionEvent; import com.google.gwt.event.logical.shared.SelectionHandler; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.RootPanel; import com.google.gwt.user.client.ui.Tree; import com.google.gwt.user.client.ui.TreeItem; /** * 検証用のツリーモジュール。 * HTML上には以下のid属性を定義しておくこと。 * <pre> * <div id="buttonContainer"></div> * <div id="treeContainer"></div> * </pre> */ public class Tree_sample implements EntryPoint { /** * This is the entry point method. */ public void onModuleLoad() { final Tree tree = new Tree(); tree.addSelectionHandler(new SelectionHandler<TreeItem>() { @Override public void onSelection(SelectionEvent<TreeItem> event) { System.out.println(event.getSelectedItem().getState()); } }); tree.addTextItem("Tree1"); tree.addTextItem("Tree2"); final TreeItem treeItem = new TreeItem("Tree3"); tree.addItem(treeItem); final TreeItem childItem1 = new TreeItem("ChildTree1"); treeItem.addItem(childItem1); Button removeButton = new Button("削除"); // 選択した要素を削除する。 removeButton.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { TreeItem parent = tree.getSelectedItem().getParentItem(); tree.getSelectedItem().remove(); parent.setState(parent.getChildCount() > 0); // イベントが発行されるのがいやであればこちらで。 //parent.setState(parent.getChildCount() > 0, false); } }); tree.addTextItem("Tree2"); RootPanel.get("treeContainer").add(tree); RootPanel.get("buttonContainer").add(removeButton); } }
スタックトレースはこんな感じでで出てました。
14:25:22.818 [ERROR] [tree_sample] Uncaught exception escaped java.lang.NullPointerException: null at com.google.gwt.user.client.ui.Tree.findDeepestOpenChild(Tree.java:1012) at com.google.gwt.user.client.ui.Tree.findDeepestOpenChild(Tree.java:1015) at com.google.gwt.user.client.ui.Tree.moveSelectionUp(Tree.java:1252) at com.google.gwt.user.client.ui.Tree.keyboardNavigation(Tree.java:1103) at com.google.gwt.user.client.ui.Tree.onBrowserEvent(Tree.java:647) at com.google.gwt.user.client.DOM.dispatchEventImpl(DOM.java:1351) at com.google.gwt.user.client.DOM.dispatchEvent(DOM.java:1307) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.lang.reflect.Method.invoke(Unknown Source) at com.google.gwt.dev.shell.MethodAdaptor.invoke(MethodAdaptor.java:103) at com.google.gwt.dev.shell.MethodDispatch.invoke(MethodDispatch.java:71) at com.google.gwt.dev.shell.OophmSessionHandler.invoke(OophmSessionHandler.java:172) at com.google.gwt.dev.shell.BrowserChannelServer.reactToMessagesWhileWaitingForReturn(BrowserChannelServer.java:337) at com.google.gwt.dev.shell.BrowserChannelServer.invokeJavascript(BrowserChannelServer.java:218) at com.google.gwt.dev.shell.ModuleSpaceOOPHM.doInvoke(ModuleSpaceOOPHM.java:136) at com.google.gwt.dev.shell.ModuleSpace.invokeNative(ModuleSpace.java:561) at com.google.gwt.dev.shell.ModuleSpace.invokeNativeObject(ModuleSpace.java:269) at com.google.gwt.dev.shell.JavaScriptHost.invokeNativeObject(JavaScriptHost.java:91) at com.google.gwt.core.client.impl.Impl.apply(Impl.java) at com.google.gwt.core.client.impl.Impl.entry0(Impl.java:213) at sun.reflect.GeneratedMethodAccessor30.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.lang.reflect.Method.invoke(Unknown Source) at com.google.gwt.dev.shell.MethodAdaptor.invoke(MethodAdaptor.java:103) at com.google.gwt.dev.shell.MethodDispatch.invoke(MethodDispatch.java:71) at com.google.gwt.dev.shell.OophmSessionHandler.invoke(OophmSessionHandler.java:172) at com.google.gwt.dev.shell.BrowserChannelServer.reactToMessages(BrowserChannelServer.java:292) at com.google.gwt.dev.shell.BrowserChannelServer.processConnection(BrowserChannelServer.java:546) at com.google.gwt.dev.shell.BrowserChannelServer.run(BrowserChannelServer.java:363) at java.lang.Thread.run(Unknown Source)
原因
TreeItemは内部に自身が展開しているかどうかの状態を持っているのですが、
TreeItem#removeを利用して削除した場合、TreeItemの状態が維持されてしまうようです。
# 自分の使うメソッドが違うのかもしれません。。。
そのため、その状態のままキーボードを用いてツリー間の移動を行うと、
TreeItemに子要素が存在するとみなしてこのTreeItemの子要素を探索しに行ってしまい、
存在しないためnullが返されTreeItemの状態が取れずNullPointerExceptionが発生してしまいます。
以下TreeItemの該当箇所のソースです。
private TreeItem findDeepestOpenChild(TreeItem item) { if (!item.getState()) { return item; } return findDeepestOpenChild(item.getChild(item.getChildCount() - 1)); }
回避策
というわけで回避策です。
一番手っ取り早いのは上記ソースコードにパッチを当てることでしょう。
こんな感じに。
892c892 < if (!item.getState()) { --- > if (!item.getState() || item.getChildCount() == 0) {
あるいはこんな感じにTreeItem#removeした場合に、展開状態を更新するようにしておきます。
Tree tree = new Tree(); // ....<中略>.... // 削除対象の親となるTreeItemを取得しておく TreeItem parent = tree.getSelectedItem().getParentItem(); // TreeItemを削除 tree.getSelectedItem().remove(); // 親の展開状態を更新 // 既に展開されておりかつ子要素が存在する場合だけ展開状態を維持する parent.setState(parent.getState() && parent.getChildCount() > 0); // 展開状態のイベントをdispatchさせたくないようであればこちらを利用する。 // parent.setState(parent.getState() && parent.getChildCount() > 0, false);
ただこの回避策をとった場合、子要素が0件になった場合に必ず展開状態が閉じてしまうことになります。
子要素が0件でも展開状態を維持しておきたい場合は、これではダメです。
とりあえず自分は後者で回避しました。
以上です。