From 0fee10e494bc4f7842bede393c5b242090c89b37 Mon Sep 17 00:00:00 2001
From: felmer <felmer>
Date: Mon, 12 Dec 2011 08:51:11 +0000
Subject: [PATCH] Refactoring: AbstractBrowserGrid absorbed into TypedTableGrid

SVN: 23968
---
 .../{grid => }/ColumnSettingsConfigurer.java  |   24 +-
 .../client/application/ui/TypedTableGrid.java | 2449 ++++++++++++++++-
 .../ui/grid/AbstractBrowserGrid.java          | 2283 ---------------
 .../ui/grid/BrowserGridPagingToolBar.java     |    4 +-
 .../grid/CustomColumnsMetadataProvider.java   |    2 +-
 .../application/ui/grid/ExtendedGridView.java |    2 +-
 .../ui/grid/PendingFetchManager.java          |    2 +-
 .../AbstractGridDataRefreshCallback.java      |   17 +-
 .../AuthorizationManagementConsolTest.java    |    4 +-
 .../client/application/SampleBrowserTest.java |    2 +-
 .../application/ui/util/GridTestUtils.java    |    4 +-
 11 files changed, 2382 insertions(+), 2411 deletions(-)
 rename openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/{grid => }/ColumnSettingsConfigurer.java (90%)
 delete mode 100644 openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/AbstractBrowserGrid.java

diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/ColumnSettingsConfigurer.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/ColumnSettingsConfigurer.java
similarity index 90%
rename from openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/ColumnSettingsConfigurer.java
rename to openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/ColumnSettingsConfigurer.java
index 148b87f1687..d754eccd8bf 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/ColumnSettingsConfigurer.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/ColumnSettingsConfigurer.java
@@ -14,8 +14,9 @@
  * limitations under the License.
  */
 
-package ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid;
+package ch.systemsx.cisd.openbis.generic.client.web.client.application.ui;
 
+import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -28,11 +29,15 @@ import ch.systemsx.cisd.openbis.generic.client.web.client.ICommonClientServiceAs
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.IViewContext;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.framework.DisplaySettingsManager;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.framework.IDisplaySettingsGetter;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.model.BaseEntityModel;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.AbstractColumnSettingsDataModelProvider;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.ColumnDataModel;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.ColumnSettingsDialog;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.CustomColumnsMetadataProvider;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.expressions.filter.FilterToolbar;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.GridCustomColumnInfo;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.ResultSetFetchConfig;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.SortInfo;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.TableModelRowWithObject;
 
 /**
  * A class that manages the configuring of column settings in the AbstractBrowserGrid.
@@ -42,15 +47,15 @@ import ch.systemsx.cisd.openbis.generic.shared.basic.dto.SortInfo;
  * 
  * @author Chandrasekhar Ramakrishnan
  */
