I have hit and finally found a workaround for an annoying (and apparently fixed but not released to community) bug with updating grid rows where the row is not currently in view. The problem is that the Grid jumps to the last updated row in the result of one or more rows being updated via the ListStore that backs the Grid.
The problem can be seen in the following simple example
001 package com.cloudscapesolutions.gridupdatetest.client; 002 003 import java.util.ArrayList; 004 import java.util.List; 005 006 import com.allen_sauer.gwt.log.client.Log; 007 import com.extjs.gxt.ui.client.Style.LayoutRegion; 008 import com.extjs.gxt.ui.client.data.BaseModelData; 009 import com.extjs.gxt.ui.client.data.ModelComparer; 010 import com.extjs.gxt.ui.client.data.ModelData; 011 import com.extjs.gxt.ui.client.store.ListStore; 012 import com.extjs.gxt.ui.client.widget.ContentPanel; 013 import com.extjs.gxt.ui.client.widget.Viewport; 014 import com.extjs.gxt.ui.client.widget.grid.ColumnConfig; 015 import com.extjs.gxt.ui.client.widget.grid.ColumnModel; 016 import com.extjs.gxt.ui.client.widget.grid.Grid; 017 import com.extjs.gxt.ui.client.widget.layout.BorderLayout; 018 import com.extjs.gxt.ui.client.widget.layout.BorderLayoutData; 019 import com.extjs.gxt.ui.client.widget.layout.FitLayout; 020 import com.google.gwt.core.client.EntryPoint; 021 import com.google.gwt.user.client.Command; 022 import com.google.gwt.user.client.DeferredCommand; 023 import com.google.gwt.user.client.Random; 024 import com.google.gwt.user.client.Timer; 025 import com.google.gwt.user.client.ui.RootPanel; 026 027 /** 028 * Entry point classes define <code>onModuleLoad()</code>. 029 */ 030 public class GridUpdateTest implements EntryPoint { 031 private Viewport viewport; 032 033 public void onModuleLoad() { 034 Log.setUncaughtExceptionHandler(); 035 036 DeferredCommand.addCommand(new Command() { 037 public void execute() { 038 onModuleLoad2(); 039 } 040 }); 041 } 042 043 /** 044 * This is the entry point method. 045 */ 046 public void onModuleLoad2() { 047 viewport = new Viewport(); 048 viewport.setLayout(new FitLayout()); 049 viewport.setBorders(false); 050 ContentPanel mainPanel = new ContentPanel(); 051 mainPanel.setBodyBorder(false); 052 mainPanel.setHeaderVisible(false); 053 mainPanel.setLayout(new BorderLayout()); 054 final ListStore<ModelData> store = new ListStore<ModelData>(); 055 store.setModelComparer(new ModelComparer<ModelData>() { 056 057 @Override 058 public boolean equals(ModelData m1, ModelData m2) { 059 String m1Key = m1.get("One"); 060 String m2Key = m2.get("One"); 061 if (m1Key == null || m2Key == null) { 062 if (m1Key == null && m2Key == null) return true; 063 return false; 064 } 065 // Log.info("Comparing " + m1Key + " and " + m2Key); 066 return m1Key.equals(m2Key); 067 } 068 069 }); 070 List<ColumnConfig> configs = new ArrayList<ColumnConfig>(); 071 configs.add(new ColumnConfig("One", "One", 100)); 072 configs.add(new ColumnConfig("Two", "Two", 100)); 073 configs.add(new ColumnConfig("Three", "Three", 100)); 074 ColumnModel columnModel = new ColumnModel(configs); 075 final Grid<ModelData> grid = new Grid<ModelData>(store, columnModel); 076 077 for (int i = 0; i < 100; i++) { 078 BaseModelData bmd = new BaseModelData(); 079 bmd.set("One", i+""); 080 bmd.set("Two", i* 1000000.0); 081 bmd.set("Three", 0); 082 store.add(bmd); 083 } 084 Timer t = new Timer() { 085 086 @Override 087 public void run() { 088 int toUpdate = Random.nextInt(100); 089 BaseModelData bmd = new BaseModelData(); 090 bmd.set("One", toUpdate +""); 091 bmd.set("Two", toUpdate * 1000000.0); 092 bmd.set("Three", Random.nextInt()); 093 Log.info("Updating " + toUpdate); 094 095 // DO NOT USE store.contains() IT DOESN'T USE THE 096 // COMPARATOR AND THE BaseModelData DOES NOT IMPLEMENT 097 // EQUALS 098 if (store.findModel(bmd) == null) { 099 // Darn it 100 Log.error("ModelData not contained in store:" + toUpdate); 101 store.add(bmd); 102 } 103 else { 104 store.update(bmd); 105 } 106 107 } 108 109 110 }; 111 t.scheduleRepeating(1000); 112 113 mainPanel.add(grid, new BorderLayoutData(LayoutRegion.CENTER)); 114 viewport.add(mainPanel); 115 RootPanel.get().add(viewport); 116 viewport.layout(); 117 }118 }
|
If you run the example with GXT 2.0.1 you will set a table of 100 rows and every few seconds the rows in view will change as the data is updated. The change brings the last row updated in the grid into view in the visible region of the viewport, moving the scroll bars. This is not the behaviour I would have expected, and as I say from a post on the
GXT forum it looks like the problem has been found and fixed and is available for paying customers.
I based my solutions on the suggestion in the post to change the update behavior so that events are not triggered as updates are delivered and a refresh is called afterwards. This solution actually works well on a couple of fronts as it also allows for the bulk delivery of updates to the grid which fits better with my real application usage of an updating ListStore/Grid where I am retrieving updates to a dataset from the server periodically to update the ListStore. So the updated code for my example is as follows.
001 package com.cloudscapesolutions.gridupdatetest.client; 002 003 import java.util.ArrayList; 004 import java.util.List; 005 006 import com.allen_sauer.gwt.log.client.Log; 007 import com.extjs.gxt.ui.client.Style.LayoutRegion; 008 import com.extjs.gxt.ui.client.data.BaseModelData; 009 import com.extjs.gxt.ui.client.data.ModelComparer; 010 import com.extjs.gxt.ui.client.data.ModelData; 011 import com.extjs.gxt.ui.client.store.ListStore; 012 import com.extjs.gxt.ui.client.widget.ContentPanel; 013 import com.extjs.gxt.ui.client.widget.Viewport; 014 import com.extjs.gxt.ui.client.widget.grid.ColumnConfig; 015 import com.extjs.gxt.ui.client.widget.grid.ColumnModel; 016 import com.extjs.gxt.ui.client.widget.grid.Grid; 017 import com.extjs.gxt.ui.client.widget.layout.BorderLayout; 018 import com.extjs.gxt.ui.client.widget.layout.BorderLayoutData; 019 import com.extjs.gxt.ui.client.widget.layout.FitLayout; 020 import com.google.gwt.core.client.EntryPoint; 021 import com.google.gwt.user.client.Command; 022 import com.google.gwt.user.client.DeferredCommand; 023 import com.google.gwt.user.client.Random; 024 import com.google.gwt.user.client.Timer; 025 import com.google.gwt.user.client.ui.RootPanel; 026 027 /** 028 * Entry point classes define <code>onModuleLoad()</code>. 029 */ 030 public class GridUpdateTest implements EntryPoint { 031 private Viewport viewport; 032 033 public void onModuleLoad() { 034 Log.setUncaughtExceptionHandler(); 035 036 DeferredCommand.addCommand(new Command() { 037 public void execute() { 038 onModuleLoad2(); 039 } 040 }); 041 } 042 043 /** 044 * This is the entry point method. 045 */ 046 public void onModuleLoad2() { 047 viewport = new Viewport(); 048 viewport.setLayout(new FitLayout()); 049 viewport.setBorders(false); 050 ContentPanel mainPanel = new ContentPanel(); 051 mainPanel.setBodyBorder(false); 052 mainPanel.setHeaderVisible(false); 053 mainPanel.setLayout(new BorderLayout()); 054 final ListStore<ModelData> store = new ListStore<ModelData>(); 055 store.setModelComparer(new ModelComparer<ModelData>() { 056 057 @Override 058 public boolean equals(ModelData m1, ModelData m2) { 059 String m1Key = m1.get("One"); 060 String m2Key = m2.get("One"); 061 if (m1Key == null || m2Key == null) { 062 if (m1Key == null && m2Key == null) return true; 063 return false; 064 } 065 // Log.info("Comparing " + m1Key + " and " + m2Key); 066 return m1Key.equals(m2Key); 067 } 068 069 }); 070 List<ColumnConfig> configs = new ArrayList<ColumnConfig>(); 071 configs.add(new ColumnConfig("One", "One", 100)); 072 configs.add(new ColumnConfig("Two", "Two", 100)); 073 configs.add(new ColumnConfig("Three", "Three", 100)); 074 ColumnModel columnModel = new ColumnModel(configs); 075 final Grid<ModelData> grid = new Grid<ModelData>(store, columnModel); 076 077 for (int i = 0; i < 100; i++) { 078 BaseModelData bmd = new BaseModelData(); 079 bmd.set("One", i+""); 080 bmd.set("Two", i* 1000000.0); 081 bmd.set("Three", 0); 082 store.add(bmd); 083 } 084 Timer t = new Timer() { 085 086 @Override 087 public void run() { 088 int toUpdate = Random.nextInt(100); 089 BaseModelData bmd = new BaseModelData(); 090 bmd.set("One", toUpdate +""); 091 bmd.set("Two", toUpdate * 1000000.0); 092 bmd.set("Three", Random.nextInt()); 093 Log.info("Updating " + toUpdate); 094 095 // DO NOT USE store.contains() IT DOESN'T USE THE 096 // COMPARATOR AND THE BaseModelData DOES NOT IMPLEMENT 097 // EQUALS 098 if (store.findModel(bmd) == null) { 099 // Darn it 100 Log.error("ModelData not contained in store:" + toUpdate); 101 store.add(bmd); 102 } 103 else { 104 store.setFiresEvents(false); 105 store.update(bmd); 106 store.setFiresEvents(true); 107 grid.getView().refresh(false); 108 109 } 110 111 } 112 113 114 }; 115 t.scheduleRepeating(1000); 116 117 mainPanel.add(grid, new BorderLayoutData(LayoutRegion.CENTER)); 118 viewport.add(mainPanel); 119 RootPanel.get().add(viewport); 120 viewport.layout(); 121 } 122 }
|
The relevant changes are in lines 103-109.
1. Turn off event firing for the store
2. Update the store, as many times as you need for different rows
3. Turn events back on
4. Refresh the Grid which will refetch the data.
Hopefully there will be a release of GXT soon which fixes the problem at source.
Good news, the guys at ExtJS have fixed this problem and it looks like it is going to appear in the 2.1 release when its made available!!
ReplyDelete