-public class ColumnSettingsConfigurer<T, M extends BaseEntityModel<T>>
+public class ColumnSettingsConfigurer<T extends Serializable>
 {
     public static final int DEFAULT_COLUMN_WIDTH = 150;
 
-    private final AbstractBrowserGrid<T, M> browserGrid;
+    private final TypedTableGrid<T> browserGrid;
 
     private final IViewContext<ICommonClientServiceAsync> viewContext;
 
-    private final FilterToolbar<T> filterToolbar;
+    private final FilterToolbar<TableModelRowWithObject<T>> filterToolbar;
 
     private final CustomColumnsMetadataProvider customColumnsMetadataProvider;
 
@@ -61,8 +66,9 @@ public class ColumnSettingsConfigurer<T, M extends BaseEntityModel<T>>
     // finished.
     private final ResultSetFetchConfig<String> pendingFetchConfigOrNull;
 
-    public ColumnSettingsConfigurer(AbstractBrowserGrid<T, M> browserGrid,
-            IViewContext<ICommonClientServiceAsync> viewContext, FilterToolbar<T> filterToolbar,
+    public ColumnSettingsConfigurer(TypedTableGrid<T> browserGrid,
+            IViewContext<ICommonClientServiceAsync> viewContext,
+            FilterToolbar<TableModelRowWithObject<T>> filterToolbar,
             CustomColumnsMetadataProvider customColumnsMetadataProvider, String resultSetKeyOrNull,
             ResultSetFetchConfig<String> pendingFetchConfigOrNull)
     {
@@ -77,7 +83,7 @@ public class ColumnSettingsConfigurer<T, M extends BaseEntityModel<T>>
     public void showDialog()
     {
         List<ColumnDataModel> settingsModel =
-                AbstractBrowserGrid.createColumnsSettingsModel(browserGrid.getFullColumnModel(),
+                TypedTableGrid.createColumnsSettingsModel(browserGrid.getFullColumnModel(),
                         filterToolbar.extractFilteredColumnIds());
         AbstractColumnSettingsDataModelProvider provider =
                 new AbstractColumnSettingsDataModelProvider(settingsModel)
@@ -167,7 +173,7 @@ public class ColumnSettingsConfigurer<T, M extends BaseEntityModel<T>>
 
     /**
      * Creates a new column model based on the specified column data models and the old full column
-     * model as provided by {@link AbstractBrowserGrid#getFullColumnModel()}.
+     * model as provided by {@link TypedTableGrid#getFullColumnModel()}.
      */
     private ColumnModel createNewColumnModel(List<ColumnDataModel> newColumnDataModels)
     {
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/TypedTableGrid.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/TypedTableGrid.java
index 7edec7e8acc..05af29bbac3 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/TypedTableGrid.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/TypedTableGrid.java
@@ -20,186 +20,2422 @@ import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Set;
 
+import com.extjs.gxt.ui.client.GXT;
+import com.extjs.gxt.ui.client.Style.Orientation;
+import com.extjs.gxt.ui.client.Style.SelectionMode;
+import com.extjs.gxt.ui.client.data.BasePagingLoadConfig;
+import com.extjs.gxt.ui.client.data.BasePagingLoadResult;
+import com.extjs.gxt.ui.client.data.BasePagingLoader;
+import com.extjs.gxt.ui.client.data.Loader;
+import com.extjs.gxt.ui.client.data.ModelData;
+import com.extjs.gxt.ui.client.data.PagingLoadConfig;
+import com.extjs.gxt.ui.client.data.PagingLoadResult;
+import com.extjs.gxt.ui.client.data.PagingLoader;
+import com.extjs.gxt.ui.client.data.RpcProxy;
+import com.extjs.gxt.ui.client.event.BaseEvent;
+import com.extjs.gxt.ui.client.event.ButtonEvent;
+import com.extjs.gxt.ui.client.event.Events;
+import com.extjs.gxt.ui.client.event.GridEvent;
+import com.extjs.gxt.ui.client.event.Listener;
+import com.extjs.gxt.ui.client.event.MessageBoxEvent;
+import com.extjs.gxt.ui.client.event.SelectionChangedEvent;
+import com.extjs.gxt.ui.client.event.SelectionListener;
+import com.extjs.gxt.ui.client.store.ListStore;
+import com.extjs.gxt.ui.client.widget.Component;
+import com.extjs.gxt.ui.client.widget.Container;
+import com.extjs.gxt.ui.client.widget.ContentPanel;
+import com.extjs.gxt.ui.client.widget.Dialog;
+import com.extjs.gxt.ui.client.widget.Info;
+import com.extjs.gxt.ui.client.widget.InfoConfig;
+import com.extjs.gxt.ui.client.widget.Label;
+import com.extjs.gxt.ui.client.widget.LayoutContainer;
+import com.extjs.gxt.ui.client.widget.MessageBox;
+import com.extjs.gxt.ui.client.widget.button.Button;
+import com.extjs.gxt.ui.client.widget.grid.ColumnConfig;
+import com.extjs.gxt.ui.client.widget.grid.ColumnModel;
+import com.extjs.gxt.ui.client.widget.grid.EditorGrid;
+import com.extjs.gxt.ui.client.widget.grid.EditorGrid.ClicksToEdit;
+import com.extjs.gxt.ui.client.widget.grid.Grid;
 import com.extjs.gxt.ui.client.widget.grid.GridCellRenderer;
+import com.extjs.gxt.ui.client.widget.grid.GridSelectionModel;
+import com.extjs.gxt.ui.client.widget.layout.FitLayout;
+import com.extjs.gxt.ui.client.widget.layout.RowData;
+import com.extjs.gxt.ui.client.widget.layout.RowLayout;
+import com.extjs.gxt.ui.client.widget.menu.Menu;
+import com.extjs.gxt.ui.client.widget.toolbar.PagingToolBar;
+import com.extjs.gxt.ui.client.widget.toolbar.SeparatorToolItem;
+import com.extjs.gxt.ui.client.widget.toolbar.ToolBar;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.AbstractImagePrototype;
 
+import ch.systemsx.cisd.common.shared.basic.utils.StringUtils;
 import ch.systemsx.cisd.openbis.generic.client.web.client.ICommonClientServiceAsync;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.AbstractAsyncCallback;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.Dict;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.GenericConstants;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.IGenericImageBundle;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.IViewContext;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ShowRelatedDatasetsDialog;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.VoidAsyncCallback;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.framework.AbstractTabItemFactory;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.framework.DispatcherHelper;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.framework.DisplaySettingsManager;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.framework.DisplaySettingsManager.GridDisplaySettings;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.framework.IDatabaseModificationObserver;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.framework.IDisplaySettingsGetter;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.framework.IDisplayTypeIDGenerator;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.model.BaseEntityModel;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.plugin.IClientPlugin;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.plugin.IClientPluginFactory;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.renderer.LinkRenderer;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.renderer.MaterialRenderer;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.renderer.MultilineStringCellRenderer;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.renderer.RealNumberRenderer;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.renderer.TimestampStringCellRenderer;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.renderer.VocabularyTermStringCellRenderer;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.renderer.customcolumn.core.CustomColumnStringRenderer;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.ComponentEventLogger.EventPair;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.columns.framework.IColumnDefinitionUI;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.columns.framework.LinkExtractor;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.AbstractBrowserGrid;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.columns.specific.GridCustomColumnDefinition;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.BrowserGridPagingToolBar;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.BrowserGridPagingToolBar.PagingToolBarButtonKind;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.ColumnDataModel;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.ColumnDefsAndConfigs;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.ColumnListener;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.CustomColumnsMetadataProvider;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.DisposableEntityChooser;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.ExtendedGridView;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.IBrowserGridActionInvoker;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.ICellListener;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.ICellListenerAndLinkGenerator;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.IColumnDefinitionProvider;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.IDisplayTypeIDProvider;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.IDisposableComponent;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.PendingFetchManager;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.expressions.filter.FilterToolbar;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.listener.OpenEntityDetailsTabHelper;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.listener.OpenEntityEditorTabClickListener;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.widget.IDataRefreshCallback;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.GWTUtils;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.GWTUtils.DisplayInfoTime;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.IDelegatedAction;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.IMessageProvider;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.WindowUtils;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.lang.StringEscapeUtils;
+import ch.systemsx.cisd.openbis.generic.client.web.client.dto.Constants;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.DefaultResultSetConfig;
+import ch.systemsx.cisd.openbis.generic.client.web.client.dto.EntityPropertyUpdatesResult;
+import ch.systemsx.cisd.openbis.generic.client.web.client.dto.GridCustomColumnInfo;
+import ch.systemsx.cisd.openbis.generic.client.web.client.dto.GridFilters;
+import ch.systemsx.cisd.openbis.generic.client.web.client.dto.GridRowModels;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.RelatedDataSetCriteria;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.ResultSet;
+import ch.systemsx.cisd.openbis.generic.client.web.client.dto.ResultSetFetchConfig;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.ResultSetFetchConfig.ResultSetFetchMode;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.TableExportCriteria;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.TypedTableResultSet;
+import ch.systemsx.cisd.openbis.generic.shared.basic.CodeConverter;
 import ch.systemsx.cisd.openbis.generic.shared.basic.GridRowModel;
 import ch.systemsx.cisd.openbis.generic.shared.basic.IColumnDefinition;
 import ch.systemsx.cisd.openbis.generic.shared.basic.IEntityInformationHolder;
+import ch.systemsx.cisd.openbis.generic.shared.basic.IEntityInformationHolderWithPermId;
+import ch.systemsx.cisd.openbis.generic.shared.basic.URLMethodWithParameters;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.BasicEntityType;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.ColumnSetting;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DataTypeCode;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DatabaseModificationKind;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.EntityKind;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.EntityTableCell;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.IEntityPropertiesHolder;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.IEntityProperty;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.ISerializableComparable;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.MaterialIdentifier;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.SortInfo;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.SortInfo.SortDir;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.TableModelColumnHeader;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.TableModelRowWithObject;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.TypedTableModel;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.VocabularyTerm;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.WebClientConfiguration;
 
 /**
  * Abstract superclass of all grids based on {@link TypedTableModel}.
  * 
  * @author Franz-Josef Elmer
  */
-public abstract class TypedTableGrid<T extends Serializable>
-        extends
-        AbstractBrowserGrid<TableModelRowWithObject<T>, BaseEntityModel<TableModelRowWithObject<T>>>
+public abstract class TypedTableGrid<T extends Serializable> extends LayoutContainer implements
+        IDatabaseModificationObserver, IDisplayTypeIDProvider,
+        IColumnDefinitionProvider<TableModelRowWithObject<T>>
 {
+    private static final IGenericImageBundle IMAGE_BUNDLE = GWT
+            .<IGenericImageBundle> create(IGenericImageBundle.class);
+
     public static final String GRID_POSTFIX = "-grid";
 
     /**
-     * Do not display more than this amount of columns in the report, web browsers have problem with
-     * it
+     * Do not display more than this amount of columns in the report, web browsers have problem with
+     * it
+     */
+    private static final int MAX_SHOWN_COLUMNS = 200;
+
+    /**
+     * Called when user wants to export the data. It can happen only after a previous refresh of the
+     * data has taken place. The export criteria has only the cache key
+     */
+    abstract protected void prepareExportEntities(
+            TableExportCriteria<TableModelRowWithObject<T>> exportCriteria,
+            AbstractAsyncCallback<String> callback);
+
+    // --------
+
+    /**
+     * If user selected some entities in given browser first a dialog is shown where he can select
+     * between showing data sets related to selected/displayed entities. Then a tab is displayed
+     * where these related data sets are listed.<br>
+     * <br>
+     * If no entities were selected in given browser the tab is displayed where data sets related to
+     * all entities displayed in the grid are listed.
+     */
+    protected static final <E extends IEntityInformationHolder> void showRelatedDataSets(
+            final IViewContext<ICommonClientServiceAsync> viewContext,
+            final TypedTableGrid<E> browser)
+    {
+        final List<TableModelRowWithObject<E>> selectedEntities = browser.getSelectedBaseObjects();
+        final TableExportCriteria<TableModelRowWithObject<E>> displayedEntities =
+                browser.createTableExportCriteria();
+        if (selectedEntities.isEmpty())
+        {
+            // no entity selected - show datasets related to all displayed
+            RelatedDataSetCriteria<E> relatedCriteria =
+                    RelatedDataSetCriteria.<E> createDisplayedEntities(displayedEntities);
+            ShowRelatedDatasetsDialog.showRelatedDatasetsTab(viewContext, relatedCriteria);
+        } else
+        {
+            // > 0 entity selected - show dialog with all/selected radio
+            new ShowRelatedDatasetsDialog<E>(viewContext, selectedEntities, displayedEntities,
+                    browser.getTotalCount()).show();
+        }
+    }
+
+    private final class CellListenerAndLinkGenerator implements ICellListenerAndLinkGenerator<T>
+    {
+        private final EntityKind entityKind;
+
+        private final TableModelColumnHeader header;
+
+        private CellListenerAndLinkGenerator(EntityKind entityKind, TableModelColumnHeader header)
+        {
+            this.entityKind = entityKind;
+            this.header = header;
+        }
+
+        public String tryGetLink(T entity, final ISerializableComparable value)
+        {
+            if (value == null || value.toString().length() == 0)
+            {
+                return null;
+            }
+            if (value instanceof EntityTableCell)
+            {
+                EntityTableCell entityTableCell = (EntityTableCell) value;
+                if (entityTableCell.isMissing() || entityTableCell.isFake())
+                {
+                    return null;
+                }
+                String permId = entityTableCell.getPermId();
+                if (entityTableCell.getEntityKind() == EntityKind.MATERIAL)
+                {
+                    return LinkExtractor.tryExtract(MaterialIdentifier.tryParseIdentifier(permId));
+                } else
+                {
+                    return LinkExtractor.createPermlink(entityTableCell.getEntityKind(), permId);
+                }
+
+            }
+            return LinkExtractor.createPermlink(entityKind, value.toString());
+        }
+
+        public void handle(TableModelRowWithObject<T> rowItem, boolean specialKeyPressed)
+        {
+            ISerializableComparable cellValue = rowItem.getValues().get(header.getIndex());
+            if (cellValue instanceof EntityTableCell)
+            {
+                EntityTableCell entityTableCell = (EntityTableCell) cellValue;
+                String permId = entityTableCell.getPermId();
+                if (entityTableCell.getEntityKind() == EntityKind.MATERIAL)
+                {
+                    MaterialIdentifier materialIdentifier =
+                            MaterialIdentifier.tryParseIdentifier(permId);
+                    OpenEntityDetailsTabHelper.open(viewContext, materialIdentifier,
+                            specialKeyPressed);
+                } else
+                {
+                    OpenEntityDetailsTabHelper.open(viewContext, entityTableCell.getEntityKind(),
+                            permId, specialKeyPressed);
+                }
+            } else
+            {
+                OpenEntityDetailsTabHelper.open(viewContext, entityKind, cellValue.toString(),
+                        specialKeyPressed);
+            }
+        }
+    }
+
+    /** Manager of table modifications */
+    public interface ITableModificationsManager<M extends ModelData>
+    {
+        /** @return <code>true</code> iff there are any uncommitted modifications */
+        boolean isTableDirty();
+
+        /** save all modifications made in the table to the DB */
+        void saveModifications();
+
+        /** save all modifications made in the table to the DB and call the after save action */
+        void saveModifications(IDelegatedAction afterSaveAction);
+
+        /** cancel all modifications made in the table */
+        void cancelModifications();
+
+        /** handle cell editing event */
+        void handleEditingEvent(M model, String columnID, String stringOrNull);
+
+        /** @return callback for given modifications made to specified model. */
+        AsyncCallback<EntityPropertyUpdatesResult> createApplyModificationsCallback(final M model,
+                final List<IModification> modifications);
+    }
+
+    protected interface IModification
+    {
+        String getColumnID();
+
+        String tryGetNewValue();
+    }
+
+    private static class Modification implements IModification
+    {
+        private final String columnID;
+
+        private final String newValueOrNull;
+
+        public Modification(String columnID, String newValueOrNull)
+        {
+            super();
+            this.columnID = columnID;
+            this.newValueOrNull =
+                    newValueOrNull == null ? null : StringEscapeUtils.unescapeHtml(newValueOrNull);
+        }
+
+        public String getColumnID()
+        {
+            return columnID;
+        }
+
+        public String tryGetNewValue()
+        {
+            return newValueOrNull;
+        }
+    }
+
+    protected final IViewContext<ICommonClientServiceAsync> viewContext;
+
+    protected final ICellListener<TableModelRowWithObject<T>> showEntityViewerLinkClickListener;
+
+    protected final ITableModificationsManager<BaseEntityModel<TableModelRowWithObject<T>>> tableModificationsManager;
+
+    // ------ private section. NOTE: it should remain unaccessible to subclasses! ---------------
+
+    private static final int PAGE_SIZE = Constants.GRID_PAGE_SIZE;
+
+    // set to true to see some useful debugging messages
+    private static final boolean DEBUG = false;
+
+    private final PagingLoader<PagingLoadResult<BaseEntityModel<TableModelRowWithObject<T>>>> pagingLoader;
+
+    private final ContentPanel contentPanel;
+
+    private final Grid<BaseEntityModel<TableModelRowWithObject<T>>> grid;
+
+    private final ColumnListener<TableModelRowWithObject<T>, BaseEntityModel<TableModelRowWithObject<T>>> columnListener;
+
+    private final boolean refreshAutomatically;
+
+    // the toolbar has the refresh and export buttons besides the paging controls
+    private final BrowserGridPagingToolBar pagingToolbar;
+
+    // used to change displayed filter widgets
+    private final FilterToolbar<TableModelRowWithObject<T>> filterToolbar;
+
+    private final ToolBar modificationsToolbar;
+
+    private final IDisplayTypeIDGenerator displayTypeIDGenerator;
+
+    // --------- private non-final fields
+
+    // available columns definitions
+    private Set<IColumnDefinition<TableModelRowWithObject<T>>> columnDefinitions;
+
+    private final CustomColumnsMetadataProvider customColumnsMetadataProvider;
+
+    // result set key of the last refreshed data
+    private String resultSetKeyOrNull;
+
+    // Keeps track of the pending fetch (only tracks 1)
+    private final PendingFetchManager pendingFetchManager;
+
+    private IDataRefreshCallback refreshCallback;
+
+    private LayoutContainer bottomToolbars;
+
+    private ColumnModel fullColumnModel;
+
+    private final Map<String, ICellListenerAndLinkGenerator<T>> listenerLinkGenerators =
+            new HashMap<String, ICellListenerAndLinkGenerator<T>>();
+
+    private List<TableModelColumnHeader> headers;
+
+    private List<IColumnDefinitionUI<TableModelRowWithObject<T>>> columnUIDefinitions;
+
+    private String downloadURL;
+
+    private Map<String, IColumnDefinition<TableModelRowWithObject<T>>> columnDefinitionsMap;
+
+    private String currentGridDisplayTypeID;
+
+    private List<IColumnDefinitionUI<TableModelRowWithObject<T>>> visibleColDefinitions;
+
+    protected TypedTableGrid(final IViewContext<ICommonClientServiceAsync> viewContext,
+            String browserId, IDisplayTypeIDGenerator displayTypeIDGenerator)
+    {
+        this(viewContext, browserId, false, displayTypeIDGenerator);
+    }
+
+    /**
+     * @param refreshAutomatically should the data be automatically loaded when the grid is rendered
+     *            for the first time?
+     * @param browserId unique id of the browser grid
+     */
+    protected TypedTableGrid(IViewContext<ICommonClientServiceAsync> viewContext, String browserId,
+            boolean refreshAutomatically, IDisplayTypeIDGenerator displayTypeIDGenerator)
+    {
+        String gridId = browserId + GRID_POSTFIX;
+        pendingFetchManager = new PendingFetchManager();
+        this.displayTypeIDGenerator = displayTypeIDGenerator;
+        this.viewContext = viewContext;
+        int logID = log("create browser grid " + gridId);
+        this.refreshAutomatically = refreshAutomatically;
+        this.pagingLoader = createPagingLoader();
+        this.customColumnsMetadataProvider = new CustomColumnsMetadataProvider();
+        this.grid = createGrid(pagingLoader, gridId);
+        // WORKAROUND
+        // Lazy loading of rows causes tests using experiment browser fail (selection of
+        // project in project tree grid doesn't work).
+        // Turning it off for all grids is the safest solution for our system tests framework
+        // and should improve GUI speed in development mode a bit.
+        grid.setLazyRowRender(0);
+        this.pagingToolbar =
+                new BrowserGridPagingToolBar(asActionInvoker(), viewContext, PAGE_SIZE, gridId);
+        pagingToolbar.bind(pagingLoader);
+        this.filterToolbar =
+                new FilterToolbar<TableModelRowWithObject<T>>(viewContext, gridId, this,
+                        createApplyFiltersDelagator());
+        this.tableModificationsManager = new TableModificationsManager();
+        this.modificationsToolbar =
+                new TableModificationsToolbar(viewContext, tableModificationsManager);
+
+        this.contentPanel = createEmptyContentPanel();
+        bottomToolbars = createBottomToolbars(contentPanel, pagingToolbar);
+        configureBottomToolbarSyncSize();
+        contentPanel.add(grid);
+        contentPanel.setBottomComponent(bottomToolbars);
+        contentPanel.setHeaderVisible(false);
+        columnListener =
+                new ColumnListener<TableModelRowWithObject<T>, BaseEntityModel<TableModelRowWithObject<T>>>(
+                        grid);
+        showEntityViewerLinkClickListener = createShowEntityViewerLinkClickListener();
+        registerLinkClickListenerFor(Dict.CODE, showEntityViewerLinkClickListener);
+        setLayout(new FitLayout());
+        add(contentPanel);
+
+        configureLoggingBetweenEvents(logID);
+
+        grid.addListener(Events.HeaderContextMenu, new Listener<GridEvent<ModelData>>()
+            {
+                public void handleEvent(final GridEvent<ModelData> ge)
+                {
+                    Menu menu = ge.getMenu();
+                    int itemCount = menu.getItemCount();
+                    for (int i = 2; i < itemCount; i++)
+                    {
+                        menu.remove(menu.getItem(2));
+                    }
+                }
+            });
+        if (viewContext.getModel().isEmbeddedMode())
+        {
+            removeButtons(PagingToolBarButtonKind.CONFIG, PagingToolBarButtonKind.REFRESH);
+        }
+        setId(browserId);
+    }
+
+    public void removeButtons(PagingToolBarButtonKind... buttonKinds)
+    {
+        pagingToolbar.removeButtons(buttonKinds);
+    }
+
+    private ICellListener<TableModelRowWithObject<T>> createShowEntityViewerLinkClickListener()
+    {
+        return new ICellListener<TableModelRowWithObject<T>>()
+            {
+                public void handle(TableModelRowWithObject<T> rowItem, boolean keyPressed)
+                {
+                    showEntityViewer(rowItem, false, keyPressed);
+                }
+            };
+    }
+
+    private void configureBottomToolbarSyncSize()
+    {
+        // fixes problems with:
+        // - no 'overflow' button when some buttons don't fit into pagingToolbar
+        pagingLoader.addListener(Loader.Load, new Listener<BaseEvent>()
+            {
+                public void handleEvent(BaseEvent be)
+                {
+                    pagingToolbar.syncSize();
+                }
+            });
+        // - hidden paging toolbar
+        pagingToolbar.addListener(Events.AfterLayout, new Listener<BaseEvent>()
+            {
+                public void handleEvent(BaseEvent be)
+                {
+                    contentPanel.syncSize();
+                }
+            });
+        // - bottom toolbar is not resized when new filter row appears
+        filterToolbar.addListener(Events.AfterLayout, new Listener<BaseEvent>()
+            {
+                public void handleEvent(BaseEvent be)
+                {
+                    contentPanel.syncSize();
+                }
+            });
+    }
+
+    private void configureLoggingBetweenEvents(int logID)
+    {
+        if (viewContext.isLoggingEnabled())
+        {
+            ComponentEventLogger logger = new ComponentEventLogger(viewContext, getId());
+            logger.prepareLoggingBetweenEvents(contentPanel, EventPair.RENDER);
+            logger.prepareLoggingBetweenEvents(this, EventPair.LAYOUT);
+            logger.prepareLoggingBetweenEvents(grid, EventPair.LAYOUT);
+            logger.prepareLoggingBetweenEvents(contentPanel, EventPair.LAYOUT);
+            logger.prepareLoggingBetweenEvents(bottomToolbars, EventPair.LAYOUT);
+            logger.prepareLoggingBetweenEvents(filterToolbar, EventPair.LAYOUT);
+            logger.prepareLoggingBetweenEvents(pagingToolbar, EventPair.LAYOUT);
+            viewContext.logStop(logID);
+        }
+    }
+
+    protected int log(String message)
+    {
+        return viewContext.log(message + " [" + getId() + "]");
+    }
+
+    protected void showEntityInformationHolderViewer(IEntityInformationHolderWithPermId entity,
+            boolean editMode, boolean inBackground)
+    {
+        if (editMode
+                && OpenEntityEditorTabClickListener.forbidDeletedEntityModification(viewContext,
+                        entity))
+        {
+            return;
+        }
+        final EntityKind entityKind = entity.getEntityKind();
+        final AbstractTabItemFactory tabView;
+        BasicEntityType entityType = entity.getEntityType();
+        final IClientPluginFactory clientPluginFactory =
+                viewContext.getClientPluginFactoryProvider().getClientPluginFactory(entityKind,
+                        entityType);
+        final IClientPlugin<BasicEntityType, IEntityInformationHolderWithPermId> createClientPlugin =
+                clientPluginFactory.createClientPlugin(entityKind);
+        if (editMode)
+        {
+            tabView = createClientPlugin.createEntityEditor(entity);
+        } else
+        {
+            tabView = createClientPlugin.createEntityViewer(entity);
+        }
+        tabView.setInBackground(inBackground);
+        DispatcherHelper.dispatchNaviEvent(tabView);
+    }
+
+    /** Refreshes the grid without showing the loading progress bar */
+    protected final void refreshGridSilently()
+    {
+        grid.setLoadMask(false);
+        int id = log("refresh silently");
+        refresh();
+        grid.setLoadMask(true);
+        viewContext.logStop(id);
+    }
+
+    /**
+     * Shows/hides the load mask immediately.
+     * 
+     * @param loadMask Load mask is shown if <code>true</code> otherwise it is hidden.
+     */
+    public void setLoadMaskImmediately(boolean loadMask)
+    {
+        if (grid.isRendered())
+        {
+            if (loadMask)
+            {
+                grid.el().mask(GXT.MESSAGES.loadMask_msg());
+            } else
+            {
+                grid.el().unmask();
+            }
+        }
+
+    }
+
+    /**
+     * Registers the specified listener for clicks on links in the specified column.
+     * 
+     * @param columnID Column ID. Not case sensitive.
+     * @param listener Listener handle single clicks.
+     */
+    protected final void registerLinkClickListenerFor(final String columnID,
+            final ICellListener<TableModelRowWithObject<T>> listener)
+    {
+        if (viewContext.isSimpleOrEmbeddedMode() == false)
+        {
+            columnListener.registerLinkClickListener(columnID, listener);
+        }
+    }
+
+    /**
+     * Allows multiple selection instead of single selection.
+     */
+    protected final void allowMultipleSelection()
+    {
+        grid.getSelectionModel().setSelectionMode(SelectionMode.MULTI);
+    }
+
+    protected List<TableModelRowWithObject<T>> getGridElements()
+    {
+        List<BaseEntityModel<TableModelRowWithObject<T>>> models = grid.getStore().getModels();
+        List<TableModelRowWithObject<T>> elements = new ArrayList<TableModelRowWithObject<T>>();
+        for (BaseEntityModel<TableModelRowWithObject<T>> model : models)
+        {
+            elements.add(model.getBaseObject());
+        }
+        return elements;
+    }
+
+    private IDelegatedAction createApplyFiltersDelagator()
+    {
+        return new IDelegatedAction()
+            {
+                public void execute()
+                {
+                    if (resultSetKeyOrNull != null && pendingFetchManager.hasNoPendingFetch())
+                    {
+                        ResultSetFetchConfig<String> fetchConfig =
+                                ResultSetFetchConfig.createFetchFromCache(resultSetKeyOrNull);
+                        reloadData(fetchConfig);
+                    }
+                }
+            };
+    }
+
+    /** @return this grid as a disposable component with a specified toolbar at the top. */
+    protected DisposableEntityChooser<TableModelRowWithObject<T>> asDisposableWithToolbar(
+            final IDisposableComponent toolbar)
+    {
+        final LayoutContainer container = new LayoutContainer();
+        container.setLayout(new RowLayout());
+        container.add(toolbar.getComponent());
+        container.add(this, new RowData(1, 1));
+
+        return asDisposableEntityChooser(container, toolbar);
+    }
+
+    /** @return this grid as a disposable component */
+    protected final DisposableEntityChooser<TableModelRowWithObject<T>> asDisposableWithoutToolbar()
+    {
+        return asDisposableEntityChooser(this);
+    }
+
+    /**
+     * @return this grid as a disposable component with a specified toolbar at the top and a tree on
+     *         the left.
+     */
+    protected final DisposableEntityChooser<TableModelRowWithObject<T>> asDisposableWithToolbarAndTree(
+            final IDisposableComponent toolbar, final Component tree, String headerOrNull)
+    {
+        // WORKAROUND: BorderLayout causes problems when rendered in a tab
+        // We use RowLayout here but we loose the split this way.
+        final LayoutContainer container = new LayoutContainer();
+        container.setLayout(new RowLayout(Orientation.VERTICAL));
+        container.add(toolbar.getComponent(), new RowData(1, -1));
+
+        final LayoutContainer subContainer = new LayoutContainer();
+        subContainer.setLayout(new RowLayout(Orientation.HORIZONTAL));
+        subContainer.add(tree, new RowData(300, 1));
+        setHeader(headerOrNull);
+        subContainer.add(this, new RowData(1, 1));
+        container.add(subContainer, new RowData(1, 1));
+
+        return asDisposableEntityChooser(container, toolbar);
+    }
+
+    protected final void setHeader(String headerOrNull)
+    {
+        if (headerOrNull != null)
+        {
+            this.contentPanel.setHeaderVisible(true);
+            this.contentPanel.setHeading(headerOrNull);
+        } else
+        {
+            this.contentPanel.setHeaderVisible(false);
+        }
+    }
+
+    protected final DisposableEntityChooser<TableModelRowWithObject<T>> asDisposableEntityChooser(
+            final Component mainComponent, final IDisposableComponent... disposableComponents)
+    {
+        final TypedTableGrid<T> self = this;
+        return new DisposableEntityChooser<TableModelRowWithObject<T>>()
+            {
+                public TableModelRowWithObject<T> tryGetSingleSelected()
+                {
+                    List<BaseEntityModel<TableModelRowWithObject<T>>> items = getSelectedItems();
+                    if (items.isEmpty())
+                    {
+                        return null;
+                    } else
+                    {
+                        return items.get(0).getBaseObject();
+                    }
+                }
+
+                public void dispose()
+                {
+                    debug("dispose a browser");
+                    self.disposeCache();
+                    for (IDisposableComponent disposableComponent : disposableComponents)
+                    {
+                        disposableComponent.dispose();
+                    }
+                }
+
+                public Component getComponent()
+                {
+                    return mainComponent;
+                }
+
+                public DatabaseModificationKind[] getRelevantModifications()
+                {
+                    return self.getRelevantModifications();
+                }
+
+                public void update(Set<DatabaseModificationKind> observedModifications)
+                {
+                    self.update(observedModifications);
+                }
+
+            };
+    }
+
+    @Override
+    protected void onRender(final Element parent, final int pos)
+    {
+        super.onRender(parent, pos);
+        if (refreshAutomatically)
+        {
+            int id = log("layout automatically");
+            layout();
+            viewContext.logStop(id);
+            id = log("refresh automatically");
+            refresh();
+            viewContext.logStop(id);
+        }
+    }
+
+    private PagingLoader<PagingLoadResult<BaseEntityModel<TableModelRowWithObject<T>>>> createPagingLoader()
+    {
+        final RpcProxy<PagingLoadResult<BaseEntityModel<TableModelRowWithObject<T>>>> proxy =
+                new RpcProxy<PagingLoadResult<BaseEntityModel<TableModelRowWithObject<T>>>>()
+                    {
+                        @Override
+                        protected void load(
+                                Object loadConfig,
+                                AsyncCallback<PagingLoadResult<BaseEntityModel<TableModelRowWithObject<T>>>> callback)
+                        {
+                            loadData((PagingLoadConfig) loadConfig, callback);
+                        }
+                    };
+        final BasePagingLoader<PagingLoadResult<BaseEntityModel<TableModelRowWithObject<T>>>> newPagingLoader =
+                new BasePagingLoader<PagingLoadResult<BaseEntityModel<TableModelRowWithObject<T>>>>(
+                        proxy);
+        newPagingLoader.setRemoteSort(true);
+
+        return newPagingLoader;
+    }
+
+    private void loadData(
+            final PagingLoadConfig loadConfig,
+            final AsyncCallback<PagingLoadResult<BaseEntityModel<TableModelRowWithObject<T>>>> callback)
+    {
+        if (pendingFetchManager.hasNoPendingFetch())
+        {
+            // this can happen when user wants to sort data - the refresh method is not called
+            if (resultSetKeyOrNull == null)
+            {
+                // data are not yet cached, so we ignore this call - should not really happen
+                return;
+            }
+            pendingFetchManager.pushPendingFetchConfig(ResultSetFetchConfig
+                    .createFetchFromCache(resultSetKeyOrNull));
+        }
+        GridFilters<TableModelRowWithObject<T>> filters = filterToolbar.getFilters();
+        final DefaultResultSetConfig<String, TableModelRowWithObject<T>> resultSetConfig =
+                createPagingConfig(loadConfig, filters, getGridDisplayTypeID());
+        debug("create a refresh callback " + pendingFetchManager.tryTopPendingFetchConfig());
+        final ListEntitiesCallback listCallback =
+                new ListEntitiesCallback(viewContext, callback, resultSetConfig);
+
+        listEntities(resultSetConfig, listCallback);
+    }
+
+    // Default visibility so that friend classes can use -- should otherwise be considered private
+    void debug(String msg)
+    {
+        if (DEBUG)
+        {
+            String text =
+                    "[grid: " + getGridDisplayTypeID() + ", cache: " + resultSetKeyOrNull + "] "
+                            + msg;
+            System.out.println(text);
+        }
+    }
+
+    private DefaultResultSetConfig<String, TableModelRowWithObject<T>> createPagingConfig(
+            PagingLoadConfig loadConfig, GridFilters<TableModelRowWithObject<T>> filters,
+            String gridDisplayId)
+    {
+        int limit = loadConfig.getLimit();
+        int offset = loadConfig.getOffset();
+        com.extjs.gxt.ui.client.data.SortInfo sortInfo = loadConfig.getSortInfo();
+
+        DefaultResultSetConfig<String, TableModelRowWithObject<T>> resultSetConfig =
+                new DefaultResultSetConfig<String, TableModelRowWithObject<T>>();
+        resultSetConfig.setLimit(limit);
+        resultSetConfig.setOffset(offset);
+        resultSetConfig.setAvailableColumns(columnDefinitions);
+        SortInfo translatedSortInfo = translateSortInfo(sortInfo);
+        Set<String> columnIDs = getIDsOfColumnsToBeShown();
+        resultSetConfig.setIDsOfPresentedColumns(columnIDs);
+        resultSetConfig.setSortInfo(translatedSortInfo);
+        resultSetConfig.setFilters(filters);
+        resultSetConfig.setCacheConfig(pendingFetchManager.tryTopPendingFetchConfig());
+        resultSetConfig.setGridDisplayId(gridDisplayId);
+        resultSetConfig.setCustomColumnErrorMessageLong(viewContext.getDisplaySettingsManager()
+                .isDebuggingModeEnabled());
+        return resultSetConfig;
+    }
+
+    private Set<String> getIDsOfColumnsToBeShown()
+    {
+        Set<String> columnIDs = new HashSet<String>();
+        DisplaySettingsManager manager = viewContext.getDisplaySettingsManager();
+        List<ColumnSetting> columnSettings = manager.getColumnSettings(getGridDisplayTypeID());
+        if (columnSettings != null)
+        {
+            for (ColumnSetting columnSetting : columnSettings)
+            {
+                if (columnSetting.isHidden() == false)
+                {
+                    columnIDs.add(columnSetting.getColumnID());
+                }
+            }
+        }
+        List<IColumnDefinition<TableModelRowWithObject<T>>> visibleColumns =
+                getVisibleColumns(columnDefinitions);
+        for (IColumnDefinition<TableModelRowWithObject<T>> definition : visibleColumns)
+        {
+            columnIDs.add(definition.getIdentifier());
+        }
+        return columnIDs;
+    }
+
+    private static <T> SortInfo translateSortInfo(com.extjs.gxt.ui.client.data.SortInfo sortInfo)
+    {
+        return translateSortInfo(sortInfo.getSortField(), sortInfo.getSortDir());
+    }
+
+    private static <T> SortInfo translateSortInfo(String sortFieldId,
+            com.extjs.gxt.ui.client.Style.SortDir sortDir)
+    {
+        SortInfo sortInfo = new SortInfo();
+        sortInfo.setSortField(sortFieldId);
+        sortInfo.setSortDir(translate(sortDir));
+
+        return sortInfo;
+    }
+
+    private static SortDir translate(com.extjs.gxt.ui.client.Style.SortDir sortDir)
+    {
+        if (sortDir.equals(com.extjs.gxt.ui.client.Style.SortDir.ASC))
+        {
+            return SortDir.ASC;
+        } else if (sortDir.equals(com.extjs.gxt.ui.client.Style.SortDir.DESC))
+        {
+            return SortDir.DESC;
+        } else if (sortDir.equals(com.extjs.gxt.ui.client.Style.SortDir.NONE))
+        {
+            return SortDir.NONE;
+        } else
+        {
+            throw new IllegalStateException("unknown sort dir: " + sortDir);
+        }
+    }
+
+    private static com.extjs.gxt.ui.client.Style.SortDir translate(SortDir sortDir)
+    {
+        if (sortDir.equals(SortDir.ASC))
+        {
+            return com.extjs.gxt.ui.client.Style.SortDir.ASC;
+        } else if (sortDir.equals(SortDir.DESC))
+        {
+            return com.extjs.gxt.ui.client.Style.SortDir.DESC;
+        } else if (sortDir.equals(SortDir.NONE))
+        {
+            return com.extjs.gxt.ui.client.Style.SortDir.NONE;
+        } else
+        {
+            throw new IllegalStateException("unknown sort dir: " + sortDir);
+        }
+    }
+
+    /** @return number of rows in the grid */
+    public final int getRowNumber()
+    {
+        return grid.getStore().getCount();
+    }
+
+    public final class ListEntitiesCallback extends
+            AbstractAsyncCallback<ResultSet<TableModelRowWithObject<T>>>
+    {
+        private final AsyncCallback<PagingLoadResult<BaseEntityModel<TableModelRowWithObject<T>>>> delegate;
+
+        // configuration with which the listing was called
+        private DefaultResultSetConfig<String, TableModelRowWithObject<T>> resultSetConfig;
+
+        private int logID;
+
+        private boolean reloadingPhase;
+
+        public ListEntitiesCallback(
+                final IViewContext<?> viewContext,
+                final AsyncCallback<PagingLoadResult<BaseEntityModel<TableModelRowWithObject<T>>>> delegate,
+                final DefaultResultSetConfig<String, TableModelRowWithObject<T>> resultSetConfig)
+        {
+            super(viewContext);
+            this.delegate = delegate;
+            this.resultSetConfig = resultSetConfig;
+            logID = log("load data");
+        }
+
+        //
+        // AbstractAsyncCallback
+        //
+
+        @Override
+        public final void finishOnFailure(final Throwable caught)
+        {
+            reenableAfterFailure();
+            // no need to show error message - it should be shown by DEFAULT_CALLBACK_LISTENER
+            caught.printStackTrace();
+            delegate.onFailure(caught);
+        }
+
+        public final void reenableAfterFailure()
+        {
+            grid.el().unmask();
+            onComplete(false);
+            pagingToolbar.enable(); // somehow enabling toolbar is lost in its handleEvent() method
+        }
+
+        @Override
+        protected void performSuccessActionOrIgnore(final IDelegatedAction successAction)
+        {
+            if (tableModificationsManager.isTableDirty())
+            {
+                final Listener<MessageBoxEvent> listener = new Listener<MessageBoxEvent>()
+                    {
+                        public void handleEvent(MessageBoxEvent me)
+                        {
+                            if (me.getButtonClicked().getItemId().equals(Dialog.YES))
+                            {
+                                tableModificationsManager.saveModifications(new IDelegatedAction()
+                                    {
+                                        public void execute()
+                                        {
+                                            // ignore this callback and refresh the table
+                                            ignore();
+                                            reenableAfterFailure();
+                                            refresh();
+                                        }
+                                    });
+                            } else
+                            {
+                                tableModificationsManager.cancelModifications();
+                                successAction.execute();
+                            }
+                        }
+                    };
+                final String title =
+                        viewContext.getMessage(Dict.CONFIRM_SAVE_TABLE_MODIFICATIONS_DIALOG_TITLE);
+                final String msg =
+                        viewContext
+                                .getMessage(Dict.CONFIRM_SAVE_TABLE_MODIFICATIONS_DIALOG_MESSAGE);
+                MessageBox.confirm(title, msg, listener);
+            } else
+            {
+                successAction.execute();
+            }
+        }
+
+        @Override
+        protected final void process(final ResultSet<TableModelRowWithObject<T>> result)
+        {
+            viewContext.logStop(logID);
+            logID = log("process loaded data");
+            // save the key of the result, later we can refer to the result in the cache using this
+            // key
+            String key = result.getResultSetKey();
+            saveCacheKey(key);
+            GridRowModels<TableModelRowWithObject<T>> rowModels = result.getList();
+            boolean partial = result.isPartial();
+            List<GridCustomColumnInfo> customColumnMetadata = rowModels.getCustomColumnsMetadata();
+            customColumnsMetadataProvider.setCustomColumnsMetadata(customColumnMetadata);
+
+            if (reloadingPhase)
+            {
+                reloadingPhase = false;
+            } else if (partial)
+            {
+                reloadingPhase = true;
+                BasePagingLoadConfig loadConfig = new BasePagingLoadConfig();
+                loadConfig.setLimit(resultSetConfig.getLimit());
+                loadConfig.setOffset(resultSetConfig.getOffset());
+                SortInfo sortInfo = resultSetConfig.getSortInfo();
+                if (sortInfo != null)
+                {
+                    String sortField = sortInfo.getSortField();
+                    if (sortField != null)
+                    {
+                        loadConfig.setSortField(sortField);
+                        loadConfig.setSortDir(translate(sortInfo.getSortDir()));
+                    }
+                }
+                resultSetConfig =
+                        createPagingConfig(loadConfig, filterToolbar.getFilters(),
+                                resultSetConfig.tryGetGridDisplayId());
+                resultSetConfig.setCacheConfig(ResultSetFetchConfig
+                        .createFetchFromCacheAndRecompute(key));
+                this.reuse();
+                listEntities(resultSetConfig, this);
+            }
+            // convert the result to the model data for the grid control
+            final List<BaseEntityModel<TableModelRowWithObject<T>>> models =
+                    createModels(rowModels);
+            final PagingLoadResult<BaseEntityModel<TableModelRowWithObject<T>>> loadResult =
+                    new BasePagingLoadResult<BaseEntityModel<TableModelRowWithObject<T>>>(models,
+                            resultSetConfig.getOffset(), result.getTotalLength());
+
+            delegate.onSuccess(loadResult);
+            pagingToolbar.enableExportButton();
+            pagingToolbar.updateDefaultConfigButton(true);
+
+            if (reloadingPhase == false)
+            {
+                pagingToolbar.enable();
+                filterToolbar.refreshColumnFiltersDistinctValues(rowModels
+                        .getColumnDistinctValues());
+            } else
+            {
+                pagingToolbar.disableForLoadingRest();
+            }
+            onComplete(true);
+
+            viewContext.logStop(logID);
+        }
+
+        // notify that the refresh is done
+        private void onComplete(boolean wasSuccessful)
+        {
+            pendingFetchManager.popPendingFetch();
+            refreshCallback.postRefresh(wasSuccessful);
+        }
+
+        private List<BaseEntityModel<TableModelRowWithObject<T>>> createModels(
+                final GridRowModels<TableModelRowWithObject<T>> gridRowModels)
+        {
+            final List<BaseEntityModel<TableModelRowWithObject<T>>> result =
+                    new ArrayList<BaseEntityModel<TableModelRowWithObject<T>>>();
+            initializeModelCreation();
+            for (final GridRowModel<TableModelRowWithObject<T>> entity : gridRowModels)
+            {
+                BaseEntityModel<TableModelRowWithObject<T>> model = createModel(entity);
+                result.add(model);
+            }
+            return result;
+        }
+
+        @Override
+        /* Note: we want to differentiate between callbacks in different subclasses of this grid. */
+        public String getCallbackId()
+        {
+            return grid.getId();
+        }
+    }
+
+    protected Set<String> getIDsOfVisibleColumns()
+    {
+        Set<String> visibleColumnIds = new HashSet<String>();
+        for (int i = 0, n = fullColumnModel.getColumnCount(); i < n; i++)
+        {
+            ColumnConfig column = fullColumnModel.getColumn(i);
+            if (column.isHidden() == false)
+            {
+                visibleColumnIds.add(column.getId());
+            }
+        }
+        return visibleColumnIds;
+    }
+
+    // wraps this browser into the interface appropriate for the toolbar. If this class would just
+    // implement the interface it could be very confusing for the code reader.
+    protected IBrowserGridActionInvoker asActionInvoker()
+    {
+        final TypedTableGrid<T> delegate = this;
+        return new IBrowserGridActionInvoker()
+            {
+                public void export(boolean allColumns)
+                {
+                    delegate.export(allColumns);
+                }
+
+                public void refresh()
+                {
+                    int id = log("refresh in action invoker");
+                    delegate.refresh();
+                    viewContext.logStop(id);
+                }
+
+                public void configure()
+                {
+                    delegate.configureColumnSettings();
+                }
+
+                public void toggleFilters(boolean show)
+                {
+                    if (show)
+                    {
+                        int logId = log("adding filters");
+                        delegate.showFiltersBar();
+                        viewContext.logStop(logId);
+                    } else
+                    {
+                        int logId = log("removing filters");
+                        bottomToolbars.remove(filterToolbar);
+                        bottomToolbars.layout();
+                        viewContext.logStop(logId);
+                    }
+                }
+
+            };
+    }
+
+    protected void showFiltersBar()
+    {
+        // always show filters under modifications
+        int position = bottomToolbars.getItemCount() > 1 ? 1 : 0;
+        bottomToolbars.insert(filterToolbar, position);
+        bottomToolbars.layout();
+    }
+
+    protected void showModificationsBar()
+    {
+        if (bottomToolbars.getItems().contains(modificationsToolbar) == false)
+        {
+            GWTUtils.displayInfo(viewContext.getMessage(Dict.TABLE_MODIFICATIONS_INFO_TITLE),
+                    viewContext.getMessage(Dict.TABLE_MODIFICATIONS_INFO_TEXT),
+                    DisplayInfoTime.LONG);
+            bottomToolbars.insert(modificationsToolbar, 0);
+            bottomToolbars.layout();
+        }
+    }
+
+    protected void hideModificationsBar()
+    {
+        bottomToolbars.remove(modificationsToolbar);
+        bottomToolbars.layout();
+    }
+
+    protected static interface ISelectedEntityInvoker<M>
+    {
+        void invoke(M selectedItem, boolean keyPressed);
+    }
+
+    protected final ISelectedEntityInvoker<BaseEntityModel<TableModelRowWithObject<T>>> asShowEntityInvoker(
+            final boolean editMode)
+    {
+        return new ISelectedEntityInvoker<BaseEntityModel<TableModelRowWithObject<T>>>()
+            {
+                public void invoke(BaseEntityModel<TableModelRowWithObject<T>> selectedItem,
+                        boolean keyPressed)
+                {
+                    if (selectedItem != null)
+                    {
+                        showEntityViewer(selectedItem.getBaseObject(), editMode, keyPressed);
+                    }
+                }
+            };
+    }
+
+    private ISelectedEntityInvoker<BaseEntityModel<TableModelRowWithObject<T>>> createNotImplementedInvoker()
+    {
+        return new ISelectedEntityInvoker<BaseEntityModel<TableModelRowWithObject<T>>>()
+            {
+                public void invoke(BaseEntityModel<TableModelRowWithObject<T>> selectedItem,
+                        boolean keyPressed)
+                {
+                    MessageBox.alert(viewContext.getMessage(Dict.MESSAGEBOX_WARNING),
+                            viewContext.getMessage(Dict.NOT_IMPLEMENTED), null);
+                }
+            };
+    }
+
+    /**
+     * @return a button which has no action but is enabled only when one entity in the grid is
+     *         selected. Useful only for writing prototypes.
+     */
+    protected final Button createSelectedItemDummyButton(final String title)
+    {
+        return createSelectedItemButton(title, createNotImplementedInvoker());
+    }
+
+    /**
+     * @return like {@link #createSelectedItemButton(String, ISelectedEntityInvoker)} with button id
+     *         set
+     */
+    protected final Button createSelectedItemButton(final String title, final String id,
+            final ISelectedEntityInvoker<BaseEntityModel<TableModelRowWithObject<T>>> invoker)
+    {
+        final Button button = createSelectedItemButton(title, invoker);
+        button.setId(id);
+        return button;
+    }
+
+    /**
+     * @return a button which is enabled only when one entity in the grid is selected. When button
+     *         is pressed, the specified invoker action is performed.
+     */
+    protected final Button createSelectedItemButton(final String title,
+            final ISelectedEntityInvoker<BaseEntityModel<TableModelRowWithObject<T>>> invoker)
+    {
+        final Button button = new Button(title, new SelectionListener<ButtonEvent>()
+            {
+                @Override
+                public void componentSelected(ButtonEvent ce)
+                {
+                    List<BaseEntityModel<TableModelRowWithObject<T>>> selectedItems =
+                            getSelectedItems();
+                    if (selectedItems.isEmpty() == false)
+                    {
+                        invoker.invoke(selectedItems.get(0), false);
+                    }
+                }
+            });
+        enableButtonOnSelectedItem(button);
+        return button;
+    }
+
+    /**
+     * @return a button which is enabled only when at least one entity in the grid is selected, and
+     *         with specified selection listener set.
+     */
+    protected final Button createSelectedItemsButton(final String title,
+            SelectionListener<ButtonEvent> listener)
+    {
+        Button button = new Button(title);
+        button.addSelectionListener(listener);
+        enableButtonOnSelectedItems(button);
+        return button;
+    }
+
+    /** adds given <var>button</var> to grid {@link PagingToolBar} */
+    protected final void addButton(Button button)
+    {
+        pagingToolbar.add(button);
+    }
+
+    /**
+     * Given <var>button</var> will be enabled only if at least one item is selected in the grid.
+     */
+    protected final void enableButtonOnSelectedItems(final Button button)
+    {
+        button.setEnabled(false);
+        addGridSelectionChangeListener(new Listener<SelectionChangedEvent<ModelData>>()
+            {
+                public void handleEvent(SelectionChangedEvent<ModelData> se)
+                {
+                    boolean enabled = se.getSelection().size() > 0;
+                    button.setEnabled(enabled);
+                }
+
+            });
+    }
+
+    /**
+     * Given <var>button</var> will be enabled only if exactly one item is selected in the grid.
+     */
+    protected final void enableButtonOnSelectedItem(final Button button)
+    {
+        button.setEnabled(false);
+        addGridSelectionChangeListener(new Listener<SelectionChangedEvent<ModelData>>()
+            {
+                public void handleEvent(SelectionChangedEvent<ModelData> se)
+                {
+                    boolean enabled = se.getSelection().size() == 1;
+                    button.setEnabled(enabled);
+                }
+
+            });
+    }
+
+    /**
+     * Given <var>button</var> will have title changed depending on number of items selected in the
+     * grid.
+     */
+    protected final void changeButtonTitleOnSelectedItems(final Button button,
+            final String noSelectedItemsTitle, final String selectedItemsTitle)
+    {
+        addGridSelectionChangeListener(new Listener<SelectionChangedEvent<ModelData>>()
+            {
+                public void handleEvent(SelectionChangedEvent<ModelData> se)
+                {
+                    boolean noSelected = se.getSelection().size() == 0;
+                    button.setText(noSelected ? noSelectedItemsTitle : selectedItemsTitle);
+                }
+
+            });
+    }
+
+    public void addGridSelectionChangeListener(Listener<SelectionChangedEvent<ModelData>> listener)
+    {
+        grid.getSelectionModel().addListener(Events.SelectionChange, listener);
+    }
+
+    /**
+     * Returns all models of selected items or an empty list if nothing selected.
+     */
+    public final List<BaseEntityModel<TableModelRowWithObject<T>>> getSelectedItems()
+    {
+        return grid.getSelectionModel().getSelectedItems();
+    }
+
+    /**
+     * Returns all base objects of selected items or an empty list if nothing selected.
+     */
+    protected final List<TableModelRowWithObject<T>> getSelectedBaseObjects()
+    {
+        List<BaseEntityModel<TableModelRowWithObject<T>>> items = getSelectedItems();
+        List<TableModelRowWithObject<T>> data = new ArrayList<TableModelRowWithObject<T>>();
+        for (BaseEntityModel<TableModelRowWithObject<T>> item : items)
+        {
+            data.add(item.getBaseObject());
+        }
+        return data;
+    }
+
+    protected final IDelegatedAction createRefreshGridAction()
+    {
+        return new IDelegatedAction()
+            {
+                public void execute()
+                {
+                    int id = log("execute refrish grid action");
+                    refresh();
+                    viewContext.logStop(id);
+                }
+            };
+    }
+
+    /** Refreshes grid and filters (resets filter selection) */
+    protected final void refreshGridWithFilters()
+    {
+        // N.B. -- The order in which things are refreshed and configured is significant
+
+        // export and config buttons are enabled when ListEntitiesCallback is complete
+        pagingToolbar.disableExportButton();
+        pagingToolbar.updateDefaultConfigButton(false);
+
+        // Need to reset filter fields *before* refreshing the grid so the list can
+        // be correctly retrieved
+        filterToolbar.resetFilterFields();
+        filterToolbar.resetFilterSelectionWithoutApply();
+
+        int id = log("refresh grid with filters");
+        refresh();
+        viewContext.logStop(id);
+
+        // Need to refresh the filter toolbar *after* refreshing the grid, because it
+        // has a dependency on information from the grid that gets updated with the refresh
+        filterToolbar.refresh();
+    }
+
+    protected final void updateDefaultRefreshButton()
+    {
+        boolean isEnabled = isRefreshEnabled();
+        pagingToolbar.updateDefaultRefreshButton(isEnabled);
+    }
+
+    /**
+     * Refreshes the grid.
+     * <p>
+     * Note that, doing so, the result set associated on the server side will be removed.
+     * </p>
+     */
+    protected final void refresh(boolean refreshColumnsDefinition)
+    {
+        refresh(null, refreshColumnsDefinition);
+    }
+
+    /**
+     * @param externalRefreshCallbackOrNull external class can define it's own refresh callback
+     *            method. It will be merged with the internal one.
+     */
+    protected final void refresh(final IDataRefreshCallback externalRefreshCallbackOrNull,
+            boolean refreshColumnsDefinition)
+    {
+        int id = log("refresh (refreshColumnsDefinition=" + refreshColumnsDefinition + ")");
+        pagingToolbar.updateDefaultRefreshButton(false);
+        debug("clean cache for refresh");
+        this.refreshCallback = createRefreshCallback(externalRefreshCallbackOrNull);
+        if (columnDefinitions == null || refreshColumnsDefinition)
+        {
+            recreateColumnModelAndRefreshColumnsWithFilters();
+        }
+        reloadData(createDisposeAndRefreshFetchMode());
+        viewContext.logStop(id);
+    }
+
+    private ResultSetFetchConfig<String> createDisposeAndRefreshFetchMode()
+    {
+        if (resultSetKeyOrNull != null)
+        {
+            return ResultSetFetchConfig.createClearComputeAndCache(resultSetKeyOrNull);
+        } else
+        {
+            return ResultSetFetchConfig.createComputeAndCache();
+        }
+    }
+
+    private ColumnDefsAndConfigs<TableModelRowWithObject<T>> createColumnDefsAndConfigs()
+    {
+        ColumnDefsAndConfigs<TableModelRowWithObject<T>> defsAndConfigs = createColumnsDefinition();
+        // add custom columns
+        List<GridCustomColumnInfo> customColumnsMetadata =
+                customColumnsMetadataProvider.getCustomColumnsMetadata();
+        if (customColumnsMetadata.size() > 0)
+        {
+            List<IColumnDefinitionUI<TableModelRowWithObject<T>>> customColumnsDefs =
+                    createCustomColumnDefinitions(customColumnsMetadata);
+            defsAndConfigs.addColumns(customColumnsDefs, viewContext);
+
+            for (GridCustomColumnInfo gridCustomColumnInfo : customColumnsMetadata)
+            {
+
+                DataTypeCode columnType = gridCustomColumnInfo.getDataType();
+                GridCellRenderer<BaseEntityModel<?>> columnRenderer = null;
+
+                if (DataTypeCode.REAL.equals(columnType))
+                {
+                    columnRenderer =
+                            new RealNumberRenderer(viewContext.getDisplaySettingsManager()
+                                    .getRealNumberFormatingParameters());
+
+                } else if (DataTypeCode.VARCHAR.equals(columnType))
+                {
+                    columnRenderer = new CustomColumnStringRenderer();
+                }
+
+                if (columnRenderer != null)
+                {
+                    defsAndConfigs.setGridCellRendererFor(gridCustomColumnInfo.getCode(),
+                            columnRenderer);
+                }
+            }
+        }
+
+        return defsAndConfigs;
+    }
+
+    protected final void recreateColumnModelAndRefreshColumnsWithFilters()
+    {
+        int logId = log("recreateColumnModelAndRefreshColumnsWithFilters");
+
+        ColumnDefsAndConfigs<TableModelRowWithObject<T>> defsAndConfigs =
+                createColumnDefsAndConfigs();
+
+        this.columnDefinitions = defsAndConfigs.getColumnDefs();
+        ColumnModel columnModel = createColumnModel(defsAndConfigs.getColumnConfigs());
+
+        refreshColumnsAndFilters(columnModel);
+
+        viewContext.logStop(logId);
+    }
+
+    private static <T> List<IColumnDefinitionUI<T>> createCustomColumnDefinitions(
+            List<GridCustomColumnInfo> customColumnsMetadata)
+    {
+        List<IColumnDefinitionUI<T>> defs = new ArrayList<IColumnDefinitionUI<T>>();
+        for (GridCustomColumnInfo columnMetadata : customColumnsMetadata)
+        {
+            IColumnDefinitionUI<T> colDef = new GridCustomColumnDefinition<T>(columnMetadata);
+            defs.add(colDef);
+        }
+        return defs;
+    }
+
+    private void refreshColumnsAndFilters(ColumnModel columnModel)
+    {
+        ColumnModel newColumnModel = columnModel;
+        GridDisplaySettings settings = tryApplyDisplaySettings(newColumnModel);
+        if (settings != null && settings.getColumnConfigs() != null)
+        {
+            newColumnModel = createColumnModel(settings.getColumnConfigs());
+            rebuildFiltersFromIds(settings.getFilteredColumnIds());
+        } else
+        {
+            filterToolbar.rebuildColumnFilters(getInitialFilters());
+        }
+        changeColumnModel(newColumnModel, settings != null ? settings.getSortField() : null,
+                settings != null ? settings.getSortDir() : null);
+    }
+
+    private void hideLoadingMask()
+    {
+        if (grid.isRendered() && grid.el() != null)
+        {
+            grid.el().unmask();
+        }
+    }
+
+    private GridDisplaySettings tryApplyDisplaySettings(ColumnModel columnModel)
+    {
+        List<IColumnDefinition<TableModelRowWithObject<T>>> initialFilters = getInitialFilters();
+        return viewContext.getDisplaySettingsManager().tryApplySettings(getGridDisplayTypeID(),
+                columnModel, extractColumnIds(initialFilters), getGridSortInfo());
+    }
+
+    private void reconfigureGrid(ColumnModel columnModelOfVisible)
+    {
+        List<Listener<?>> sortlisteners =
+                new ArrayList<Listener<?>>(grid.getListeners(Events.SortChange));
+        for (Listener<?> listener : sortlisteners)
+        {
+            grid.removeListener(Events.SortChange, listener);
+        }
+
+        grid.reconfigure(grid.getStore(), columnModelOfVisible);
+
+        for (Listener<?> listener : sortlisteners)
+        {
+            grid.addListener(Events.SortChange, listener);
+        }
+    }
+
+    private void changeColumnModel(ColumnModel columnModel, String sortField, SortDir sortDir)
+    {
+        fullColumnModel = columnModel;
+
+        int logId = log("grid reconfigure");
+        ColumnModel columnModelOfVisible = trimToVisibleColumns(columnModel);
+
+        if (sortDir != null && sortField != null)
+        {
+            pagingLoader.setSortDir(translate(sortDir));
+            pagingLoader.setSortField(sortField);
+        }
+        reconfigureGrid(columnModelOfVisible);
+
+        viewContext.logStop(logId);
+        registerGridSettingsChangesListener();
+        // add listeners of full column model to trimmed model
+        List<Listener<? extends BaseEvent>> listeners =
+                fullColumnModel.getListeners(Events.WidthChange);
+        for (Listener<? extends BaseEvent> listener : listeners)
+        {
+            columnModelOfVisible.addListener(Events.WidthChange, listener);
+            columnModelOfVisible.addListener(Events.ColumnMove, listener); // track drag&drop
+        }
+    }
+
+    private ColumnModel trimToVisibleColumns(ColumnModel columnModel)
+    {
+        int maxVisibleColumns = getWebClientConfiguration().getMaxVisibleColumns();
+        int counter = 0;
+        List<ColumnConfig> columns = new ArrayList<ColumnConfig>();
+        for (int i = 0, n = columnModel.getColumnCount(); i < n; i++)
+        {
+            ColumnConfig column = columnModel.getColumn(i);
+            if (column.isHidden() == false)
+            {
+                counter++;
+                if (counter <= maxVisibleColumns)
+                {
+                    columns.add(column);
+                } else
+                {
+                    column.setHidden(true);
+                }
+            }
+        }
+        if (counter > maxVisibleColumns)
+        {
+            saveColumnDisplaySettings(); // save changes made to full model
+            InfoConfig infoConfig =
+                    new InfoConfig(viewContext.getMessage(Dict.VISIBLE_COLUMNS_LIMITED_TITLE),
+                            viewContext.getMessage(Dict.VISIBLE_COLUMNS_LIMITED_MSG,
+                                    maxVisibleColumns, counter));
+            infoConfig.height = 100; // a bit higher
+            infoConfig.display = 5000; // 5s
+            Info.display(infoConfig);
+        }
+        ColumnModel trimmedModel = createColumnModel(columns);
+        return trimmedModel;
+    }
+
+    private void registerGridSettingsChangesListener()
+    {
+        viewContext.getDisplaySettingsManager().registerGridSettingsChangesListener(
+                getGridDisplayTypeID(), createDisplaySettingsUpdater());
+    }
+
+    // Refreshes the data, does not clear the cache. Does not change the column model.
+    // Default visibility so that friend classes can use -- should otherwise be considered private
+    void reloadData(ResultSetFetchConfig<String> resultSetFetchConfig)
+    {
+        if (pendingFetchManager.hasPendingFetch())
+        {
+            debug("Cannot reload the data with the mode '" + resultSetFetchConfig
+                    + "'; there is an unfinished request already: "
+                    + pendingFetchManager.tryTopPendingFetchConfig());
+            return;
+        }
+        pendingFetchManager.pushPendingFetchConfig(resultSetFetchConfig);
+        pagingLoader.load(0, PAGE_SIZE);
+    }
+
+    private IDisplaySettingsGetter createDisplaySettingsUpdater()
+    {
+        return new IDisplaySettingsGetter()
+            {
+                public ColumnModel getColumnModel()
+                {
+                    return TypedTableGrid.this.getFullColumnModel();
+                }
+
+                public List<String> getFilteredColumnIds()
+                {
+                    return filterToolbar.extractFilteredColumnIds();
+                }
+
+                public Object getModifier()
+                {
+                    return TypedTableGrid.this;
+                }
+
+                public SortInfo getSortState()
+                {
+                    return TypedTableGrid.this.getGridSortInfo();
+                }
+            };
+    }
+
+    // returns true if some filters have changed
+    // Default visibility so that friend classes can use -- should otherwise be considered private
+    boolean rebuildFiltersFromIds(List<String> filteredColumnIds)
+    {
+        List<IColumnDefinition<TableModelRowWithObject<T>>> filteredColumns =
+                getColumnDefinitions(filteredColumnIds);
+        return filterToolbar.rebuildColumnFilters(filteredColumns);
+    }
+
+    public List<IColumnDefinition<TableModelRowWithObject<T>>> getColumnDefinitions(
+            List<String> columnIds)
+    {
+        Map<String, IColumnDefinition<TableModelRowWithObject<T>>> colsMap =
+                asColumnIdMap(columnDefinitions);
+        List<IColumnDefinition<TableModelRowWithObject<T>>> columns =
+                new ArrayList<IColumnDefinition<TableModelRowWithObject<T>>>();
+        for (String columnId : columnIds)
+        {
+            IColumnDefinition<TableModelRowWithObject<T>> colDef = colsMap.get(columnId);
+            assert colDef != null : "Cannot find a column '" + columnId;
+            columns.add(colDef);
+        }
+        return columns;
+    }
+
+    protected final String createGridDisplayTypeID(String suffixOrNull)
+    {
+        if (displayTypeIDGenerator == null)
+        {
+            throw new IllegalStateException("Undefined display type ID generator.");
+        }
+        if (suffixOrNull == null)
+        {
+            return displayTypeIDGenerator.createID();
+        } else
+        {
+            return displayTypeIDGenerator.createID(suffixOrNull);
+        }
+    }
+
+    private IDataRefreshCallback createRefreshCallback(
+            IDataRefreshCallback externalRefreshCallbackOrNull)
+    {
+        IDataRefreshCallback internalCallback = createInternalPostRefreshCallback();
+        if (externalRefreshCallbackOrNull == null)
+        {
+            return internalCallback;
+        } else
+        {
+            return mergeCallbacks(internalCallback, externalRefreshCallbackOrNull);
+        }
+    }
+
+    private IDataRefreshCallback createInternalPostRefreshCallback()
+    {
+        return new IDataRefreshCallback()
+            {
+                public void postRefresh(boolean wasSuccessful)
+                {
+                    if (customColumnsMetadataProvider.getHasChangedAndSetFalse())
+                    {
+                        recreateColumnModelAndRefreshColumnsWithFilters();
+                    }
+
+                    updateDefaultRefreshButton();
+
+                    if (wasSuccessful)
+                    {
+                        hideLoadingMask();
+                        pagingToolbar.updateDefaultConfigButton(true);
+                        pagingToolbar.enableExportButton();
+                    }
+                }
+            };
+    }
+
+    private static IDataRefreshCallback mergeCallbacks(final IDataRefreshCallback c1,
+            final IDataRefreshCallback c2)
+    {
+        return new IDataRefreshCallback()
+            {
+                public void postRefresh(boolean wasSuccessful)
+                {
+                    c1.postRefresh(wasSuccessful);
+                    c2.postRefresh(wasSuccessful);
+                }
+            };
+    }
+
+    private List<IColumnDefinition<TableModelRowWithObject<T>>> getVisibleColumns(
+            Set<IColumnDefinition<TableModelRowWithObject<T>>> availableColumns)
+    {
+        Map<String, IColumnDefinition<TableModelRowWithObject<T>>> availableColumnsMap =
+                asColumnIdMap(availableColumns);
+        return getVisibleColumns(availableColumnsMap, fullColumnModel);
+    }
+
+    private void saveCacheKey(final String newResultSetKey)
+    {
+        resultSetKeyOrNull = newResultSetKey;
+        debug("saving new cache key");
+    }
+
+    private void disposeCache()
+    {
+        if (resultSetKeyOrNull != null)
+        {
+            removeResultSet(resultSetKeyOrNull);
+            resultSetKeyOrNull = null;
+        }
+    }
+
+    private void removeResultSet(String resultSetKey)
+    {
+        viewContext.getService().removeResultSet(resultSetKey,
+                new VoidAsyncCallback<Void>(viewContext));
+    }
+
+    /**
+     * Export always deals with data from the previous refresh operation
+     * 
+     * @param allColumns whether all columns should be exported
+     */
+    private void export(boolean allColumns)
+    {
+        export(allColumns, new ExportEntitiesCallback(viewContext));
+    }
+
+    /**
+     * Shows the dialog allowing to configure visibility and order of the table columns.
+     */
+    private void configureColumnSettings()
+    {
+        assert grid != null && grid.getColumnModel() != null : "Grid must be loaded";
+        ColumnSettingsConfigurer<T> columnSettingsConfigurer =
+                new ColumnSettingsConfigurer<T>(this, viewContext, filterToolbar,
+                        customColumnsMetadataProvider, resultSetKeyOrNull,
+                        pendingFetchManager.tryTopPendingFetchConfig());
+        columnSettingsConfigurer.showDialog();
+    }
+
+    // Default visibility so that friend classes can use -- should otherwise be considered private
+    void saveColumnDisplaySettings()
+    {
+        IDisplaySettingsGetter settingsUpdater = createDisplaySettingsUpdater();
+        viewContext.getDisplaySettingsManager().storeSettings(getGridDisplayTypeID(),
+                settingsUpdater, false);
+    }
+
+    // @Private - for tests
+    public final void export(boolean allColumns, final AbstractAsyncCallback<String> callback)
+    {
+        final TableExportCriteria<TableModelRowWithObject<T>> exportCriteria =
+                createTableExportCriteria(allColumns);
+
+        prepareExportEntities(exportCriteria, callback);
+    }
+
+    // for visible columns
+    protected final TableExportCriteria<TableModelRowWithObject<T>> createTableExportCriteria()
+    {
+        return createTableExportCriteria(false);
+    }
+
+    protected final TableExportCriteria<TableModelRowWithObject<T>> createTableExportCriteria(
+            boolean allColumns)
+    {
+        assert columnDefinitions != null : "refresh before exporting!";
+        assert resultSetKeyOrNull != null : "refresh before exporting, resultSetKey is null!";
+
+        final List<IColumnDefinition<TableModelRowWithObject<T>>> columnDefs =
+                allColumns ? new ArrayList<IColumnDefinition<TableModelRowWithObject<T>>>(
+                        columnDefinitions) : getVisibleColumns(columnDefinitions);
+        SortInfo sortInfo = getGridSortInfo();
+        final TableExportCriteria<TableModelRowWithObject<T>> exportCriteria =
+                new TableExportCriteria<TableModelRowWithObject<T>>(resultSetKeyOrNull, sortInfo,
+                        filterToolbar.getFilters(), columnDefs, columnDefinitions,
+                        getGridDisplayTypeID());
+        return exportCriteria;
+    }
+
+    // returns info about sorting in current grid
+    public SortInfo getGridSortInfo()
+    {
+        ListStore<BaseEntityModel<TableModelRowWithObject<T>>> store = grid.getStore();
+        return translateSortInfo(store.getSortField(), store.getSortDir());
+    }
+
+    /** @return the number of all objects cached in the browser */
+    public int getTotalCount()
+    {
+        return pagingToolbar.getTotalCount();
+    }
+
+    // Default visibility so that friend classes can use -- should otherwise be considered private
+    void refreshColumnsSettings()
+    {
+        grid.setLoadMask(false);
+        grid.getView().refresh(true);
+        grid.setLoadMask(true);
+    }
+
+    protected final void addEntityOperationsLabel()
+    {
+        pagingToolbar.addEntityOperationsLabel();
+    }
+
+    protected final void addEntityOperationsSeparator()
+    {
+        pagingToolbar.add(new SeparatorToolItem());
+    }
+
+    protected final GridCellRenderer<BaseEntityModel<?>> createMultilineStringCellRenderer()
+    {
+        return new MultilineStringCellRenderer();
+    }
+
+    @SuppressWarnings("deprecation")
+    protected GridCellRenderer<BaseEntityModel<?>> createInternalLinkCellRenderer()
+    {
+        // NOTE: this renderer doesn't support special rendering of deleted entities
+        return LinkRenderer.createLinkRenderer();
+    }
+
+    protected WebClientConfiguration getWebClientConfiguration()
+    {
+        return viewContext.getModel().getApplicationInfo().getWebClientConfiguration();
+    }
+
+    // ------- generic static helpers
+
+    private static <T> List<String> extractColumnIds(List<IColumnDefinition<T>> columns)
+    {
+        List<String> columnsIds = new ArrayList<String>();
+        for (IColumnDefinition<T> column : columns)
+        {
+            columnsIds.add(column.getIdentifier());
+        }
+        return columnsIds;
+    }
+
+    // Default visibility so that friend classes can use -- should otherwise be considered private
+    public static List<ColumnDataModel> createColumnsSettingsModel(ColumnModel cm,
+            List<String> filteredColumnsIds)
+    {
+        Set<String> filteredColumnsMap = new HashSet<String>(filteredColumnsIds);
+        int cols = cm.getColumnCount();
+        List<ColumnDataModel> list = new ArrayList<ColumnDataModel>();
+        for (int i = 0; i < cols; i++)
+        {
+            if (cm.getColumnHeader(i) == null || cm.getColumnHeader(i).equals("") || cm.isFixed(i))
+            {
+                continue;
+            }
+            String columnId = cm.getColumnId(i);
+            boolean isVisible = cm.isHidden(i) == false;
+            boolean hasFilter = filteredColumnsMap.contains(columnId);
+            list.add(new ColumnDataModel(cm.getColumnHeader(i), isVisible, hasFilter, columnId));
+        }
+        return list;
+    }
+
+    private static ContentPanel createEmptyContentPanel()
+    {
+        final ContentPanel contentPanel = new ContentPanel();
+        contentPanel.setBorders(false);
+        contentPanel.setBodyBorder(false);
+        contentPanel.setLayout(new FitLayout());
+        return contentPanel;
+    }
+
+    // creates filter and paging toolbars
+    private static <T> LayoutContainer createBottomToolbars(final Container<?> parentContainer,
+            ToolBar pagingToolbar)
+    {
+        LayoutContainer bottomToolbars = new ContainerKeeper(parentContainer);
+        bottomToolbars.setMonitorWindowResize(true);
+        bottomToolbars.setLayout(new RowLayout(com.extjs.gxt.ui.client.Style.Orientation.VERTICAL));
+        // Adding paging toolbar before data are loaded fixes problems with the toolbar not visible
+        // if user quickly changes tab / hides section before it is layouted. On the other hand
+        // it is slower than adding it after requesting server for data.
+        bottomToolbars.add(pagingToolbar, new RowData(1, -1));
+        // filter toolbar is added on request
+        return bottomToolbars;
+    }
+
+    private static final class ContainerKeeper extends LayoutContainer
+    {
+        private final Container<?> parentContainer;
+
+        private ContainerKeeper(Container<?> parentContainer)
+        {
+            this.parentContainer = parentContainer;
+        }
+
+        @Override
+        protected void onWindowResize(int aWidth, int aHeight)
+        {
+            super.onWindowResize(aWidth, aHeight);
+            if (isVisible())
+            {
+                this.setWidth(parentContainer.getWidth());
+            }
+        }
+    }
+
+    private Grid<BaseEntityModel<TableModelRowWithObject<T>>> createGrid(
+            PagingLoader<PagingLoadResult<BaseEntityModel<TableModelRowWithObject<T>>>> dataLoader,
+            String gridId)
+    {
+
+        ListStore<BaseEntityModel<TableModelRowWithObject<T>>> listStore =
+                new ListStore<BaseEntityModel<TableModelRowWithObject<T>>>(dataLoader);
+        ColumnModel columnModel = createColumnModel(new ArrayList<ColumnConfig>());
+        EditorGrid<BaseEntityModel<TableModelRowWithObject<T>>> editorGrid =
+                new EditorGrid<BaseEntityModel<TableModelRowWithObject<T>>>(listStore, columnModel);
+        editorGrid.setId(gridId);
+        editorGrid.setLoadMask(true);
+        editorGrid
+                .setSelectionModel(new GridSelectionModel<BaseEntityModel<TableModelRowWithObject<T>>>());
+        editorGrid.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
+        editorGrid.setView(new ExtendedGridView());
+        editorGrid.setStripeRows(true);
+        editorGrid.setColumnReordering(true);
+        editorGrid.setClicksToEdit(ClicksToEdit.TWO);
+        editorGrid.addListener(Events.BeforeEdit,
+                new Listener<GridEvent<BaseEntityModel<TableModelRowWithObject<T>>>>()
+                    {
+                        public void handleEvent(
+                                GridEvent<BaseEntityModel<TableModelRowWithObject<T>>> event)
+                        {
+                            if (viewContext.isSimpleOrEmbeddedMode())
+                            {
+                                MessageBox
+                                        .info("Not Allowed",
+                                                "Sorry, table cell editing is not allowed in current viewing mode",
+                                                null);
+                                event.setCancelled(true);
+                            } else
+                            {
+                                BaseEntityModel<TableModelRowWithObject<T>> model =
+                                        event.getModel();
+                                String columnID = event.getProperty();
+                                boolean editable = isEditable(model, columnID);
+                                if (editable == false)
+                                {
+                                    showNonEditableTableCellMessage(model, columnID);
+                                }
+                                event.setCancelled(editable == false);
+                            }
+                        }
+                    });
+        editorGrid.addListener(Events.AfterEdit,
+                new Listener<GridEvent<BaseEntityModel<TableModelRowWithObject<T>>>>()
+                    {
+                        public void handleEvent(
+                                GridEvent<BaseEntityModel<TableModelRowWithObject<T>>> event)
+                        {
+                            BaseEntityModel<TableModelRowWithObject<T>> model = event.getModel();
+                            String columnID = event.getProperty();
+                            Object value = event.getValue();
+                            String newValueNotNull = StringUtils.toStringEmptyIfNull(value);
+                            String oldValueNotNull =
+                                    StringUtils.toStringEmptyIfNull(event.getStartValue());
+                            if (oldValueNotNull.equals(newValueNotNull))
+                            {
+                                event.setCancelled(true);
+                            } else
+                            {
+                                showModificationsBar();
+                                if (value instanceof VocabularyTerm)
+                                {
+                                    VocabularyTerm term = (VocabularyTerm) value;
+                                    value = term.getCode();
+                                }
+                                tableModificationsManager.handleEditingEvent(model, columnID,
+                                        StringUtils.toStringOrNull(value));
+                            }
+                        }
+                    });
+        editorGrid.addListener(Events.SortChange, new Listener<BaseEvent>()
+            {
+                public void handleEvent(BaseEvent be)
+                {
+                    saveColumnDisplaySettings();
+                }
+            });
+
+        return editorGrid;
+    }
+
+    /**
+     * Returns <code>true</code> if cell specified by model and column ID is editable. Default
+     * implementation false.
      */
-    private static final int MAX_SHOWN_COLUMNS = 200;
+    protected boolean isEditable(BaseEntityModel<TableModelRowWithObject<T>> model, String columnID)
+    {
+        return false;
+    }
 
     /**
-     * If user selected some entities in given browser first a dialog is shown where he can select
-     * between showing data sets related to selected/displayed entities. Then a tab is displayed
-     * where these related data sets are listed.<br>
-     * <br>
-     * If no entities were selected in given browser the tab is displayed where data sets related to
-     * all entities displayed in the grid are listed.
+     * Shows a message that the table cell of specified column and row (model) isn't editable.
      */
-    protected static final <E extends IEntityInformationHolder> void showRelatedDataSets(
-            final IViewContext<ICommonClientServiceAsync> viewContext,
-            final TypedTableGrid<E> browser)
+    protected void showNonEditableTableCellMessage(
+            BaseEntityModel<TableModelRowWithObject<T>> model, String columnID)
     {
-        final List<TableModelRowWithObject<E>> selectedEntities = browser.getSelectedBaseObjects();
-        final TableExportCriteria<TableModelRowWithObject<E>> displayedEntities =
-                browser.createTableExportCriteria();
-        if (selectedEntities.isEmpty())
-        {
-            // no entity selected - show datasets related to all displayed
-            RelatedDataSetCriteria<E> relatedCriteria =
-                    RelatedDataSetCriteria.<E> createDisplayedEntities(displayedEntities);
-            ShowRelatedDatasetsDialog.showRelatedDatasetsTab(viewContext, relatedCriteria);
-        } else
+        MessageBox.info("Not Editable", "Sorry, this table cell isn't editable", null);
+    }
+
+    /**
+     * Tries to return the property of specified properties holder which is specified by the
+     * property column name without a prefix like <code>property-</code> but with prefix which
+     * distinguishes internal from externally name space.
+     */
+    protected IEntityProperty tryGetProperty(IEntityPropertiesHolder propertiesHolder,
+            String propertyColumnNameWithoutPrefix)
+    {
+        String propertyTypeCode =
+                CodeConverter.getPropertyTypeCode(propertyColumnNameWithoutPrefix);
+        List<IEntityProperty> properties = propertiesHolder.getProperties();
+        for (IEntityProperty property : properties)
         {
-            // > 0 entity selected - show dialog with all/selected radio
-            new ShowRelatedDatasetsDialog<E>(viewContext, selectedEntities, displayedEntities,
-                    browser.getTotalCount()).show();
+            if (property.getPropertyType().getCode().equals(propertyTypeCode))
+            {
+                return property;
+            }
         }
+        return null;
     }
 
-    private final class CellListenerAndLinkGenerator implements ICellListenerAndLinkGenerator<T>
+    // this should be the only place where we create the grid column model.
+    private static ColumnModel createColumnModel(List<ColumnConfig> columConfigs)
     {
-        private final EntityKind entityKind;
+        return new ColumnModel(columConfigs);
+    }
 
-        private final TableModelColumnHeader header;
+    public ColumnModel getFullColumnModel()
+    {
+        return fullColumnModel;
+    }
 
-        private CellListenerAndLinkGenerator(EntityKind entityKind, TableModelColumnHeader header)
+    private static final class ExportEntitiesCallback extends AbstractAsyncCallback<String>
+    {
+        public ExportEntitiesCallback(final IViewContext<ICommonClientServiceAsync> viewContext)
         {
-            this.entityKind = entityKind;
-            this.header = header;
+            super(viewContext);
         }
 
-        public String tryGetLink(T entity, final ISerializableComparable value)
+        @Override
+        protected void process(final String exportDataKey)
         {
-            if (value == null || value.toString().length() == 0)
+            final URLMethodWithParameters methodWithParameters =
+                    new URLMethodWithParameters(
+                            GenericConstants.FILE_EXPORTER_DOWNLOAD_SERVLET_NAME);
+            methodWithParameters.addParameter(GenericConstants.EXPORT_CRITERIA_KEY_PARAMETER,
+                    exportDataKey);
+            WindowUtils.openWindow(methodWithParameters.toString());
+        }
+
+    }
+
+    // creates a map to quickly find a definition by its identifier
+    private static <T> Map<String, IColumnDefinition<T>> asColumnIdMap(
+            final Set<IColumnDefinition<T>> defs)
+    {
+        final Map<String, IColumnDefinition<T>> map = new HashMap<String, IColumnDefinition<T>>();
+        for (final IColumnDefinition<T> def : defs)
+        {
+            map.put(def.getIdentifier(), def);
+        }
+        return map;
+    }
+
+    /**
+     * @param availableColumns map of all available columns definitions.
+     * @param columnModel describes the visual properties of the columns. Connected with
+     *            availableColumns by column id.
+     * @return list of columns definitions for those columns which are currently shown
+     */
+    private static <T/* column definition */> List<T> getVisibleColumns(
+            final Map<String, T> availableColumns, final ColumnModel columnModel)
+    {
+        final List<T> selectedColumnDefs = new ArrayList<T>();
+        final int columnCount = columnModel.getColumnCount();
+        for (int i = 0; i < columnCount; i++)
+        {
+            if (columnModel.isHidden(i) == false)
             {
-                return null;
+                final String columnId = columnModel.getColumnId(i);
+                selectedColumnDefs.add(availableColumns.get(columnId));
             }
-            if (value instanceof EntityTableCell)
+        }
+        return selectedColumnDefs;
+    }
+
+    /** Creates callback that refreshes the grid. */
+    protected final AbstractAsyncCallback<Void> createRefreshCallback(
+            IBrowserGridActionInvoker invoker)
+    {
+        return new RefreshCallback(viewContext, invoker);
+    }
+
+    /** Callback that refreshes the grid. */
+    private static final class RefreshCallback extends AbstractAsyncCallback<Void>
+    {
+        private final IBrowserGridActionInvoker invoker;
+
+        public RefreshCallback(IViewContext<?> viewContext, IBrowserGridActionInvoker invoker)
+        {
+            super(viewContext);
+            this.invoker = invoker;
+        }
+
+        @Override
+        protected void process(Void result)
+        {
+            invoker.refresh();
+        }
+    }
+
+    /** {@link SelectionListener} that creates a dialog with selected data items. */
+    protected abstract class AbstractCreateDialogListener extends SelectionListener<ButtonEvent>
+    {
+        @Override
+        public void componentSelected(ButtonEvent ce)
+        {
+            List<TableModelRowWithObject<T>> data = getSelectedBaseObjects();
+            IBrowserGridActionInvoker invoker = asActionInvoker();
+            if (validateSelectedData(data))
             {
-                EntityTableCell entityTableCell = (EntityTableCell) value;
-                if (entityTableCell.isMissing() || entityTableCell.isFake())
-                {
-                    return null;
-                }
-                String permId = entityTableCell.getPermId();
-                if (entityTableCell.getEntityKind() == EntityKind.MATERIAL)
-                {
-                    return LinkExtractor.tryExtract(MaterialIdentifier.tryParseIdentifier(permId));
-                } else
-                {
-                    return LinkExtractor.createPermlink(entityTableCell.getEntityKind(), permId);
-                }
+                createDialog(data, invoker).show();
+            }
+        }
+
+        /**
+         * If specified data is valid returns true, otherwise returns false. Dialog will be shown
+         * only if this method returns true. Default implementation always returns true.
+         */
+        protected boolean validateSelectedData(List<TableModelRowWithObject<T>> data)
+        {
+            return true;
+        }
+
+        protected abstract Dialog createDialog(List<TableModelRowWithObject<T>> data,
+                IBrowserGridActionInvoker invoker);
+    }
+
+    //
+    // Table Modifications
+    //
+
+    /**
+     * Apply specified modifications to the model. Should be overriden by subclasses. Default
+     * implementation does nothing.
+     */
+    protected void applyModifications(BaseEntityModel<TableModelRowWithObject<T>> model,
+            List<IModification> modifications)
+    {
+
+    }
+
+    private class TableModificationsManager implements
+            ITableModificationsManager<BaseEntityModel<TableModelRowWithObject<T>>>
+    {
+        private final Map<BaseEntityModel<TableModelRowWithObject<T>>, List<IModification>> modificationsByModel =
+                new LinkedHashMap<BaseEntityModel<TableModelRowWithObject<T>>, List<IModification>>();
+
+        private final Map<BaseEntityModel<TableModelRowWithObject<T>>, String> failedModifications =
+                new HashMap<BaseEntityModel<TableModelRowWithObject<T>>, String>();
 
+        private int finishedModifications = 0;
+
+        private IDelegatedAction afterSaveActionOrNull;
+
+        //
+        // ITableModificationsManager
+        //
+
+        // @Override
+        public boolean isTableDirty()
+        {
+            return isApplyModificationsComplete() == false;
+        }
+
+        public void saveModifications()
+        {
+            saveModifications(null);
+        }
+
+        private void setAfterSaveAction(IDelegatedAction afterSaveAction)
+        {
+            this.afterSaveActionOrNull = afterSaveAction;
+        }
+
+        public void saveModifications(IDelegatedAction afterSaveAction)
+        {
+            setAfterSaveAction(afterSaveAction);
+            finishedModifications = 0;
+            for (Entry<BaseEntityModel<TableModelRowWithObject<T>>, List<IModification>> entry : modificationsByModel
+                    .entrySet())
+            {
+                applyModifications(entry.getKey(), entry.getValue());
             }
-            return LinkExtractor.createPermlink(entityKind, value.toString());
         }
 
-        public void handle(TableModelRowWithObject<T> rowItem, boolean specialKeyPressed)
+        public void cancelModifications()
         {
-            ISerializableComparable cellValue = rowItem.getValues().get(header.getIndex());
-            if (cellValue instanceof EntityTableCell)
+            clearModifications();
+            grid.getStore().rejectChanges();
+            refresh(); // WORKAROUND remove this refresh after LMS-2397 is resolved
+        }
+
+        public void handleEditingEvent(BaseEntityModel<TableModelRowWithObject<T>> model,
+                String columnID, String newValueOrNull)
+        {
+            List<IModification> modificationsForModel = modificationsByModel.get(model);
+            if (modificationsForModel == null)
             {
-                EntityTableCell entityTableCell = (EntityTableCell) cellValue;
-                String permId = entityTableCell.getPermId();
-                if (entityTableCell.getEntityKind() == EntityKind.MATERIAL)
-                {
-                    MaterialIdentifier materialIdentifier =
-                            MaterialIdentifier.tryParseIdentifier(permId);
-                    OpenEntityDetailsTabHelper.open(viewContext, materialIdentifier,
-                            specialKeyPressed);
-                } else
+                modificationsForModel = new ArrayList<IModification>();
+                modificationsByModel.put(model, modificationsForModel);
+            }
+            modificationsForModel.add(new Modification(columnID, newValueOrNull));
+        }
+
+        public AsyncCallback<EntityPropertyUpdatesResult> createApplyModificationsCallback(
+                final BaseEntityModel<TableModelRowWithObject<T>> model,
+                final List<IModification> modifications)
+        {
+            return new AbstractAsyncCallback<EntityPropertyUpdatesResult>(viewContext)
                 {
-                    OpenEntityDetailsTabHelper.open(viewContext, entityTableCell.getEntityKind(),
-                            permId, specialKeyPressed);
-                }
+                    @Override
+                    protected void process(EntityPropertyUpdatesResult result)
+                    {
+                        finishedModifications++;
+                        String errorMessage = result.tryGetErrorMessage();
+                        if (errorMessage != null)
+                        {
+                            handleError(errorMessage);
+                        }
+                        if (isApplyModificationsComplete())
+                        {
+                            onApplyModificationsComplete();
+                        }
+                    }
+
+                    @Override
+                    public void finishOnFailure(Throwable caught)
+                    {
+                        finishedModifications++;
+                        handleError(caught.getMessage());
+                        if (isApplyModificationsComplete())
+                        {
+                            onApplyModificationsComplete();
+                        }
+                    }
+
+                    private void handleError(String errorMessage)
+                    {
+                        failedModifications.put(model, errorMessage);
+                    }
+                };
+        }
+
+        //
+
+        private boolean isApplyModificationsComplete()
+        {
+            return finishedModifications == modificationsByModel.size();
+        }
+
+        private void onApplyModificationsComplete()
+        {
+            if (failedModifications.size() > 0)
+            {
+                String failureTitle =
+                        (failedModifications.size() == modificationsByModel.size()) ? "Operation failed"
+                                : "Operation partly failed";
+                String failureReport = createFailedModificationsReport();
+                MessageBox.alert(failureTitle, failureReport, null);
+                refresh();
             } else
             {
-                OpenEntityDetailsTabHelper.open(viewContext, entityKind, cellValue.toString(),
-                        specialKeyPressed);
+                GWTUtils.displayInfo("All modifications successfully applied.");
+                grid.getStore().commitChanges(); // no need to refresh - everything should be valid
+            }
+            clearModifications();
+            if (afterSaveActionOrNull != null)
+            {
+                afterSaveActionOrNull.execute();
             }
         }
-    }
 
-    private final Map<String, ICellListenerAndLinkGenerator<T>> listenerLinkGenerators =
-            new HashMap<String, ICellListenerAndLinkGenerator<T>>();
-
-    private List<TableModelColumnHeader> headers;
+        private String createFailedModificationsReport()
+        {
+            assert failedModifications.size() > 0;
+            StringBuilder result = new StringBuilder();
+            result.append("Modifications of " + failedModifications.size() + " entities failed:");
+            for (String error : failedModifications.values())
+            {
+                result.append("<br/>- " + error);
+            }
+            return result.toString();
+        }
 
-    private List<IColumnDefinitionUI<TableModelRowWithObject<T>>> columnUIDefinitions;
+        private void clearModifications()
+        {
+            finishedModifications = 0;
+            failedModifications.clear();
+            modificationsByModel.clear();
+            hideModificationsBar();
+        }
 
-    private String downloadURL;
+    }
 
-    private Map<String, IColumnDefinition<TableModelRowWithObject<T>>> columnDefinitions;
+    /** Toolbar for handling table modifications */
+    static class TableModificationsToolbar extends ToolBar
+    {
 
-    private String currentGridDisplayTypeID;
+        public TableModificationsToolbar(final IMessageProvider messageProvider,
+                final ITableModificationsManager<?> manager)
+        {
+            add(new Label(messageProvider.getMessage(Dict.TABLE_MODIFICATIONS)));
 
-    private List<IColumnDefinitionUI<TableModelRowWithObject<T>>> visibleColDefinitions;
+            final AbstractImagePrototype confirmIcon =
+                    AbstractImagePrototype.create(IMAGE_BUNDLE.getConfirmIcon());
+            final AbstractImagePrototype cancelIcon =
+                    AbstractImagePrototype.create(IMAGE_BUNDLE.getCancelIcon());
+            add(new Button("Save", confirmIcon, new SelectionListener<ButtonEvent>()
+                {
+                    @Override
+                    public void componentSelected(ButtonEvent be)
+                    {
+                        manager.saveModifications();
+                    }
+                }));
+            add(new Button("Cancel", cancelIcon, new SelectionListener<ButtonEvent>()
+                {
+                    @Override
+                    public void componentSelected(ButtonEvent be)
+                    {
+                        manager.cancelModifications();
+                    }
+                }));
+        }
 
-    protected TypedTableGrid(IViewContext<ICommonClientServiceAsync> viewContext, String browserId,
-            boolean refreshAutomatically, IDisplayTypeIDGenerator displayTypeIDGenerator)
-    {
-        super(viewContext, browserId + GRID_POSTFIX, refreshAutomatically, displayTypeIDGenerator);
-        setId(browserId);
     }
 
-    protected TypedTableGrid(IViewContext<ICommonClientServiceAsync> viewContext, String browserId,
-            IDisplayTypeIDGenerator displayTypeIDGenerator)
+    /**
+     * To be subclassed if columns in the grid depend on the internal grid configuration.
+     * 
+     * @return id at which grid display settings are saved.
+     */
+    public String getGridDisplayTypeID()
     {
-        super(viewContext, browserId + GRID_POSTFIX, displayTypeIDGenerator);
-        setId(browserId);
+        return createGridDisplayTypeID(null);
     }
 
     protected void setDownloadURL(String downloadURL)
@@ -214,17 +2450,22 @@ public abstract class TypedTableGrid<T extends Serializable>
             DefaultResultSetConfig<String, TableModelRowWithObject<T>> resultSetConfig,
             AbstractAsyncCallback<TypedTableResultSet<T>> callback);
 
-    @Override
+    /**
+     * Creates a column model with all available columns from scratch without taking user settings
+     * into account.
+     * 
+     * @return definition of all the columns in the grid
+     */
     protected ColumnDefsAndConfigs<TableModelRowWithObject<T>> createColumnsDefinition()
     {
         ColumnDefsAndConfigs<TableModelRowWithObject<T>> definitions =
                 ColumnDefsAndConfigs.create(createColDefinitions(), viewContext);
         Set<IColumnDefinition<TableModelRowWithObject<T>>> columnDefs = definitions.getColumnDefs();
-        columnDefinitions = new HashMap<String, IColumnDefinition<TableModelRowWithObject<T>>>();
+        columnDefinitionsMap = new HashMap<String, IColumnDefinition<TableModelRowWithObject<T>>>();
         for (IColumnDefinition<TableModelRowWithObject<T>> definition : columnDefs)
         {
             String identifier = definition.getIdentifier();
-            columnDefinitions.put(identifier, definition);
+            columnDefinitionsMap.put(identifier, definition);
         }
         if (headers != null)
         {
@@ -276,7 +2517,13 @@ public abstract class TypedTableGrid<T extends Serializable>
         }
     }
 
-    @Override
+    /**
+     * Initializes creation of model from received data. This is a hook method called before {
+     * {@link #createModel(GridRowModel)} is invoked for all rows. This implementation does nothing.
+     * Subclasses usually override this method by creating an instance attribute which holds a list
+     * of visible column definitions. This speeds up invocation of
+     * {@link #createModel(GridRowModel)}.
+     */
     protected void initializeModelCreation()
     {
         Set<String> visibleColumnIds = getIDsOfVisibleColumns();
@@ -292,7 +2539,7 @@ public abstract class TypedTableGrid<T extends Serializable>
         }
     }
 
-    @Override
+    /** Converts specified entity into a grid row model */
     protected BaseEntityModel<TableModelRowWithObject<T>> createModel(
             GridRowModel<TableModelRowWithObject<T>> entity)
     {
@@ -368,7 +2615,6 @@ public abstract class TypedTableGrid<T extends Serializable>
         return getId() + "_" + columnID;
     }
 
-    @Override
     protected void listEntities(
             final DefaultResultSetConfig<String, TableModelRowWithObject<T>> resultSetConfig,
             final AbstractAsyncCallback<ResultSet<TableModelRowWithObject<T>>> callback)
@@ -399,7 +2645,7 @@ public abstract class TypedTableGrid<T extends Serializable>
         currentGridDisplayTypeID = getGridDisplayTypeID();
     }
 
-    @Override
+    /** @return should the refresh button be enabled? */
     protected boolean isRefreshEnabled()
     {
         return true;
@@ -412,20 +2658,21 @@ public abstract class TypedTableGrid<T extends Serializable>
      * set of display settings. Thus column models and filters should be refreshed before data
      * loading.
      */
-    @Override
     protected void refresh()
     {
         String gridDisplayTypeID = getGridDisplayTypeID();
         refresh(gridDisplayTypeID.equals(currentGridDisplayTypeID) == false);
     }
 
-    @Override
+    /**
+     * Shows the detail view for the specified entity
+     */
     protected void showEntityViewer(TableModelRowWithObject<T> entity, boolean editMode,
             boolean inBackground)
     {
     }
 
-    @Override
+    /** @return on which fields filters should be switched on by default? */
     protected List<IColumnDefinition<TableModelRowWithObject<T>>> getInitialFilters()
     {
 
@@ -434,7 +2681,7 @@ public abstract class TypedTableGrid<T extends Serializable>
         List<String> ids = getColumnIdsOfFilters();
         for (String id : ids)
         {
-            IColumnDefinition<TableModelRowWithObject<T>> definition = columnDefinitions.get(id);
+            IColumnDefinition<TableModelRowWithObject<T>> definition = columnDefinitionsMap.get(id);
             if (definition != null)
             {
                 definitions.add(definition);
@@ -460,7 +2707,7 @@ public abstract class TypedTableGrid<T extends Serializable>
 
     protected List<T> getContainedGridElements()
     {
-        List<TableModelRowWithObject<T>> wrappedElements = super.getGridElements();
+        List<TableModelRowWithObject<T>> wrappedElements = getGridElements();
         List<T> elements = new ArrayList<T>();
         for (TableModelRowWithObject<T> wrappedElement : wrappedElements)
         {
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/AbstractBrowserGrid.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/AbstractBrowserGrid.java
deleted file mode 100644
index c2778327d27..00000000000
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/AbstractBrowserGrid.java
+++ /dev/null
@@ -1,2283 +0,0 @@
-/*
- * Copyright 2009 ETH Zuerich, CISD
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-
-import com.extjs.gxt.ui.client.GXT;
-import com.extjs.gxt.ui.client.Style.Orientation;
-import com.extjs.gxt.ui.client.Style.SelectionMode;
-import com.extjs.gxt.ui.client.data.BasePagingLoadConfig;
-import com.extjs.gxt.ui.client.data.BasePagingLoadResult;
-import com.extjs.gxt.ui.client.data.BasePagingLoader;
-import com.extjs.gxt.ui.client.data.Loader;
-import com.extjs.gxt.ui.client.data.ModelData;
-import com.extjs.gxt.ui.client.data.PagingLoadConfig;
-import com.extjs.gxt.ui.client.data.PagingLoadResult;
-import com.extjs.gxt.ui.client.data.PagingLoader;
-import com.extjs.gxt.ui.client.data.RpcProxy;
-import com.extjs.gxt.ui.client.event.BaseEvent;
-import com.extjs.gxt.ui.client.event.ButtonEvent;
-import com.extjs.gxt.ui.client.event.Events;
-import com.extjs.gxt.ui.client.event.GridEvent;
-import com.extjs.gxt.ui.client.event.Listener;
-import com.extjs.gxt.ui.client.event.MessageBoxEvent;
-import com.extjs.gxt.ui.client.event.SelectionChangedEvent;
-import com.extjs.gxt.ui.client.event.SelectionListener;
-import com.extjs.gxt.ui.client.store.ListStore;
-import com.extjs.gxt.ui.client.widget.Component;
-import com.extjs.gxt.ui.client.widget.Container;
-import com.extjs.gxt.ui.client.widget.ContentPanel;
-import com.extjs.gxt.ui.client.widget.Dialog;
-import com.extjs.gxt.ui.client.widget.Info;
-import com.extjs.gxt.ui.client.widget.InfoConfig;
-import com.extjs.gxt.ui.client.widget.Label;
-import com.extjs.gxt.ui.client.widget.LayoutContainer;
-import com.extjs.gxt.ui.client.widget.MessageBox;
-import com.extjs.gxt.ui.client.widget.button.Button;
-import com.extjs.gxt.ui.client.widget.grid.ColumnConfig;
-import com.extjs.gxt.ui.client.widget.grid.ColumnModel;
-import com.extjs.gxt.ui.client.widget.grid.EditorGrid;
-import com.extjs.gxt.ui.client.widget.grid.EditorGrid.ClicksToEdit;
-import com.extjs.gxt.ui.client.widget.grid.Grid;
-import com.extjs.gxt.ui.client.widget.grid.GridCellRenderer;
-import com.extjs.gxt.ui.client.widget.grid.GridSelectionModel;
-import com.extjs.gxt.ui.client.widget.layout.FitLayout;
-import com.extjs.gxt.ui.client.widget.layout.RowData;
-import com.extjs.gxt.ui.client.widget.layout.RowLayout;
-import com.extjs.gxt.ui.client.widget.menu.Menu;
-import com.extjs.gxt.ui.client.widget.toolbar.PagingToolBar;
-import com.extjs.gxt.ui.client.widget.toolbar.SeparatorToolItem;
-import com.extjs.gxt.ui.client.widget.toolbar.ToolBar;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.user.client.Element;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.AbstractImagePrototype;
-
-import ch.systemsx.cisd.common.shared.basic.utils.StringUtils;
-import ch.systemsx.cisd.openbis.generic.client.web.client.ICommonClientServiceAsync;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.AbstractAsyncCallback;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.Dict;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.GenericConstants;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.IGenericImageBundle;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.IViewContext;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.VoidAsyncCallback;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.framework.AbstractTabItemFactory;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.framework.DispatcherHelper;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.framework.DisplaySettingsManager;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.framework.DisplaySettingsManager.GridDisplaySettings;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.framework.IDatabaseModificationObserver;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.framework.IDisplaySettingsGetter;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.framework.IDisplayTypeIDGenerator;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.model.BaseEntityModel;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.plugin.IClientPlugin;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.plugin.IClientPluginFactory;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.renderer.LinkRenderer;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.renderer.MultilineStringCellRenderer;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.renderer.RealNumberRenderer;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.renderer.customcolumn.core.CustomColumnStringRenderer;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.ComponentEventLogger;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.ComponentEventLogger.EventPair;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.columns.framework.IColumnDefinitionUI;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.columns.specific.GridCustomColumnDefinition;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.BrowserGridPagingToolBar.PagingToolBarButtonKind;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.expressions.filter.FilterToolbar;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.listener.OpenEntityEditorTabClickListener;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.widget.IDataRefreshCallback;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.GWTUtils;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.GWTUtils.DisplayInfoTime;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.IDelegatedAction;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.IMessageProvider;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.WindowUtils;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.lang.StringEscapeUtils;
-import ch.systemsx.cisd.openbis.generic.client.web.client.dto.Constants;
-import ch.systemsx.cisd.openbis.generic.client.web.client.dto.DefaultResultSetConfig;
-import ch.systemsx.cisd.openbis.generic.client.web.client.dto.EntityPropertyUpdatesResult;
-import ch.systemsx.cisd.openbis.generic.client.web.client.dto.GridCustomColumnInfo;
-import ch.systemsx.cisd.openbis.generic.client.web.client.dto.GridFilters;
-import ch.systemsx.cisd.openbis.generic.client.web.client.dto.GridRowModels;
-import ch.systemsx.cisd.openbis.generic.client.web.client.dto.ResultSet;
-import ch.systemsx.cisd.openbis.generic.client.web.client.dto.ResultSetFetchConfig;
-import ch.systemsx.cisd.openbis.generic.client.web.client.dto.TableExportCriteria;
-import ch.systemsx.cisd.openbis.generic.shared.basic.CodeConverter;
-import ch.systemsx.cisd.openbis.generic.shared.basic.GridRowModel;
-import ch.systemsx.cisd.openbis.generic.shared.basic.IColumnDefinition;
-import ch.systemsx.cisd.openbis.generic.shared.basic.IEntityInformationHolderWithPermId;
-import ch.systemsx.cisd.openbis.generic.shared.basic.URLMethodWithParameters;
-import ch.systemsx.cisd.openbis.generic.shared.basic.dto.BasicEntityType;
-import ch.systemsx.cisd.openbis.generic.shared.basic.dto.ColumnSetting;
-import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DataTypeCode;
-import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DatabaseModificationKind;
-import ch.systemsx.cisd.openbis.generic.shared.basic.dto.EntityKind;
-import ch.systemsx.cisd.openbis.generic.shared.basic.dto.IEntityPropertiesHolder;
-import ch.systemsx.cisd.openbis.generic.shared.basic.dto.IEntityProperty;
-import ch.systemsx.cisd.openbis.generic.shared.basic.dto.SortInfo;
-import ch.systemsx.cisd.openbis.generic.shared.basic.dto.SortInfo.SortDir;
-import ch.systemsx.cisd.openbis.generic.shared.basic.dto.VocabularyTerm;
-import ch.systemsx.cisd.openbis.generic.shared.basic.dto.WebClientConfiguration;
-
-/**
- * @author Tomasz Pylak
- */
-public abstract class AbstractBrowserGrid<T/* Entity */, M extends BaseEntityModel<T>> extends
-        LayoutContainer implements IDatabaseModificationObserver, IDisplayTypeIDProvider,
-        IColumnDefinitionProvider<T>
-{
-    private static final IGenericImageBundle IMAGE_BUNDLE = GWT
-            .<IGenericImageBundle> create(IGenericImageBundle.class);
-
-    /**
-     * Shows the detail view for the specified entity
-     */
-    abstract protected void showEntityViewer(T entity, boolean editMode, boolean inBackground);
-
-    abstract protected void listEntities(DefaultResultSetConfig<String, T> resultSetConfig,
-            AbstractAsyncCallback<ResultSet<T>> callback);
-
-    /** Converts specified entity into a grid row model */
-    abstract protected M createModel(GridRowModel<T> entity);
-
-    /**
-     * Called when user wants to export the data. It can happen only after a previous refresh of the
-     * data has taken place. The export criteria has only the cache key
-     */
-    abstract protected void prepareExportEntities(TableExportCriteria<T> exportCriteria,
-            AbstractAsyncCallback<String> callback);
-
-    /**
-     * Creates a column model with all available columns from scratch without taking user settings
-     * into account.
-     * 
-     * @return definition of all the columns in the grid
-     */
-    abstract protected ColumnDefsAndConfigs<T> createColumnsDefinition();
-
-    /**
-     * Called when the user wants to refresh the data. Should be implemented by calling
-     * {@link #refresh(IDataRefreshCallback, boolean) at some point. }
-     */
-    abstract protected void refresh();
-
-    /** @return should the refresh button be enabled? */
-    abstract protected boolean isRefreshEnabled();
-
-    /** @return on which fields filters should be switched on by default? */
-    abstract protected List<IColumnDefinition<T>> getInitialFilters();
-
-    /**
-     * To be subclassed if columns in the grid depend on the internal grid configuration.
-     * 
-     * @return id at which grid display settings are saved.
-     */
-    public String getGridDisplayTypeID()
-    {
-        return createGridDisplayTypeID(null);
-    }
-
-    // --------
-
-    protected final IViewContext<ICommonClientServiceAsync> viewContext;
-
-    protected final ICellListener<T> showEntityViewerLinkClickListener;
-
-    protected final ITableModificationsManager<M> tableModificationsManager;
-
-    // ------ private section. NOTE: it should remain unaccessible to subclasses! ---------------
-
-    private static final int PAGE_SIZE = Constants.GRID_PAGE_SIZE;
-
-    // set to true to see some useful debugging messages
-    private static final boolean DEBUG = false;
-
-    private final PagingLoader<PagingLoadResult<M>> pagingLoader;
-
-    private final ContentPanel contentPanel;
-
-    private final Grid<M> grid;
-
-    private final ColumnListener<T, M> columnListener;
-
-    private final boolean refreshAutomatically;
-
-    // the toolbar has the refresh and export buttons besides the paging controls
-    private final BrowserGridPagingToolBar pagingToolbar;
-
-    // used to change displayed filter widgets
-    private final FilterToolbar<T> filterToolbar;
-
-    private final ToolBar modificationsToolbar;
-
-    private final IDisplayTypeIDGenerator displayTypeIDGenerator;
-
-    // --------- private non-final fields
-
-    // available columns definitions
-    private Set<IColumnDefinition<T>> columnDefinitions;
-
-    private final CustomColumnsMetadataProvider customColumnsMetadataProvider;
-
-    // result set key of the last refreshed data
-    private String resultSetKeyOrNull;
-
-    // Keeps track of the pending fetch (only tracks 1)
-    private final PendingFetchManager pendingFetchManager;
-
-    private IDataRefreshCallback refreshCallback;
-
-    private LayoutContainer bottomToolbars;
-
-    private ColumnModel fullColumnModel;
-
-    protected AbstractBrowserGrid(final IViewContext<ICommonClientServiceAsync> viewContext,
-            String gridId, IDisplayTypeIDGenerator displayTypeIDGenerator)
-    {
-        this(viewContext, gridId, false, displayTypeIDGenerator);
-    }
-
-    /**
-     * @param refreshAutomatically should the data be automatically loaded when the grid is rendered
-     *            for the first time?
-     * @param gridId unique id of the grid which will can used by testframework to identify the grid
-     *            and identify the callback which fills it in.
-     */
-    protected AbstractBrowserGrid(IViewContext<ICommonClientServiceAsync> viewContext,
-            String gridId, boolean refreshAutomatically,
-            IDisplayTypeIDGenerator displayTypeIDGenerator)
-    {
-        pendingFetchManager = new PendingFetchManager();
-        this.displayTypeIDGenerator = displayTypeIDGenerator;
-        this.viewContext = viewContext;
-        int logID = log("create browser grid " + gridId);
-        this.refreshAutomatically = refreshAutomatically;
-        this.pagingLoader = createPagingLoader();
-        this.customColumnsMetadataProvider = new CustomColumnsMetadataProvider();
-        this.grid = createGrid(pagingLoader, gridId);
-        // WORKAROUND
-        // Lazy loading of rows causes tests using experiment browser fail (selection of
-        // project in project tree grid doesn't work).
-        // Turning it off for all grids is the safest solution for our system tests framework
-        // and should improve GUI speed in development mode a bit.
-        grid.setLazyRowRender(0);
-        this.pagingToolbar =
-                new BrowserGridPagingToolBar(asActionInvoker(), viewContext, PAGE_SIZE, gridId);
-        pagingToolbar.bind(pagingLoader);
-        this.filterToolbar =
-                new FilterToolbar<T>(viewContext, gridId, this, createApplyFiltersDelagator());
-        this.tableModificationsManager = new TableModificationsManager();
-        this.modificationsToolbar =
-                new TableModificationsToolbar(viewContext, tableModificationsManager);
-
-        this.contentPanel = createEmptyContentPanel();
-        bottomToolbars = createBottomToolbars(contentPanel, pagingToolbar);
-        configureBottomToolbarSyncSize();
-        contentPanel.add(grid);
-        contentPanel.setBottomComponent(bottomToolbars);
-        contentPanel.setHeaderVisible(false);
-        columnListener = new ColumnListener<T, M>(grid);
-        showEntityViewerLinkClickListener = createShowEntityViewerLinkClickListener();
-        registerLinkClickListenerFor(Dict.CODE, showEntityViewerLinkClickListener);
-        setLayout(new FitLayout());
-        add(contentPanel);
-
-        configureLoggingBetweenEvents(logID);
-
-        grid.addListener(Events.HeaderContextMenu, new Listener<GridEvent<ModelData>>()
-            {
-                public void handleEvent(final GridEvent<ModelData> ge)
-                {
-                    Menu menu = ge.getMenu();
-                    int itemCount = menu.getItemCount();
-                    for (int i = 2; i < itemCount; i++)
-                    {
-                        menu.remove(menu.getItem(2));
-                    }
-                }
-            });
-        if (viewContext.getModel().isEmbeddedMode())
-        {
-            removeButtons(PagingToolBarButtonKind.CONFIG, PagingToolBarButtonKind.REFRESH);
-        }
-    }
-
-    public void removeButtons(PagingToolBarButtonKind... buttonKinds)
-    {
-        pagingToolbar.removeButtons(buttonKinds);
-    }
-
-    private ICellListener<T> createShowEntityViewerLinkClickListener()
-    {
-        return new ICellListener<T>()
-            {
-                public void handle(T rowItem, boolean keyPressed)
-                {
-                    showEntityViewer(rowItem, false, keyPressed);
-                }
-            };
-    }
-
-    private void configureBottomToolbarSyncSize()
-    {
-        // fixes problems with:
-        // - no 'overflow' button when some buttons don't fit into pagingToolbar
-        pagingLoader.addListener(Loader.Load, new Listener<BaseEvent>()
-            {
-                public void handleEvent(BaseEvent be)
-                {
-                    pagingToolbar.syncSize();
-                }
-            });
-        // - hidden paging toolbar
-        pagingToolbar.addListener(Events.AfterLayout, new Listener<BaseEvent>()
-            {
-                public void handleEvent(BaseEvent be)
-                {
-                    contentPanel.syncSize();
-                }
-            });
-        // - bottom toolbar is not resized when new filter row appears
-        filterToolbar.addListener(Events.AfterLayout, new Listener<BaseEvent>()
-            {
-                public void handleEvent(BaseEvent be)
-                {
-                    contentPanel.syncSize();
-                }
-            });
-    }
-
-    private void configureLoggingBetweenEvents(int logID)
-    {
-        if (viewContext.isLoggingEnabled())
-        {
-            ComponentEventLogger logger = new ComponentEventLogger(viewContext, getId());
-            logger.prepareLoggingBetweenEvents(contentPanel, EventPair.RENDER);
-            logger.prepareLoggingBetweenEvents(this, EventPair.LAYOUT);
-            logger.prepareLoggingBetweenEvents(grid, EventPair.LAYOUT);
-            logger.prepareLoggingBetweenEvents(contentPanel, EventPair.LAYOUT);
-            logger.prepareLoggingBetweenEvents(bottomToolbars, EventPair.LAYOUT);
-            logger.prepareLoggingBetweenEvents(filterToolbar, EventPair.LAYOUT);
-            logger.prepareLoggingBetweenEvents(pagingToolbar, EventPair.LAYOUT);
-            viewContext.logStop(logID);
-        }
-    }
-
-    protected int log(String message)
-    {
-        return viewContext.log(message + " [" + getId() + "]");
-    }
-
-    protected void showEntityInformationHolderViewer(IEntityInformationHolderWithPermId entity,
-            boolean editMode, boolean inBackground)
-    {
-        if (editMode
-                && OpenEntityEditorTabClickListener.forbidDeletedEntityModification(viewContext,
-                        entity))
-        {
-            return;
-        }
-        final EntityKind entityKind = entity.getEntityKind();
-        final AbstractTabItemFactory tabView;
-        BasicEntityType entityType = entity.getEntityType();
-        final IClientPluginFactory clientPluginFactory =
-                viewContext.getClientPluginFactoryProvider().getClientPluginFactory(entityKind,
-                        entityType);
-        final IClientPlugin<BasicEntityType, IEntityInformationHolderWithPermId> createClientPlugin =
-                clientPluginFactory.createClientPlugin(entityKind);
-        if (editMode)
-        {
-            tabView = createClientPlugin.createEntityEditor(entity);
-        } else
-        {
-            tabView = createClientPlugin.createEntityViewer(entity);
-        }
-        tabView.setInBackground(inBackground);
-        DispatcherHelper.dispatchNaviEvent(tabView);
-    }
-
-    /** Refreshes the grid without showing the loading progress bar */
-    protected final void refreshGridSilently()
-    {
-        grid.setLoadMask(false);
-        int id = log("refresh silently");
-        refresh();
-        grid.setLoadMask(true);
-        viewContext.logStop(id);
-    }
-
-    /**
-     * Shows/hides the load mask immediately.
-     * 
-     * @param loadMask Load mask is shown if <code>true</code> otherwise it is hidden.
-     */
-    public void setLoadMaskImmediately(boolean loadMask)
-    {
-        if (grid.isRendered())
-        {
-            if (loadMask)
-            {
-                grid.el().mask(GXT.MESSAGES.loadMask_msg());
-            } else
-            {
-                grid.el().unmask();
-            }
-        }
-
-    }
-
-    /**
-     * Registers the specified listener for clicks on links in the specified column.
-     * 
-     * @param columnID Column ID. Not case sensitive.
-     * @param listener Listener handle single clicks.
-     */
-    protected final void registerLinkClickListenerFor(final String columnID,
-            final ICellListener<T> listener)
-    {
-        if (viewContext.isSimpleOrEmbeddedMode() == false)
-        {
-            columnListener.registerLinkClickListener(columnID, listener);
-        }
-    }
-
-    /**
-     * Allows multiple selection instead of single selection.
-     */
-    protected final void allowMultipleSelection()
-    {
-        grid.getSelectionModel().setSelectionMode(SelectionMode.MULTI);
-    }
-
-    protected List<T> getGridElements()
-    {
-        List<M> models = grid.getStore().getModels();
-        List<T> elements = new ArrayList<T>();
-        for (M model : models)
-        {
-            elements.add(model.getBaseObject());
-        }
-        return elements;
-    }
-
-    private IDelegatedAction createApplyFiltersDelagator()
-    {
-        return new IDelegatedAction()
-            {
-                public void execute()
-                {
-                    if (resultSetKeyOrNull != null && pendingFetchManager.hasNoPendingFetch())
-                    {
-                        ResultSetFetchConfig<String> fetchConfig =
-                                ResultSetFetchConfig.createFetchFromCache(resultSetKeyOrNull);
-                        reloadData(fetchConfig);
-                    }
-                }
-            };
-    }
-
-    /** @return this grid as a disposable component with a specified toolbar at the top. */
-    protected DisposableEntityChooser<T> asDisposableWithToolbar(final IDisposableComponent toolbar)
-    {
-        final LayoutContainer container = new LayoutContainer();
-        container.setLayout(new RowLayout());
-        container.add(toolbar.getComponent());
-        container.add(this, new RowData(1, 1));
-
-        return asDisposableEntityChooser(container, toolbar);
-    }
-
-    /** @return this grid as a disposable component */
-    protected final DisposableEntityChooser<T> asDisposableWithoutToolbar()
-    {
-        return asDisposableEntityChooser(this);
-    }
-
-    /**
-     * @return this grid as a disposable component with a specified toolbar at the top and a tree on
-     *         the left.
-     */
-    protected final DisposableEntityChooser<T> asDisposableWithToolbarAndTree(
-            final IDisposableComponent toolbar, final Component tree, String headerOrNull)
-    {
-        // WORKAROUND: BorderLayout causes problems when rendered in a tab
-        // We use RowLayout here but we loose the split this way.
-        final LayoutContainer container = new LayoutContainer();
-        container.setLayout(new RowLayout(Orientation.VERTICAL));
-        container.add(toolbar.getComponent(), new RowData(1, -1));
-
-        final LayoutContainer subContainer = new LayoutContainer();
-        subContainer.setLayout(new RowLayout(Orientation.HORIZONTAL));
-        subContainer.add(tree, new RowData(300, 1));
-        setHeader(headerOrNull);
-        subContainer.add(this, new RowData(1, 1));
-        container.add(subContainer, new RowData(1, 1));
-
-        return asDisposableEntityChooser(container, toolbar);
-    }
-
-    protected final void setHeader(String headerOrNull)
-    {
-        if (headerOrNull != null)
-        {
-            this.contentPanel.setHeaderVisible(true);
-            this.contentPanel.setHeading(headerOrNull);
-        } else
-        {
-            this.contentPanel.setHeaderVisible(false);
-        }
-    }
-
-    protected final DisposableEntityChooser<T> asDisposableEntityChooser(
-            final Component mainComponent, final IDisposableComponent... disposableComponents)
-    {
-        final AbstractBrowserGrid<T, M> self = this;
-        return new DisposableEntityChooser<T>()
-            {
-                public T tryGetSingleSelected()
-                {
-                    List<M> items = getSelectedItems();
-                    if (items.isEmpty())
-                    {
-                        return null;
-                    } else
-                    {
-                        return items.get(0).getBaseObject();
-                    }
-                }
-
-                public void dispose()
-                {
-                    debug("dispose a browser");
-                    self.disposeCache();
-                    for (IDisposableComponent disposableComponent : disposableComponents)
-                    {
-                        disposableComponent.dispose();
-                    }
-                }
-
-                public Component getComponent()
-                {
-                    return mainComponent;
-                }
-
-                public DatabaseModificationKind[] getRelevantModifications()
-                {
-                    return self.getRelevantModifications();
-                }
-
-                public void update(Set<DatabaseModificationKind> observedModifications)
-                {
-                    self.update(observedModifications);
-                }
-
-            };
-    }
-
-    @Override
-    protected void onRender(final Element parent, final int pos)
-    {
-        super.onRender(parent, pos);
-        if (refreshAutomatically)
-        {
-            int id = log("layout automatically");
-            layout();
-            viewContext.logStop(id);
-            id = log("refresh automatically");
-            refresh();
-            viewContext.logStop(id);
-        }
-    }
-
-    private PagingLoader<PagingLoadResult<M>> createPagingLoader()
-    {
-        final RpcProxy<PagingLoadResult<M>> proxy = new RpcProxy<PagingLoadResult<M>>()
-            {
-                @Override
-                protected void load(Object loadConfig, AsyncCallback<PagingLoadResult<M>> callback)
-                {
-                    loadData((PagingLoadConfig) loadConfig, callback);
-                }
-            };
-        final BasePagingLoader<PagingLoadResult<M>> newPagingLoader =
-                new BasePagingLoader<PagingLoadResult<M>>(proxy);
-        newPagingLoader.setRemoteSort(true);
-
-        return newPagingLoader;
-    }
-
-    private void loadData(final PagingLoadConfig loadConfig,
-            final AsyncCallback<PagingLoadResult<M>> callback)
-    {
-        if (pendingFetchManager.hasNoPendingFetch())
-        {
-            // this can happen when user wants to sort data - the refresh method is not called
-            if (resultSetKeyOrNull == null)
-            {
-                // data are not yet cached, so we ignore this call - should not really happen
-                return;
-            }
-            pendingFetchManager.pushPendingFetchConfig(ResultSetFetchConfig
-                    .createFetchFromCache(resultSetKeyOrNull));
-        }
-        GridFilters<T> filters = filterToolbar.getFilters();
-        final DefaultResultSetConfig<String, T> resultSetConfig =
-                createPagingConfig(loadConfig, filters, getGridDisplayTypeID());
-        debug("create a refresh callback " + pendingFetchManager.tryTopPendingFetchConfig());
-        final ListEntitiesCallback listCallback =
-                new ListEntitiesCallback(viewContext, callback, resultSetConfig);
-
-        listEntities(resultSetConfig, listCallback);
-    }
-
-    // Default visibility so that friend classes can use -- should otherwise be considered private
-    void debug(String msg)
-    {
-        if (DEBUG)
-        {
-            String text =
-                    "[grid: " + getGridDisplayTypeID() + ", cache: " + resultSetKeyOrNull + "] "
-                            + msg;
-            System.out.println(text);
-        }
-    }
-
-    private DefaultResultSetConfig<String, T> createPagingConfig(PagingLoadConfig loadConfig,
-            GridFilters<T> filters, String gridDisplayId)
-    {
-        int limit = loadConfig.getLimit();
-        int offset = loadConfig.getOffset();
-        com.extjs.gxt.ui.client.data.SortInfo sortInfo = loadConfig.getSortInfo();
-
-        DefaultResultSetConfig<String, T> resultSetConfig = new DefaultResultSetConfig<String, T>();
-        resultSetConfig.setLimit(limit);
-        resultSetConfig.setOffset(offset);
-        resultSetConfig.setAvailableColumns(columnDefinitions);
-        SortInfo translatedSortInfo = translateSortInfo(sortInfo);
-        Set<String> columnIDs = getIDsOfColumnsToBeShown();
-        resultSetConfig.setIDsOfPresentedColumns(columnIDs);
-        resultSetConfig.setSortInfo(translatedSortInfo);
-        resultSetConfig.setFilters(filters);
-        resultSetConfig.setCacheConfig(pendingFetchManager.tryTopPendingFetchConfig());
-        resultSetConfig.setGridDisplayId(gridDisplayId);
-        resultSetConfig.setCustomColumnErrorMessageLong(viewContext.getDisplaySettingsManager()
-                .isDebuggingModeEnabled());
-        return resultSetConfig;
-    }
-
-    private Set<String> getIDsOfColumnsToBeShown()
-    {
-        Set<String> columnIDs = new HashSet<String>();
-        DisplaySettingsManager manager = viewContext.getDisplaySettingsManager();
-        List<ColumnSetting> columnSettings = manager.getColumnSettings(getGridDisplayTypeID());
-        if (columnSettings != null)
-        {
-            for (ColumnSetting columnSetting : columnSettings)
-            {
-                if (columnSetting.isHidden() == false)
-                {
-                    columnIDs.add(columnSetting.getColumnID());
-                }
-            }
-        }
-        List<IColumnDefinition<T>> visibleColumns = getVisibleColumns(columnDefinitions);
-        for (IColumnDefinition<T> definition : visibleColumns)
-        {
-            columnIDs.add(definition.getIdentifier());
-        }
-        return columnIDs;
-    }
-
-    private static <T> SortInfo translateSortInfo(com.extjs.gxt.ui.client.data.SortInfo sortInfo)
-    {
-        return translateSortInfo(sortInfo.getSortField(), sortInfo.getSortDir());
-    }
-
-    private static <T> SortInfo translateSortInfo(String sortFieldId,
-            com.extjs.gxt.ui.client.Style.SortDir sortDir)
-    {
-        SortInfo sortInfo = new SortInfo();
-        sortInfo.setSortField(sortFieldId);
-        sortInfo.setSortDir(translate(sortDir));
-
-        return sortInfo;
-    }
-
-    private static SortDir translate(com.extjs.gxt.ui.client.Style.SortDir sortDir)
-    {
-        if (sortDir.equals(com.extjs.gxt.ui.client.Style.SortDir.ASC))
-        {
-            return SortDir.ASC;
-        } else if (sortDir.equals(com.extjs.gxt.ui.client.Style.SortDir.DESC))
-        {
-            return SortDir.DESC;
-        } else if (sortDir.equals(com.extjs.gxt.ui.client.Style.SortDir.NONE))
-        {
-            return SortDir.NONE;
-        } else
-        {
-            throw new IllegalStateException("unknown sort dir: " + sortDir);
-        }
-    }
-
-    private static com.extjs.gxt.ui.client.Style.SortDir translate(SortDir sortDir)
-    {
-        if (sortDir.equals(SortDir.ASC))
-        {
-            return com.extjs.gxt.ui.client.Style.SortDir.ASC;
-        } else if (sortDir.equals(SortDir.DESC))
-        {
-            return com.extjs.gxt.ui.client.Style.SortDir.DESC;
-        } else if (sortDir.equals(SortDir.NONE))
-        {
-            return com.extjs.gxt.ui.client.Style.SortDir.NONE;
-        } else
-        {
-            throw new IllegalStateException("unknown sort dir: " + sortDir);
-        }
-    }
-
-    /** @return number of rows in the grid */
-    public final int getRowNumber()
-    {
-        return grid.getStore().getCount();
-    }
-
-    public final class ListEntitiesCallback extends AbstractAsyncCallback<ResultSet<T>>
-    {
-        private final AsyncCallback<PagingLoadResult<M>> delegate;
-
-        // configuration with which the listing was called
-        private DefaultResultSetConfig<String, T> resultSetConfig;
-
-        private int logID;
-
-        private boolean reloadingPhase;
-
-        public ListEntitiesCallback(final IViewContext<?> viewContext,
-                final AsyncCallback<PagingLoadResult<M>> delegate,
-                final DefaultResultSetConfig<String, T> resultSetConfig)
-        {
-            super(viewContext);
-            this.delegate = delegate;
-            this.resultSetConfig = resultSetConfig;
-            logID = log("load data");
-        }
-
-        //
-        // AbstractAsyncCallback
-        //
-
-        @Override
-        public final void finishOnFailure(final Throwable caught)
-        {
-            reenableAfterFailure();
-            // no need to show error message - it should be shown by DEFAULT_CALLBACK_LISTENER
-            caught.printStackTrace();
-            delegate.onFailure(caught);
-        }
-
-        public final void reenableAfterFailure()
-        {
-            grid.el().unmask();
-            onComplete(false);
-            pagingToolbar.enable(); // somehow enabling toolbar is lost in its handleEvent() method
-        }
-
-        @Override
-        protected void performSuccessActionOrIgnore(final IDelegatedAction successAction)
-        {
-            if (tableModificationsManager.isTableDirty())
-            {
-                final Listener<MessageBoxEvent> listener = new Listener<MessageBoxEvent>()
-                    {
-                        public void handleEvent(MessageBoxEvent me)
-                        {
-                            if (me.getButtonClicked().getItemId().equals(Dialog.YES))
-                            {
-                                tableModificationsManager.saveModifications(new IDelegatedAction()
-                                    {
-                                        public void execute()
-                                        {
-                                            // ignore this callback and refresh the table
-                                            ignore();
-                                            reenableAfterFailure();
-                                            refresh();
-                                        }
-                                    });
-                            } else
-                            {
-                                tableModificationsManager.cancelModifications();
-                                successAction.execute();
-                            }
-                        }
-                    };
-                final String title =
-                        viewContext.getMessage(Dict.CONFIRM_SAVE_TABLE_MODIFICATIONS_DIALOG_TITLE);
-                final String msg =
-                        viewContext
-                                .getMessage(Dict.CONFIRM_SAVE_TABLE_MODIFICATIONS_DIALOG_MESSAGE);
-                MessageBox.confirm(title, msg, listener);
-            } else
-            {
-                successAction.execute();
-            }
-        }
-
-        @Override
-        protected final void process(final ResultSet<T> result)
-        {
-            viewContext.logStop(logID);
-            logID = log("process loaded data");
-            // save the key of the result, later we can refer to the result in the cache using this
-            // key
-            String key = result.getResultSetKey();
-            saveCacheKey(key);
-            GridRowModels<T> rowModels = result.getList();
-            boolean partial = result.isPartial();
-            List<GridCustomColumnInfo> customColumnMetadata = rowModels.getCustomColumnsMetadata();
-            customColumnsMetadataProvider.setCustomColumnsMetadata(customColumnMetadata);
-
-            if (reloadingPhase)
-            {
-                reloadingPhase = false;
-            } else if (partial)
-            {
-                reloadingPhase = true;
-                BasePagingLoadConfig loadConfig = new BasePagingLoadConfig();
-                loadConfig.setLimit(resultSetConfig.getLimit());
-                loadConfig.setOffset(resultSetConfig.getOffset());
-                SortInfo sortInfo = resultSetConfig.getSortInfo();
-                if (sortInfo != null)
-                {
-                    String sortField = sortInfo.getSortField();
-                    if (sortField != null)
-                    {
-                        loadConfig.setSortField(sortField);
-                        loadConfig.setSortDir(translate(sortInfo.getSortDir()));
-                    }
-                }
-                resultSetConfig =
-                        createPagingConfig(loadConfig, filterToolbar.getFilters(),
-                                resultSetConfig.tryGetGridDisplayId());
-                resultSetConfig.setCacheConfig(ResultSetFetchConfig
-                        .createFetchFromCacheAndRecompute(key));
-                this.reuse();
-                listEntities(resultSetConfig, this);
-            }
-            // convert the result to the model data for the grid control
-            final List<M> models = createModels(rowModels);
-            final PagingLoadResult<M> loadResult =
-                    new BasePagingLoadResult<M>(models, resultSetConfig.getOffset(),
-                            result.getTotalLength());
-
-            delegate.onSuccess(loadResult);
-            pagingToolbar.enableExportButton();
-            pagingToolbar.updateDefaultConfigButton(true);
-
-            if (reloadingPhase == false)
-            {
-                pagingToolbar.enable();
-                filterToolbar.refreshColumnFiltersDistinctValues(rowModels
-                        .getColumnDistinctValues());
-            } else
-            {
-                pagingToolbar.disableForLoadingRest();
-            }
-            onComplete(true);
-
-            viewContext.logStop(logID);
-        }
-
-        // notify that the refresh is done
-        private void onComplete(boolean wasSuccessful)
-        {
-            pendingFetchManager.popPendingFetch();
-            refreshCallback.postRefresh(wasSuccessful);
-        }
-
-        private List<M> createModels(final GridRowModels<T> gridRowModels)
-        {
-            final List<M> result = new ArrayList<M>();
-            initializeModelCreation();
-            for (final GridRowModel<T> entity : gridRowModels)
-            {
-                M model = createModel(entity);
-                result.add(model);
-            }
-            return result;
-        }
-
-        @Override
-        /* Note: we want to differentiate between callbacks in different subclasses of this grid. */
-        public String getCallbackId()
-        {
-            return grid.getId();
-        }
-    }
-
-    /**
-     * Initializes creation of model from received data. This is a hook method called before {
-     * {@link AbstractBrowserGrid#createModel(GridRowModel)} is invoked for all rows. This
-     * implementation does nothing. Subclasses usually override this method by creating an instance
-     * attribute which holds a list of visible column definitions. This speeds up invocation of
-     * {@link AbstractBrowserGrid#createModel(GridRowModel)}.
-     */
-    protected void initializeModelCreation()
-    {
-    }
-
-    protected Set<String> getIDsOfVisibleColumns()
-    {
-        Set<String> visibleColumnIds = new HashSet<String>();
-        for (int i = 0, n = fullColumnModel.getColumnCount(); i < n; i++)
-        {
-            ColumnConfig column = fullColumnModel.getColumn(i);
-            if (column.isHidden() == false)
-            {
-                visibleColumnIds.add(column.getId());
-            }
-        }
-        return visibleColumnIds;
-    }
-
-    // wraps this browser into the interface appropriate for the toolbar. If this class would just
-    // implement the interface it could be very confusing for the code reader.
-    protected IBrowserGridActionInvoker asActionInvoker()
-    {
-        final AbstractBrowserGrid<T, M> delegate = this;
-        return new IBrowserGridActionInvoker()
-            {
-                public void export(boolean allColumns)
-                {
-                    delegate.export(allColumns);
-                }
-
-                public void refresh()
-                {
-                    int id = log("refresh in action invoker");
-                    delegate.refresh();
-                    viewContext.logStop(id);
-                }
-
-                public void configure()
-                {
-                    delegate.configureColumnSettings();
-                }
-
-                public void toggleFilters(boolean show)
-                {
-                    if (show)
-                    {
-                        int logId = log("adding filters");
-                        delegate.showFiltersBar();
-                        viewContext.logStop(logId);
-                    } else
-                    {
-                        int logId = log("removing filters");
-                        bottomToolbars.remove(filterToolbar);
-                        bottomToolbars.layout();
-                        viewContext.logStop(logId);
-                    }
-                }
-
-            };
-    }
-
-    protected void showFiltersBar()
-    {
-        // always show filters under modifications
-        int position = bottomToolbars.getItemCount() > 1 ? 1 : 0;
-        bottomToolbars.insert(filterToolbar, position);
-        bottomToolbars.layout();
-    }
-
-    protected void showModificationsBar()
-    {
-        if (bottomToolbars.getItems().contains(modificationsToolbar) == false)
-        {
-            GWTUtils.displayInfo(viewContext.getMessage(Dict.TABLE_MODIFICATIONS_INFO_TITLE),
-                    viewContext.getMessage(Dict.TABLE_MODIFICATIONS_INFO_TEXT),
-                    DisplayInfoTime.LONG);
-            bottomToolbars.insert(modificationsToolbar, 0);
-            bottomToolbars.layout();
-        }
-    }
-
-    protected void hideModificationsBar()
-    {
-        bottomToolbars.remove(modificationsToolbar);
-        bottomToolbars.layout();
-    }
-
-    protected static interface ISelectedEntityInvoker<M>
-    {
-        void invoke(M selectedItem, boolean keyPressed);
-    }
-
-    protected final ISelectedEntityInvoker<M> asShowEntityInvoker(final boolean editMode)
-    {
-        return new ISelectedEntityInvoker<M>()
-            {
-                public void invoke(M selectedItem, boolean keyPressed)
-                {
-                    if (selectedItem != null)
-                    {
-                        showEntityViewer(selectedItem.getBaseObject(), editMode, keyPressed);
-                    }
-                }
-            };
-    }
-
-    private ISelectedEntityInvoker<M> createNotImplementedInvoker()
-    {
-        return new ISelectedEntityInvoker<M>()
-            {
-                public void invoke(M selectedItem, boolean keyPressed)
-                {
-                    MessageBox.alert(viewContext.getMessage(Dict.MESSAGEBOX_WARNING),
-                            viewContext.getMessage(Dict.NOT_IMPLEMENTED), null);
-                }
-            };
-    }
-
-    /**
-     * @return a button which has no action but is enabled only when one entity in the grid is
-     *         selected. Useful only for writing prototypes.
-     */
-    protected final Button createSelectedItemDummyButton(final String title)
-    {
-        return createSelectedItemButton(title, createNotImplementedInvoker());
-    }
-
-    /**
-     * @return like {@link #createSelectedItemButton(String, ISelectedEntityInvoker)} with button id
-     *         set
-     */
-    protected final Button createSelectedItemButton(final String title, final String id,
-            final ISelectedEntityInvoker<M> invoker)
-    {
-        final Button button = createSelectedItemButton(title, invoker);
-        button.setId(id);
-        return button;
-    }
-
-    /**
-     * @return a button which is enabled only when one entity in the grid is selected. When button
-     *         is pressed, the specified invoker action is performed.
-     */
-    protected final Button createSelectedItemButton(final String title,
-            final ISelectedEntityInvoker<M> invoker)
-    {
-        final Button button = new Button(title, new SelectionListener<ButtonEvent>()
-            {
-                @Override
-                public void componentSelected(ButtonEvent ce)
-                {
-                    List<M> selectedItems = getSelectedItems();
-                    if (selectedItems.isEmpty() == false)
-                    {
-                        invoker.invoke(selectedItems.get(0), false);
-                    }
-                }
-            });
-        enableButtonOnSelectedItem(button);
-        return button;
-    }
-
-    /**
-     * @return a button which is enabled only when at least one entity in the grid is selected, and
-     *         with specified selection listener set.
-     */
-    protected final Button createSelectedItemsButton(final String title,
-            SelectionListener<ButtonEvent> listener)
-    {
-        Button button = new Button(title);
-        button.addSelectionListener(listener);
-        enableButtonOnSelectedItems(button);
-        return button;
-    }
-
-    /** adds given <var>button</var> to grid {@link PagingToolBar} */
-    protected final void addButton(Button button)
-    {
-        pagingToolbar.add(button);
-    }
-
-    /**
-     * Given <var>button</var> will be enabled only if at least one item is selected in the grid.
-     */
-    protected final void enableButtonOnSelectedItems(final Button button)
-    {
-        button.setEnabled(false);
-        addGridSelectionChangeListener(new Listener<SelectionChangedEvent<ModelData>>()
-            {
-                public void handleEvent(SelectionChangedEvent<ModelData> se)
-                {
-                    boolean enabled = se.getSelection().size() > 0;
-                    button.setEnabled(enabled);
-                }
-
-            });
-    }
-
-    /**
-     * Given <var>button</var> will be enabled only if exactly one item is selected in the grid.
-     */
-    protected final void enableButtonOnSelectedItem(final Button button)
-    {
-        button.setEnabled(false);
-        addGridSelectionChangeListener(new Listener<SelectionChangedEvent<ModelData>>()
-            {
-                public void handleEvent(SelectionChangedEvent<ModelData> se)
-                {
-                    boolean enabled = se.getSelection().size() == 1;
-                    button.setEnabled(enabled);
-                }
-
-            });
-    }
-
-    /**
-     * Given <var>button</var> will have title changed depending on number of items selected in the
-     * grid.
-     */
-    protected final void changeButtonTitleOnSelectedItems(final Button button,
-            final String noSelectedItemsTitle, final String selectedItemsTitle)
-    {
-        addGridSelectionChangeListener(new Listener<SelectionChangedEvent<ModelData>>()
-            {
-                public void handleEvent(SelectionChangedEvent<ModelData> se)
-                {
-                    boolean noSelected = se.getSelection().size() == 0;
-                    button.setText(noSelected ? noSelectedItemsTitle : selectedItemsTitle);
-                }
-
-            });
-    }
-
-    public void addGridSelectionChangeListener(Listener<SelectionChangedEvent<ModelData>> listener)
-    {
-        grid.getSelectionModel().addListener(Events.SelectionChange, listener);
-    }
-
-    /**
-     * Returns all models of selected items or an empty list if nothing selected.
-     */
-    public final List<M> getSelectedItems()
-    {
-        return grid.getSelectionModel().getSelectedItems();
-    }
-
-    /**
-     * Returns all base objects of selected items or an empty list if nothing selected.
-     */
-    protected final List<T> getSelectedBaseObjects()
-    {
-        List<M> items = getSelectedItems();
-        List<T> data = new ArrayList<T>();
-        for (BaseEntityModel<T> item : items)
-        {
-            data.add(item.getBaseObject());
-        }
-        return data;
-    }
-
-    protected final IDelegatedAction createRefreshGridAction()
-    {
-        return new IDelegatedAction()
-            {
-                public void execute()
-                {
-                    int id = log("execute refrish grid action");
-                    refresh();
-                    viewContext.logStop(id);
-                }
-            };
-    }
-
-    /** Refreshes grid and filters (resets filter selection) */
-    protected final void refreshGridWithFilters()
-    {
-        // N.B. -- The order in which things are refreshed and configured is significant
-
-        // export and config buttons are enabled when ListEntitiesCallback is complete
-        pagingToolbar.disableExportButton();
-        pagingToolbar.updateDefaultConfigButton(false);
-
-        // Need to reset filter fields *before* refreshing the grid so the list can
-        // be correctly retrieved
-        filterToolbar.resetFilterFields();
-        filterToolbar.resetFilterSelectionWithoutApply();
-
-        int id = log("refresh grid with filters");
-        refresh();
-        viewContext.logStop(id);
-
-        // Need to refresh the filter toolbar *after* refreshing the grid, because it
-        // has a dependency on information from the grid that gets updated with the refresh
-        filterToolbar.refresh();
-    }
-
-    protected final void updateDefaultRefreshButton()
-    {
-        boolean isEnabled = isRefreshEnabled();
-        pagingToolbar.updateDefaultRefreshButton(isEnabled);
-    }
-
-    /**
-     * Refreshes the grid.
-     * <p>
-     * Note that, doing so, the result set associated on the server side will be removed.
-     * </p>
-     */
-    protected final void refresh(boolean refreshColumnsDefinition)
-    {
-        refresh(null, refreshColumnsDefinition);
-    }
-
-    /**
-     * @param externalRefreshCallbackOrNull external class can define it's own refresh callback
-     *            method. It will be merged with the internal one.
-     */
-    protected final void refresh(final IDataRefreshCallback externalRefreshCallbackOrNull,
-            boolean refreshColumnsDefinition)
-    {
-        int id = log("refresh (refreshColumnsDefinition=" + refreshColumnsDefinition + ")");
-        pagingToolbar.updateDefaultRefreshButton(false);
-        debug("clean cache for refresh");
-        this.refreshCallback = createRefreshCallback(externalRefreshCallbackOrNull);
-        if (columnDefinitions == null || refreshColumnsDefinition)
-        {
-            recreateColumnModelAndRefreshColumnsWithFilters();
-        }
-        reloadData(createDisposeAndRefreshFetchMode());
-        viewContext.logStop(id);
-    }
-
-    private ResultSetFetchConfig<String> createDisposeAndRefreshFetchMode()
-    {
-        if (resultSetKeyOrNull != null)
-        {
-            return ResultSetFetchConfig.createClearComputeAndCache(resultSetKeyOrNull);
-        } else
-        {
-            return ResultSetFetchConfig.createComputeAndCache();
-        }
-    }
-
-    private ColumnDefsAndConfigs<T> createColumnDefsAndConfigs()
-    {
-        ColumnDefsAndConfigs<T> defsAndConfigs = createColumnsDefinition();
-        // add custom columns
-        List<GridCustomColumnInfo> customColumnsMetadata =
-                customColumnsMetadataProvider.getCustomColumnsMetadata();
-        if (customColumnsMetadata.size() > 0)
-        {
-            List<IColumnDefinitionUI<T>> customColumnsDefs =
-                    createCustomColumnDefinitions(customColumnsMetadata);
-            defsAndConfigs.addColumns(customColumnsDefs, viewContext);
-
-            for (GridCustomColumnInfo gridCustomColumnInfo : customColumnsMetadata)
-            {
-
-                DataTypeCode columnType = gridCustomColumnInfo.getDataType();
-                GridCellRenderer<BaseEntityModel<?>> columnRenderer = null;
-
-                if (DataTypeCode.REAL.equals(columnType))
-                {
-                    columnRenderer =
-                            new RealNumberRenderer(viewContext.getDisplaySettingsManager()
-                                    .getRealNumberFormatingParameters());
-
-                } else if (DataTypeCode.VARCHAR.equals(columnType))
-                {
-                    columnRenderer = new CustomColumnStringRenderer();
-                }
-
-                if (columnRenderer != null)
-                {
-                    defsAndConfigs.setGridCellRendererFor(gridCustomColumnInfo.getCode(),
-                            columnRenderer);
-                }
-            }
-        }
-
-        return defsAndConfigs;
-    }
-
-    protected final void recreateColumnModelAndRefreshColumnsWithFilters()
-    {
-        int logId = log("recreateColumnModelAndRefreshColumnsWithFilters");
-
-        ColumnDefsAndConfigs<T> defsAndConfigs = createColumnDefsAndConfigs();
-
-        this.columnDefinitions = defsAndConfigs.getColumnDefs();
-        ColumnModel columnModel = createColumnModel(defsAndConfigs.getColumnConfigs());
-
-        refreshColumnsAndFilters(columnModel);
-
-        viewContext.logStop(logId);
-    }
-
-    private static <T> List<IColumnDefinitionUI<T>> createCustomColumnDefinitions(
-            List<GridCustomColumnInfo> customColumnsMetadata)
-    {
-        List<IColumnDefinitionUI<T>> defs = new ArrayList<IColumnDefinitionUI<T>>();
-        for (GridCustomColumnInfo columnMetadata : customColumnsMetadata)
-        {
-            IColumnDefinitionUI<T> colDef = new GridCustomColumnDefinition<T>(columnMetadata);
-            defs.add(colDef);
-        }
-        return defs;
-    }
-
-    private void refreshColumnsAndFilters(ColumnModel columnModel)
-    {
-        ColumnModel newColumnModel = columnModel;
-        GridDisplaySettings settings = tryApplyDisplaySettings(newColumnModel);
-        if (settings != null && settings.getColumnConfigs() != null)
-        {
-            newColumnModel = createColumnModel(settings.getColumnConfigs());
-            rebuildFiltersFromIds(settings.getFilteredColumnIds());
-        } else
-        {
-            filterToolbar.rebuildColumnFilters(getInitialFilters());
-        }
-        changeColumnModel(newColumnModel, settings != null ? settings.getSortField() : null,
-                settings != null ? settings.getSortDir() : null);
-    }
-
-    private void hideLoadingMask()
-    {
-        if (grid.isRendered() && grid.el() != null)
-        {
-            grid.el().unmask();
-        }
-    }
-
-    private GridDisplaySettings tryApplyDisplaySettings(ColumnModel columnModel)
-    {
-        List<IColumnDefinition<T>> initialFilters = getInitialFilters();
-        return viewContext.getDisplaySettingsManager().tryApplySettings(getGridDisplayTypeID(),
-                columnModel, extractColumnIds(initialFilters), getGridSortInfo());
-    }
-
-    private void reconfigureGrid(ColumnModel columnModelOfVisible)
-    {
-        List<Listener<?>> sortlisteners =
-                new ArrayList<Listener<?>>(grid.getListeners(Events.SortChange));
-        for (Listener<?> listener : sortlisteners)
-        {
-            grid.removeListener(Events.SortChange, listener);
-        }
-
-        grid.reconfigure(grid.getStore(), columnModelOfVisible);
-
-        for (Listener<?> listener : sortlisteners)
-        {
-            grid.addListener(Events.SortChange, listener);
-        }
-    }
-
-    private void changeColumnModel(ColumnModel columnModel, String sortField, SortDir sortDir)
-    {
-        fullColumnModel = columnModel;
-
-        int logId = log("grid reconfigure");
-        ColumnModel columnModelOfVisible = trimToVisibleColumns(columnModel);
-
-        if (sortDir != null && sortField != null)
-        {
-            pagingLoader.setSortDir(translate(sortDir));
-            pagingLoader.setSortField(sortField);
-        }
-        reconfigureGrid(columnModelOfVisible);
-
-        viewContext.logStop(logId);
-        registerGridSettingsChangesListener();
-        // add listeners of full column model to trimmed model
-        List<Listener<? extends BaseEvent>> listeners =
-                fullColumnModel.getListeners(Events.WidthChange);
-        for (Listener<? extends BaseEvent> listener : listeners)
-        {
-            columnModelOfVisible.addListener(Events.WidthChange, listener);
-            columnModelOfVisible.addListener(Events.ColumnMove, listener); // track drag&drop
-        }
-    }
-
-    private ColumnModel trimToVisibleColumns(ColumnModel columnModel)
-    {
-        int maxVisibleColumns = getWebClientConfiguration().getMaxVisibleColumns();
-        int counter = 0;
-        List<ColumnConfig> columns = new ArrayList<ColumnConfig>();
-        for (int i = 0, n = columnModel.getColumnCount(); i < n; i++)
-        {
-            ColumnConfig column = columnModel.getColumn(i);
-            if (column.isHidden() == false)
-            {
-                counter++;
-                if (counter <= maxVisibleColumns)
-                {
-                    columns.add(column);
-                } else
-                {
-                    column.setHidden(true);
-                }
-            }
-        }
-        if (counter > maxVisibleColumns)
-        {
-            saveColumnDisplaySettings(); // save changes made to full model
-            InfoConfig infoConfig =
-                    new InfoConfig(viewContext.getMessage(Dict.VISIBLE_COLUMNS_LIMITED_TITLE),
-                            viewContext.getMessage(Dict.VISIBLE_COLUMNS_LIMITED_MSG,
-                                    maxVisibleColumns, counter));
-            infoConfig.height = 100; // a bit higher
-            infoConfig.display = 5000; // 5s
-            Info.display(infoConfig);
-        }
-        ColumnModel trimmedModel = createColumnModel(columns);
-        return trimmedModel;
-    }
-
-    private void registerGridSettingsChangesListener()
-    {
-        viewContext.getDisplaySettingsManager().registerGridSettingsChangesListener(
-                getGridDisplayTypeID(), createDisplaySettingsUpdater());
-    }
-
-    // Refreshes the data, does not clear the cache. Does not change the column model.
-    // Default visibility so that friend classes can use -- should otherwise be considered private
-    void reloadData(ResultSetFetchConfig<String> resultSetFetchConfig)
-    {
-        if (pendingFetchManager.hasPendingFetch())
-        {
-            debug("Cannot reload the data with the mode '" + resultSetFetchConfig
-                    + "'; there is an unfinished request already: "
-                    + pendingFetchManager.tryTopPendingFetchConfig());
-            return;
-        }
-        pendingFetchManager.pushPendingFetchConfig(resultSetFetchConfig);
-        pagingLoader.load(0, PAGE_SIZE);
-    }
-
-    private IDisplaySettingsGetter createDisplaySettingsUpdater()
-    {
-        return new IDisplaySettingsGetter()
-            {
-                public ColumnModel getColumnModel()
-                {
-                    return AbstractBrowserGrid.this.getFullColumnModel();
-                }
-
-                public List<String> getFilteredColumnIds()
-                {
-                    return filterToolbar.extractFilteredColumnIds();
-                }
-
-                public Object getModifier()
-                {
-                    return AbstractBrowserGrid.this;
-                }
-
-                public SortInfo getSortState()
-                {
-                    return AbstractBrowserGrid.this.getGridSortInfo();
-                }
-            };
-    }
-
-    // returns true if some filters have changed
-    // Default visibility so that friend classes can use -- should otherwise be considered private
-    boolean rebuildFiltersFromIds(List<String> filteredColumnIds)
-    {
-        List<IColumnDefinition<T>> filteredColumns = getColumnDefinitions(filteredColumnIds);
-        return filterToolbar.rebuildColumnFilters(filteredColumns);
-    }
-
-    public List<IColumnDefinition<T>> getColumnDefinitions(List<String> columnIds)
-    {
-        Map<String, IColumnDefinition<T>> colsMap = asColumnIdMap(columnDefinitions);
-        List<IColumnDefinition<T>> columns = new ArrayList<IColumnDefinition<T>>();
-        for (String columnId : columnIds)
-        {
-            IColumnDefinition<T> colDef = colsMap.get(columnId);
-            assert colDef != null : "Cannot find a column '" + columnId;
-            columns.add(colDef);
-        }
-        return columns;
-    }
-
-    protected final String createGridDisplayTypeID(String suffixOrNull)
-    {
-        if (displayTypeIDGenerator == null)
-        {
-            throw new IllegalStateException("Undefined display type ID generator.");
-        }
-        if (suffixOrNull == null)
-        {
-            return displayTypeIDGenerator.createID();
-        } else
-        {
-            return displayTypeIDGenerator.createID(suffixOrNull);
-        }
-    }
-
-    private IDataRefreshCallback createRefreshCallback(
-            IDataRefreshCallback externalRefreshCallbackOrNull)
-    {
-        IDataRefreshCallback internalCallback = createInternalPostRefreshCallback();
-        if (externalRefreshCallbackOrNull == null)
-        {
-            return internalCallback;
-        } else
-        {
-            return mergeCallbacks(internalCallback, externalRefreshCallbackOrNull);
-        }
-    }
-
-    private IDataRefreshCallback createInternalPostRefreshCallback()
-    {
-        return new IDataRefreshCallback()
-            {
-                public void postRefresh(boolean wasSuccessful)
-                {
-                    if (customColumnsMetadataProvider.getHasChangedAndSetFalse())
-                    {
-                        recreateColumnModelAndRefreshColumnsWithFilters();
-                    }
-
-                    updateDefaultRefreshButton();
-
-                    if (wasSuccessful)
-                    {
-                        hideLoadingMask();
-                        pagingToolbar.updateDefaultConfigButton(true);
-                        pagingToolbar.enableExportButton();
-                    }
-                }
-            };
-    }
-
-    private static IDataRefreshCallback mergeCallbacks(final IDataRefreshCallback c1,
-            final IDataRefreshCallback c2)
-    {
-        return new IDataRefreshCallback()
-            {
-                public void postRefresh(boolean wasSuccessful)
-                {
-                    c1.postRefresh(wasSuccessful);
-                    c2.postRefresh(wasSuccessful);
-                }
-            };
-    }
-
-    private List<IColumnDefinition<T>> getVisibleColumns(Set<IColumnDefinition<T>> availableColumns)
-    {
-        Map<String, IColumnDefinition<T>> availableColumnsMap = asColumnIdMap(availableColumns);
-        return getVisibleColumns(availableColumnsMap, fullColumnModel);
-    }
-
-    private void saveCacheKey(final String newResultSetKey)
-    {
-        resultSetKeyOrNull = newResultSetKey;
-        debug("saving new cache key");
-    }
-
-    private void disposeCache()
-    {
-        if (resultSetKeyOrNull != null)
-        {
-            removeResultSet(resultSetKeyOrNull);
-            resultSetKeyOrNull = null;
-        }
-    }
-
-    private void removeResultSet(String resultSetKey)
-    {
-        viewContext.getService().removeResultSet(resultSetKey,
-                new VoidAsyncCallback<Void>(viewContext));
-    }
-
-    /**
-     * Export always deals with data from the previous refresh operation
-     * 
-     * @param allColumns whether all columns should be exported
-     */
-    private void export(boolean allColumns)
-    {
-        export(allColumns, new ExportEntitiesCallback(viewContext));
-    }
-
-    /**
-     * Shows the dialog allowing to configure visibility and order of the table columns.
-     */
-    private void configureColumnSettings()
-    {
-        assert grid != null && grid.getColumnModel() != null : "Grid must be loaded";
-        ColumnSettingsConfigurer<T, M> columnSettingsConfigurer =
-                new ColumnSettingsConfigurer<T, M>(this, viewContext, filterToolbar,
-                        customColumnsMetadataProvider, resultSetKeyOrNull,
-                        pendingFetchManager.tryTopPendingFetchConfig());
-        columnSettingsConfigurer.showDialog();
-    }
-
-    // Default visibility so that friend classes can use -- should otherwise be considered private
-    void saveColumnDisplaySettings()
-    {
-        IDisplaySettingsGetter settingsUpdater = createDisplaySettingsUpdater();
-        viewContext.getDisplaySettingsManager().storeSettings(getGridDisplayTypeID(),
-                settingsUpdater, false);
-    }
-
-    // @Private - for tests
-    public final void export(boolean allColumns, final AbstractAsyncCallback<String> callback)
-    {
-        final TableExportCriteria<T> exportCriteria = createTableExportCriteria(allColumns);
-
-        prepareExportEntities(exportCriteria, callback);
-    }
-
-    // for visible columns
-    protected final TableExportCriteria<T> createTableExportCriteria()
-    {
-        return createTableExportCriteria(false);
-    }
-
-    protected final TableExportCriteria<T> createTableExportCriteria(boolean allColumns)
-    {
-        assert columnDefinitions != null : "refresh before exporting!";
-        assert resultSetKeyOrNull != null : "refresh before exporting, resultSetKey is null!";
-
-        final List<IColumnDefinition<T>> columnDefs =
-                allColumns ? new ArrayList<IColumnDefinition<T>>(columnDefinitions)
-                        : getVisibleColumns(columnDefinitions);
-        SortInfo sortInfo = getGridSortInfo();
-        final TableExportCriteria<T> exportCriteria =
-                new TableExportCriteria<T>(resultSetKeyOrNull, sortInfo,
-                        filterToolbar.getFilters(), columnDefs, columnDefinitions,
-                        getGridDisplayTypeID());
-        return exportCriteria;
-    }
-
-    // returns info about sorting in current grid
-    SortInfo getGridSortInfo()
-    {
-        ListStore<M> store = grid.getStore();
-        return translateSortInfo(store.getSortField(), store.getSortDir());
-    }
-
-    /** @return the number of all objects cached in the browser */
-    public int getTotalCount()
-    {
-        return pagingToolbar.getTotalCount();
-    }
-
-    // Default visibility so that friend classes can use -- should otherwise be considered private
-    void refreshColumnsSettings()
-    {
-        grid.setLoadMask(false);
-        grid.getView().refresh(true);
-        grid.setLoadMask(true);
-    }
-
-    protected final void addEntityOperationsLabel()
-    {
-        pagingToolbar.addEntityOperationsLabel();
-    }
-
-    protected final void addEntityOperationsSeparator()
-    {
-        pagingToolbar.add(new SeparatorToolItem());
-    }
-
-    protected final GridCellRenderer<BaseEntityModel<?>> createMultilineStringCellRenderer()
-    {
-        return new MultilineStringCellRenderer();
-    }
-
-    @SuppressWarnings("deprecation")
-    protected GridCellRenderer<BaseEntityModel<?>> createInternalLinkCellRenderer()
-    {
-        // NOTE: this renderer doesn't support special rendering of deleted entities
-        return LinkRenderer.createLinkRenderer();
-    }
-
-    protected WebClientConfiguration getWebClientConfiguration()
-    {
-        return viewContext.getModel().getApplicationInfo().getWebClientConfiguration();
-    }
-
-    // ------- generic static helpers
-
-    private static <T> List<String> extractColumnIds(List<IColumnDefinition<T>> columns)
-    {
-        List<String> columnsIds = new ArrayList<String>();
-        for (IColumnDefinition<T> column : columns)
-        {
-            columnsIds.add(column.getIdentifier());
-        }
-        return columnsIds;
-    }
-
-    // Default visibility so that friend classes can use -- should otherwise be considered private
-    static List<ColumnDataModel> createColumnsSettingsModel(ColumnModel cm,
-            List<String> filteredColumnsIds)
-    {
-        Set<String> filteredColumnsMap = new HashSet<String>(filteredColumnsIds);
-        int cols = cm.getColumnCount();
-        List<ColumnDataModel> list = new ArrayList<ColumnDataModel>();
-        for (int i = 0; i < cols; i++)
-        {
-            if (cm.getColumnHeader(i) == null || cm.getColumnHeader(i).equals("") || cm.isFixed(i))
-            {
-                continue;
-            }
-            String columnId = cm.getColumnId(i);
-            boolean isVisible = cm.isHidden(i) == false;
-            boolean hasFilter = filteredColumnsMap.contains(columnId);
-            list.add(new ColumnDataModel(cm.getColumnHeader(i), isVisible, hasFilter, columnId));
-        }
-        return list;
-    }
-
-    private static ContentPanel createEmptyContentPanel()
-    {
-        final ContentPanel contentPanel = new ContentPanel();
-        contentPanel.setBorders(false);
-        contentPanel.setBodyBorder(false);
-        contentPanel.setLayout(new FitLayout());
-        return contentPanel;
-    }
-
-    // creates filter and paging toolbars
-    private static <T> LayoutContainer createBottomToolbars(final Container<?> parentContainer,
-            ToolBar pagingToolbar)
-    {
-        LayoutContainer bottomToolbars = new ContainerKeeper(parentContainer);
-        bottomToolbars.setMonitorWindowResize(true);
-        bottomToolbars.setLayout(new RowLayout(com.extjs.gxt.ui.client.Style.Orientation.VERTICAL));
-        // Adding paging toolbar before data are loaded fixes problems with the toolbar not visible
-        // if user quickly changes tab / hides section before it is layouted. On the other hand
-        // it is slower than adding it after requesting server for data.
-        bottomToolbars.add(pagingToolbar, new RowData(1, -1));
-        // filter toolbar is added on request
-        return bottomToolbars;
-    }
-
-    private static final class ContainerKeeper extends LayoutContainer
-    {
-        private final Container<?> parentContainer;
-
-        private ContainerKeeper(Container<?> parentContainer)
-        {
-            this.parentContainer = parentContainer;
-        }
-
-        @Override
-        protected void onWindowResize(int aWidth, int aHeight)
-        {
-            super.onWindowResize(aWidth, aHeight);
-            if (isVisible())
-            {
-                this.setWidth(parentContainer.getWidth());
-            }
-        }
-    }
-
-    private Grid<M> createGrid(PagingLoader<PagingLoadResult<M>> dataLoader, String gridId)
-    {
-
-        ListStore<M> listStore = new ListStore<M>(dataLoader);
-        ColumnModel columnModel = createColumnModel(new ArrayList<ColumnConfig>());
-        EditorGrid<M> editorGrid = new EditorGrid<M>(listStore, columnModel);
-        editorGrid.setId(gridId);
-        editorGrid.setLoadMask(true);
-        editorGrid.setSelectionModel(new GridSelectionModel<M>());
-        editorGrid.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
-        editorGrid.setView(new ExtendedGridView());
-        editorGrid.setStripeRows(true);
-        editorGrid.setColumnReordering(true);
-        editorGrid.setClicksToEdit(ClicksToEdit.TWO);
-        editorGrid.addListener(Events.BeforeEdit, new Listener<GridEvent<M>>()
-            {
-                public void handleEvent(GridEvent<M> event)
-                {
-                    if (viewContext.isSimpleOrEmbeddedMode())
-                    {
-                        MessageBox.info("Not Allowed",
-                                "Sorry, table cell editing is not allowed in current viewing mode",
-                                null);
-                        event.setCancelled(true);
-                    } else
-                    {
-                        M model = event.getModel();
-                        String columnID = event.getProperty();
-                        boolean editable = isEditable(model, columnID);
-                        if (editable == false)
-                        {
-                            showNonEditableTableCellMessage(model, columnID);
-                        }
-                        event.setCancelled(editable == false);
-                    }
-                }
-            });
-        editorGrid.addListener(Events.AfterEdit, new Listener<GridEvent<M>>()
-            {
-                public void handleEvent(GridEvent<M> event)
-                {
-                    M model = event.getModel();
-                    String columnID = event.getProperty();
-                    Object value = event.getValue();
-                    String newValueNotNull = StringUtils.toStringEmptyIfNull(value);
-                    String oldValueNotNull = StringUtils.toStringEmptyIfNull(event.getStartValue());
-                    if (oldValueNotNull.equals(newValueNotNull))
-                    {
-                        event.setCancelled(true);
-                    } else
-                    {
-                        showModificationsBar();
-                        if (value instanceof VocabularyTerm)
-                        {
-                            VocabularyTerm term = (VocabularyTerm) value;
-                            value = term.getCode();
-                        }
-                        tableModificationsManager.handleEditingEvent(model, columnID,
-                                StringUtils.toStringOrNull(value));
-                    }
-                }
-            });
-        editorGrid.addListener(Events.SortChange, new Listener<BaseEvent>()
-            {
-                public void handleEvent(BaseEvent be)
-                {
-                    saveColumnDisplaySettings();
-                }
-            });
-
-        return editorGrid;
-    }
-
-    /**
-     * Returns <code>true</code> if cell specified by model and column ID is editable. Default
-     * implementation false.
-     */
-    protected boolean isEditable(M model, String columnID)
-    {
-        return false;
-    }
-
-    /**
-     * Shows a message that the table cell of specified column and row (model) isn't editable.
-     */
-    protected void showNonEditableTableCellMessage(M model, String columnID)
-    {
-        MessageBox.info("Not Editable", "Sorry, this table cell isn't editable", null);
-    }
-
-    /**
-     * Tries to return the property of specified properties holder which is specified by the
-     * property column name without a prefix like <code>property-</code> but with prefix which
-     * distinguishes internal from externally name space.
-     */
-    protected IEntityProperty tryGetProperty(IEntityPropertiesHolder propertiesHolder,
-            String propertyColumnNameWithoutPrefix)
-    {
-        String propertyTypeCode =
-                CodeConverter.getPropertyTypeCode(propertyColumnNameWithoutPrefix);
-        List<IEntityProperty> properties = propertiesHolder.getProperties();
-        for (IEntityProperty property : properties)
-        {
-            if (property.getPropertyType().getCode().equals(propertyTypeCode))
-            {
-                return property;
-            }
-        }
-        return null;
-    }
-
-    // this should be the only place where we create the grid column model.
-    private static ColumnModel createColumnModel(List<ColumnConfig> columConfigs)
-    {
-        return new ColumnModel(columConfigs);
-    }
-
-    protected ColumnModel getFullColumnModel()
-    {
-        return fullColumnModel;
-    }
-
-    private static final class ExportEntitiesCallback extends AbstractAsyncCallback<String>
-    {
-        public ExportEntitiesCallback(final IViewContext<ICommonClientServiceAsync> viewContext)
-        {
-            super(viewContext);
-        }
-
-        @Override
-        protected void process(final String exportDataKey)
-        {
-            final URLMethodWithParameters methodWithParameters =
-                    new URLMethodWithParameters(
-                            GenericConstants.FILE_EXPORTER_DOWNLOAD_SERVLET_NAME);
-            methodWithParameters.addParameter(GenericConstants.EXPORT_CRITERIA_KEY_PARAMETER,
-                    exportDataKey);
-            WindowUtils.openWindow(methodWithParameters.toString());
-        }
-
-    }
-
-    // creates a map to quickly find a definition by its identifier
-    private static <T> Map<String, IColumnDefinition<T>> asColumnIdMap(
-            final Set<IColumnDefinition<T>> defs)
-    {
-        final Map<String, IColumnDefinition<T>> map = new HashMap<String, IColumnDefinition<T>>();
-        for (final IColumnDefinition<T> def : defs)
-        {
-            map.put(def.getIdentifier(), def);
-        }
-        return map;
-    }
-
-    /**
-     * @param availableColumns map of all available columns definitions.
-     * @param columnModel describes the visual properties of the columns. Connected with
-     *            availableColumns by column id.
-     * @return list of columns definitions for those columns which are currently shown
-     */
-    private static <T/* column definition */> List<T> getVisibleColumns(
-            final Map<String, T> availableColumns, final ColumnModel columnModel)
-    {
-        final List<T> selectedColumnDefs = new ArrayList<T>();
-        final int columnCount = columnModel.getColumnCount();
-        for (int i = 0; i < columnCount; i++)
-        {
-            if (columnModel.isHidden(i) == false)
-            {
-                final String columnId = columnModel.getColumnId(i);
-                selectedColumnDefs.add(availableColumns.get(columnId));
-            }
-        }
-        return selectedColumnDefs;
-    }
-
-    /** Creates callback that refreshes the grid. */
-    protected final AbstractAsyncCallback<Void> createRefreshCallback(
-            IBrowserGridActionInvoker invoker)
-    {
-        return new RefreshCallback(viewContext, invoker);
-    }
-
-    /** Callback that refreshes the grid. */
-    private static final class RefreshCallback extends AbstractAsyncCallback<Void>
-    {
-        private final IBrowserGridActionInvoker invoker;
-
-        public RefreshCallback(IViewContext<?> viewContext, IBrowserGridActionInvoker invoker)
-        {
-            super(viewContext);
-            this.invoker = invoker;
-        }
-
-        @Override
-        protected void process(Void result)
-        {
-            invoker.refresh();
-        }
-    }
-
-    /** {@link SelectionListener} that creates a dialog with selected data items. */
-    protected abstract class AbstractCreateDialogListener extends SelectionListener<ButtonEvent>
-    {
-        @Override
-        public void componentSelected(ButtonEvent ce)
-        {
-            List<T> data = getSelectedBaseObjects();
-            IBrowserGridActionInvoker invoker = asActionInvoker();
-            if (validateSelectedData(data))
-            {
-                createDialog(data, invoker).show();
-            }
-        }
-
-        /**
-         * If specified data is valid returns true, otherwise returns false. Dialog will be shown
-         * only if this method returns true. Default implementation always returns true.
-         */
-        protected boolean validateSelectedData(List<T> data)
-        {
-            return true;
-        }
-
-        protected abstract Dialog createDialog(List<T> data, IBrowserGridActionInvoker invoker);
-    }
-
-    //
-    // Table Modifications
-    //
-
-    /**
-     * Apply specified modifications to the model. Should be overriden by subclasses. Default
-     * implementation does nothing.
-     */
-    protected void applyModifications(M model, List<IModification> modifications)
-    {
-
-    }
-
-    /** Manager of table modifications */
-    interface ITableModificationsManager<M extends ModelData>
-    {
-        /** @return <code>true</code> iff there are any uncommitted modifications */
-        boolean isTableDirty();
-
-        /** save all modifications made in the table to the DB */
-        void saveModifications();
-
-        /** save all modifications made in the table to the DB and call the after save action */
-        void saveModifications(IDelegatedAction afterSaveAction);
-
-        /** cancel all modifications made in the table */
-        void cancelModifications();
-
-        /** handle cell editing event */
-        void handleEditingEvent(M model, String columnID, String stringOrNull);
-
-        /** @return callback for given modifications made to specified model. */
-        AsyncCallback<EntityPropertyUpdatesResult> createApplyModificationsCallback(final M model,
-                final List<IModification> modifications);
-    }
-
-    protected interface IModification
-    {
-        String getColumnID();
-
-        String tryGetNewValue();
-    }
-
-    private static class Modification implements IModification
-    {
-        private final String columnID;
-
-        private final String newValueOrNull;
-
-        public Modification(String columnID, String newValueOrNull)
-        {
-            super();
-            this.columnID = columnID;
-            this.newValueOrNull =
-                    newValueOrNull == null ? null : StringEscapeUtils.unescapeHtml(newValueOrNull);
-        }
-
-        public String getColumnID()
-        {
-            return columnID;
-        }
-
-        public String tryGetNewValue()
-        {
-            return newValueOrNull;
-        }
-    }
-
-    private class TableModificationsManager implements ITableModificationsManager<M>
-    {
-        private final Map<M, List<IModification>> modificationsByModel =
-                new LinkedHashMap<M, List<IModification>>();
-
-        private final Map<M, String> failedModifications = new HashMap<M, String>();
-
-        private int finishedModifications = 0;
-
-        private IDelegatedAction afterSaveActionOrNull;
-
-        //
-        // ITableModificationsManager
-        //
-
-        // @Override
-        public boolean isTableDirty()
-        {
-            return isApplyModificationsComplete() == false;
-        }
-
-        public void saveModifications()
-        {
-            saveModifications(null);
-        }
-
-        private void setAfterSaveAction(IDelegatedAction afterSaveAction)
-        {
-            this.afterSaveActionOrNull = afterSaveAction;
-        }
-
-        public void saveModifications(IDelegatedAction afterSaveAction)
-        {
-            setAfterSaveAction(afterSaveAction);
-            finishedModifications = 0;
-            for (Entry<M, List<IModification>> entry : modificationsByModel.entrySet())
-            {
-                applyModifications(entry.getKey(), entry.getValue());
-            }
-        }
-
-        public void cancelModifications()
-        {
-            clearModifications();
-            grid.getStore().rejectChanges();
-            refresh(); // WORKAROUND remove this refresh after LMS-2397 is resolved
-        }
-
-        public void handleEditingEvent(M model, String columnID, String newValueOrNull)
-        {
-            List<IModification> modificationsForModel = modificationsByModel.get(model);
-            if (modificationsForModel == null)
-            {
-                modificationsForModel = new ArrayList<IModification>();
-                modificationsByModel.put(model, modificationsForModel);
-            }
-            modificationsForModel.add(new Modification(columnID, newValueOrNull));
-        }
-
-        public AsyncCallback<EntityPropertyUpdatesResult> createApplyModificationsCallback(
-                final M model, final List<IModification> modifications)
-        {
-            return new AbstractAsyncCallback<EntityPropertyUpdatesResult>(viewContext)
-                {
-                    @Override
-                    protected void process(EntityPropertyUpdatesResult result)
-                    {
-                        finishedModifications++;
-                        String errorMessage = result.tryGetErrorMessage();
-                        if (errorMessage != null)
-                        {
-                            handleError(errorMessage);
-                        }
-                        if (isApplyModificationsComplete())
-                        {
-                            onApplyModificationsComplete();
-                        }
-                    }
-
-                    @Override
-                    public void finishOnFailure(Throwable caught)
-                    {
-                        finishedModifications++;
-                        handleError(caught.getMessage());
-                        if (isApplyModificationsComplete())
-                        {
-                            onApplyModificationsComplete();
-                        }
-                    }
-
-                    private void handleError(String errorMessage)
-                    {
-                        failedModifications.put(model, errorMessage);
-                    }
-                };
-        }
-
-        //
-
-        private boolean isApplyModificationsComplete()
-        {
-            return finishedModifications == modificationsByModel.size();
-        }
-
-        private void onApplyModificationsComplete()
-        {
-            if (failedModifications.size() > 0)
-            {
-                String failureTitle =
-                        (failedModifications.size() == modificationsByModel.size()) ? "Operation failed"
-                                : "Operation partly failed";
-                String failureReport = createFailedModificationsReport();
-                MessageBox.alert(failureTitle, failureReport, null);
-                refresh();
-            } else
-            {
-                GWTUtils.displayInfo("All modifications successfully applied.");
-                grid.getStore().commitChanges(); // no need to refresh - everything should be valid
-            }
-            clearModifications();
-            if (afterSaveActionOrNull != null)
-            {
-                afterSaveActionOrNull.execute();
-            }
-        }
-
-        private String createFailedModificationsReport()
-        {
-            assert failedModifications.size() > 0;
-            StringBuilder result = new StringBuilder();
-            result.append("Modifications of " + failedModifications.size() + " entities failed:");
-            for (String error : failedModifications.values())
-            {
-                result.append("<br/>- " + error);
-            }
-            return result.toString();
-        }
-
-        private void clearModifications()
-        {
-            finishedModifications = 0;
-            failedModifications.clear();
-            modificationsByModel.clear();
-            hideModificationsBar();
-        }
-
-    }
-
-    /** Toolbar for handling table modifications */
-    static class TableModificationsToolbar extends ToolBar
-    {
-
-        public TableModificationsToolbar(final IMessageProvider messageProvider,
-                final ITableModificationsManager<?> manager)
-        {
-            add(new Label(messageProvider.getMessage(Dict.TABLE_MODIFICATIONS)));
-
-            final AbstractImagePrototype confirmIcon =
-                    AbstractImagePrototype.create(IMAGE_BUNDLE.getConfirmIcon());
-            final AbstractImagePrototype cancelIcon =
-                    AbstractImagePrototype.create(IMAGE_BUNDLE.getCancelIcon());
-            add(new Button("Save", confirmIcon, new SelectionListener<ButtonEvent>()
-                {
-                    @Override
-                    public void componentSelected(ButtonEvent be)
-                    {
-                        manager.saveModifications();
-                    }
-                }));
-            add(new Button("Cancel", cancelIcon, new SelectionListener<ButtonEvent>()
-                {
-                    @Override
-                    public void componentSelected(ButtonEvent be)
-                    {
-                        manager.cancelModifications();
-                    }
-                }));
-        }
-
-    }
-
-}
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/BrowserGridPagingToolBar.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/BrowserGridPagingToolBar.java
index 6ca167983d4..e5ad449fb1d 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/BrowserGridPagingToolBar.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/BrowserGridPagingToolBar.java
@@ -217,12 +217,12 @@ public final class BrowserGridPagingToolBar extends PagingToolBar
         disable();
     }
 
-    protected final void updateDefaultRefreshButton(boolean isEnabled)
+    public final void updateDefaultRefreshButton(boolean isEnabled)
     {
         updateRefreshButton(refreshButton, isEnabled, messageProvider);
     }
 
-    protected final void updateDefaultConfigButton(boolean isEnabled)
+    public final void updateDefaultConfigButton(boolean isEnabled)
     {
         updateConfigButton(configButton, isEnabled, messageProvider);
     }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/CustomColumnsMetadataProvider.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/CustomColumnsMetadataProvider.java
index 00b1f45afc0..7f25604c1f4 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/CustomColumnsMetadataProvider.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/CustomColumnsMetadataProvider.java
@@ -28,7 +28,7 @@ import ch.systemsx.cisd.openbis.generic.client.web.client.dto.GridCustomColumnIn
  * 
  * @author Tomasz Pylak
  */
-class CustomColumnsMetadataProvider
+public class CustomColumnsMetadataProvider
 {
     // what custom columns are present in the grid. Used to build column definitions.
     private List<GridCustomColumnInfo> customColumnsMetadata =
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/ExtendedGridView.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/ExtendedGridView.java
index 65802c784f1..be9fb62c8b7 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/ExtendedGridView.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/ExtendedGridView.java
@@ -21,7 +21,7 @@ import com.google.gwt.user.client.Element;
  * 
  * @author Izabela Adamczyk
  */
-class ExtendedGridView extends GridView
+public class ExtendedGridView extends GridView
 {
     @Override
     protected ColumnHeader newColumnHeader()
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/PendingFetchManager.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/PendingFetchManager.java
index e1de0a17bbb..4ac57765c2c 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/PendingFetchManager.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/PendingFetchManager.java
@@ -23,7 +23,7 @@ import ch.systemsx.cisd.openbis.generic.client.web.client.dto.ResultSetFetchConf
  * 
  * @author Chandrasekhar Ramakrishnan
  */
-class PendingFetchManager
+public class PendingFetchManager
 {
     // The current pending fetch, or null if there is none.
     private ResultSetFetchConfig<String> pendingFetchConfigOrNull = null;
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/widget/AbstractGridDataRefreshCallback.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/widget/AbstractGridDataRefreshCallback.java
index 8c579ec04fd..9c01cf8b0c4 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/widget/AbstractGridDataRefreshCallback.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/widget/AbstractGridDataRefreshCallback.java
@@ -16,26 +16,27 @@
 
 package ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.widget;
 
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.model.BaseEntityModel;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.AbstractBrowserGrid;
+import java.io.Serializable;
+
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.TypedTableGrid;
 
 /**
- * Abstract {@link IDataRefreshCallback} implementation for refreshing {@link AbstractBrowserGrid}.
+ * Abstract {@link IDataRefreshCallback} implementation for refreshing {@link TypedTableGrid}.
  * 
  * @author Piotr Buczek
  */
-public abstract class AbstractGridDataRefreshCallback<T/* Entity */> implements
-        IDataRefreshCallback
+public abstract class AbstractGridDataRefreshCallback<T extends Serializable/* Entity */>
+        implements IDataRefreshCallback
 {
-    private AbstractBrowserGrid<T, BaseEntityModel<T>> grid;
+    private TypedTableGrid<T> grid;
 
-    public AbstractBrowserGrid<T, BaseEntityModel<T>> getGrid()
+    public TypedTableGrid<T> getGrid()
     {
         assert grid != null : "grid not set!";
         return grid;
     }
 
-    public void setGrid(AbstractBrowserGrid<T, BaseEntityModel<T>> grid)
+    public void setGrid(TypedTableGrid<T> grid)
     {
         this.grid = grid;
     }
diff --git a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/AuthorizationManagementConsolTest.java b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/AuthorizationManagementConsolTest.java
index 387bf760395..3b2428f37d8 100644
--- a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/AuthorizationManagementConsolTest.java
+++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/AuthorizationManagementConsolTest.java
@@ -19,6 +19,7 @@ package ch.systemsx.cisd.openbis.generic.client.web.client.application;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.menu.TopMenu.ActionMenuKind;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.TypedTableGrid;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.amc.CheckGroupTable;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.amc.CheckPersonTable;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.amc.CheckRoleAssignmentTable;
@@ -27,7 +28,6 @@ import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.amc.Cre
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.amc.FillRoleAssignmentForm;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.amc.OpenRoleAssignmentDialog;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.amc.RoleAssignmentRow;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.AbstractBrowserGrid;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.PersonGridColumnIDs;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.SpaceGridColumnIDs;
 import ch.systemsx.cisd.openbis.generic.client.web.client.testframework.AbstractGWTTestCase;
@@ -114,7 +114,7 @@ public class AuthorizationManagementConsolTest extends AbstractGWTTestCase
 
         FailureExpectation failureExpectation =
                 new FailureExpectation(
-                        (Class<? extends AsyncCallback<?>>) AbstractBrowserGrid.ListEntitiesCallback.class)
+                        (Class<? extends AsyncCallback<?>>) TypedTableGrid.ListEntitiesCallback.class)
                         .with("Authorization failure: None of method roles '[INSTANCE_ADMIN]' "
                                 + "could be found in roles of user 'o'.");
 
diff --git a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/SampleBrowserTest.java b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/SampleBrowserTest.java
index 63341ce1850..846c40f32c3 100644
--- a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/SampleBrowserTest.java
+++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/SampleBrowserTest.java
@@ -17,11 +17,11 @@
 package ch.systemsx.cisd.openbis.generic.client.web.client.application;
 
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.menu.TopMenu.ActionMenuKind;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.ColumnSettingsConfigurer;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.InvokeActionMenu;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.Login;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.Logout;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.SpaceSelectionWidget;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.ColumnSettingsConfigurer;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.sample.CheckSampleTable;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.sample.ExportSamplesTestCommand;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.sample.ListSamples;
diff --git a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/util/GridTestUtils.java b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/util/GridTestUtils.java
index a1030977338..97a4e3fedc1 100644
--- a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/util/GridTestUtils.java
+++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/util/GridTestUtils.java
@@ -25,9 +25,9 @@ import com.extjs.gxt.ui.client.store.ListStore;
 import com.extjs.gxt.ui.client.widget.button.Button;
 import com.extjs.gxt.ui.client.widget.grid.Grid;
 
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.TypedTableGrid;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.columns.framework.EntityPropertyColDef;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.columns.specific.data.DataSetPropertyColDef;
-import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.AbstractBrowserGrid;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.BrowserGridPagingToolBar;
 import ch.systemsx.cisd.openbis.generic.client.web.client.testframework.GWTTestUtil;
 import ch.systemsx.cisd.openbis.generic.client.web.client.testframework.TestUtil;
@@ -35,7 +35,7 @@ import ch.systemsx.cisd.openbis.generic.shared.basic.dto.PropertyType;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Sample;
 
 /**
- * Utility methods to test subclasses of {@link AbstractBrowserGrid}
+ * Utility methods to test subclasses of {@link TypedTableGrid}
  * 
  * @author Tomasz Pylak
  */
-- 
GitLab