    001// Licensed under the Apache License, Version 2.0 (the "License");
    002// you may not use this file except in compliance with the License.
    003// You may obtain a copy of the License at
    004//
    005// http://www.apache.org/licenses/LICENSE-2.0
    006//
    007// Unless required by applicable law or agreed to in writing, software
    008// distributed under the License is distributed on an "AS IS" BASIS,
    009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    010// See the License for the specific language governing permissions and
    011// limitations under the License.
    012
    013package org.apache.tapestry5.corelib.components;
    014
    015import org.apache.tapestry5.*;
    016import org.apache.tapestry5.annotations.*;
    017import org.apache.tapestry5.beanmodel.BeanModel;
    018import org.apache.tapestry5.beanmodel.BeanModelUtils;
    019import org.apache.tapestry5.beanmodel.PropertyModel;
    020import org.apache.tapestry5.beanmodel.services.BeanModelSource;
    021import org.apache.tapestry5.corelib.data.GridPagerPosition;
    022import org.apache.tapestry5.grid.*;
    023import org.apache.tapestry5.internal.TapestryInternalUtils;
    024import org.apache.tapestry5.internal.bindings.AbstractBinding;
    025import org.apache.tapestry5.ioc.annotations.Inject;
    026import org.apache.tapestry5.ioc.internal.util.InternalUtils;
    027import org.apache.tapestry5.services.ComponentDefaultProvider;
    028import org.apache.tapestry5.services.ComponentEventResultProcessor;
    029import org.apache.tapestry5.services.FormSupport;
    030import org.apache.tapestry5.services.javascript.JavaScriptSupport;
    031
    032import java.io.IOException;
    033import java.util.Collections;
    034import java.util.List;
    035
    036/**
    037 * A grid presents tabular data. It is a composite component, created in terms of several sub-components. The
    038 * sub-components are statically wired to the Grid, as it provides access to the data and other models that they need.
    039 *
    040 * A Grid may operate inside a {@link org.apache.tapestry5.corelib.components.Form}. By overriding the cell renderers of
    041 * properties, the default output-only behavior can be changed to produce a complex form with individual control for
    042 * editing properties of each row. There is a big caveat here: if the order of rows provided by
    043 * the {@link org.apache.tapestry5.grid.GridDataSource} changes between render and form submission, then there's the
    044 * possibility that data will be applied to the wrong server-side objects.
    045 *
    046 * For this reason, when using Grid and Form together, you should generally
    047 * provide the Grid with a {@link org.apache.tapestry5.ValueEncoder} (via the
    048 * encoder parameter), or use an entity type for the "row" parameter for which
    049 * Tapestry can provide a ValueEncoder automatically. This will allow Tapestry
    050 * to use a unique ID for each row that doesn't change when rows are reordered.
    051 *
    052 * @tapestrydoc
    053 * @see org.apache.tapestry5.beanmodel.BeanModel
    054 * @see org.apache.tapestry5.beanmodel.services.BeanModelSource
    055 * @see org.apache.tapestry5.grid.GridDataSource
    056 * @see BeanEditForm
    057 * @see BeanDisplay
    058 * @see Loop
    059 */
    060@SupportsInformalParameters
    061public class Grid implements GridModel, ClientElement
    062{
    063    /**
    064     * The source of data for the Grid to display. This will usually be a List or array but can also be an explicit
    065     * {@link GridDataSource}. For Lists and object arrays, a GridDataSource is created automatically as a wrapper
    066     * around the underlying List.
    067     */
    068    @Parameter(required = true, autoconnect = true)
    069    private GridDataSource source;
    070
    071    /**
    072     * A wrapper around the provided GridDataSource that caches access to the availableRows property. This is the source
    073     * provided to sub-components.
    074     */
    075    private GridDataSource cachingSource;
    076
    077    /**
    078     * The number of rows of data displayed on each page. If there are more rows than will fit, the Grid will divide up
    079     * the rows into "pages" and (normally) provide a pager to allow the user to navigate within the overall result
    080     * set.
    081     */
    082    @Parameter(BindingConstants.SYMBOL + ":" + ComponentParameterConstants.GRID_ROWS_PER_PAGE)
    083    private int rowsPerPage;
    084
    085    /**
    086     * Defines where the pager (used to navigate within the "pages" of results) should be displayed: "top", "bottom",
    087     * "both" or "none".
    088     */
    089    @Parameter(value = BindingConstants.SYMBOL + ":" + ComponentParameterConstants.GRID_PAGER_POSITION,
    090            defaultPrefix = BindingConstants.LITERAL)
    091    private GridPagerPosition pagerPosition;
    092
    093    /**
    094     * Used to store the current object being rendered (for the current row). This is used when parameter blocks are
    095     * provided to override the default cell renderer for a particular column ... the components within the block can
    096     * use the property bound to the row parameter to know what they should render.
    097     */
    098    @Parameter(principal = true)
    099    private Object row;
    100
    101    /**
    102     * Optional output parameter used to identify the index of the column being rendered.
    103     */
    104    @Parameter
    105    private int columnIndex;
    106
    107    /**
    108     * The model used to identify the properties to be presented and the order of presentation. The model may be
    109     * omitted, in which case a default model is generated from the first object in the data source (this implies that
    110     * the objects provided by the source are uniform). The model may be explicitly specified to override the default
    111     * behavior, say to reorder or rename columns or add additional columns. The add, include,
    112     * exclude and reorder
    113     * parameters are <em>only</em> applied to a default model, not an explicitly provided one.
    114     */
    115    @Parameter
    116    private BeanModel model;
    117
    118    /**
    119     * The model parameter after modification due to the add, include, exclude and reorder parameters.
    120     */
    121    private BeanModel dataModel;
    122
    123    /**
    124     * The model used to handle sorting of the Grid. This is generally not specified, and the built-in model supports
    125     * only single column sorting. The sort constraints (the column that is sorted, and ascending vs. descending) is
    126     * stored as persistent fields of the Grid component.
    127     */
    128    @Parameter
    129    private GridSortModel sortModel;
    130
    131    /**
    132     * A comma-separated list of property names to be added to the {@link org.apache.tapestry5.beanmodel.BeanModel}.
    133     * Cells for added columns will be blank unless a cell override is provided. This parameter is only used
    134     * when a default model is created automatically.
    135     */
    136    @Parameter(defaultPrefix = BindingConstants.LITERAL)
    137    private String add;
    138
    139    /**
    140     * A comma-separated list of property names to be retained from the
    141     * {@link org.apache.tapestry5.beanmodel.BeanModel}.
    142     * Only these properties will be retained, and the properties will also be reordered. The names are
    143     * case-insensitive. This parameter is only used
    144     * when a default model is created automatically.
    145     */
    146    @SuppressWarnings("unused")
    147    @Parameter(defaultPrefix = BindingConstants.LITERAL)
    148    private String include;
    149
    150    /**
    151     * A comma-separated list of property names to be removed from the {@link org.apache.tapestry5.beanmodel.BeanModel}
    152     * .
    153     * The names are case-insensitive. This parameter is only used
    154     * when a default model is created automatically.
    155     */
    156    @Parameter(defaultPrefix = BindingConstants.LITERAL)
    157    private String exclude;
    158
    159    /**
    160     * A comma-separated list of property names indicating the order in which the properties should be presented. The
    161     * names are case insensitive. Any properties not indicated in the list will be appended to the end of the display
    162     * order. This parameter is only used
    163     * when a default model is created automatically.
    164     */
    165    @Parameter(defaultPrefix = BindingConstants.LITERAL)
    166    private String reorder;
    167
    168    /**
    169     * A Block to render instead of the table (and pager, etc.) when the source is empty. The default is simply the text
    170     * "There is no data to display". This parameter is used to customize that message, possibly including components to
    171     * allow the user to create new objects.
    172     */
    173    //@Parameter(value = BindingConstants.SYMBOL + ":" + ComponentParameterConstants.GRID_EMPTY_BLOCK,
    174    @Parameter(value = "block:empty",
    175            defaultPrefix = BindingConstants.LITERAL)
    176    @Property(write = false)
    177    private Block empty;
    178
    179    /**
    180     * CSS class for the &lt;table&gt; element. In addition, informal parameters to the Grid are rendered in the table
    181     * element.
    182     */
    183    @Parameter(name = "class", defaultPrefix = BindingConstants.LITERAL,
    184            value = BindingConstants.SYMBOL + ":" + ComponentParameterConstants.GRID_TABLE_CSS_CLASS)
    185    @Property(write = false)
    186    private String tableClass;
    187
    188    /**
    189     * If true, then the Grid will be wrapped in an element that acts like a
    190     * {@link org.apache.tapestry5.corelib.components.Zone}; all the paging and sorting links will refresh the zone,
    191     * repainting the entire grid within it, but leaving the rest of the page (outside the zone) unchanged.
    192     */
    193    @Parameter
    194    private boolean inPlace;
    195
    196    /**
    197     * If true, then the Grid will also render a table element complete with headers if the data source is empty.
    198     * If set to true, a model parameter will have to be specified. A default model for a specific class can be
    199     * created using {@link BeanModelSource#createDisplayModel(Class, org.apache.tapestry5.commons.Messages)}.
    200     */
    201    @Parameter
    202    private boolean renderTableIfEmpty = false;
    203
    204
    205    /**
    206     * The name of the pseudo-zone that encloses the Grid. Starting in 5.4, this is always either
    207     * null or "^" and is not really used the way it was in 5.3; instead it triggers the addition
    208     * of a {@code data-inplace-grid-links} attribute in a div surrounding any links related to
    209     * sorting or pagination. The rest is sorted out on the client. See module {@code t5/core/zone}.
    210     */
    211    @Property(write = false)
    212    private String zone;
    213
    214    private boolean didRenderZoneDiv;
    215
    216
    217    /**
    218     * The pagination model for the Grid, which encapsulates current page, sort column id,
    219     * and sort ascending/descending. If not bound, a persistent property of the Grid is used.
    220     * When rendering the Grid in a loop, this should be bound in some way to keep successive instances
    221     * of the Grid configured individually.
    222     *
    223     * @since 5.4
    224     */
    225    @Parameter(value = "defaultPaginationModel")
    226    private GridPaginationModel paginationModel;
    227
    228    @Persist
    229    private GridPaginationModel defaultPaginationModel;
    230
    231    @Inject
    232    private ComponentResources resources;
    233
    234    @Inject
    235    private BeanModelSource modelSource;
    236
    237    @Environmental
    238    private JavaScriptSupport javaScriptSupport;
    239
    240    @Component(parameters =
    241            {"index=inherit:columnIndex", "lean=inherit:lean", "overrides=overrides", "zone=zone"})
    242    private GridColumns columns;
    243
    244    @Component(parameters =
    245            {"columnIndex=inherit:columnIndex", "rowsPerPage=rowsPerPage", "currentPage=currentPage", "row=row",
    246                    "overrides=overrides"}, publishParameters = "rowIndex,rowClass,volatile,encoder,lean")
    247    private GridRows rows;
    248
    249    @Component(parameters =
    250            {"source=dataSource", "rowsPerPage=rowsPerPage", "currentPage=currentPage", "zone=zone"})
    251    private GridPager pager;
    252
    253    @Component(parameters = "to=pagerTop")
    254    private Delegate pagerTop;
    255
    256    @Component(parameters = "to=pagerBottom")
    257    private Delegate pagerBottom;
    258
    259    @Component(parameters = "class=tableClass", inheritInformalParameters = true)
    260    private Any table;
    261
    262    @Environmental(false)
    263    private FormSupport formSupport;
    264
    265    /**
    266     * Defines where block and label overrides are obtained from. By default, the Grid component provides block
    267     * overrides (from its block parameters).
    268     */
    269    @Parameter(value = "this", allowNull = false)
    270    @Property(write = false)
    271    private PropertyOverrides overrides;
    272
    273    /**
    274     * Set up via the traditional or Ajax component event request handler
    275     */
    276    @Environmental
    277    private ComponentEventResultProcessor componentEventResultProcessor;
    278
    279    @Inject
    280    private ComponentDefaultProvider defaultsProvider;
    281
    282    ValueEncoder defaultEncoder()
    283    {
    284        return defaultsProvider.defaultValueEncoder("row", resources);
    285    }
    286
    287    /**
    288     * A version of GridDataSource that caches the availableRows and empty properties. This addresses TAPESTRY-2245.
    289     */
    290    static class CachingDataSource implements GridDataSource
    291    {
    292        private final GridDataSource delegate;
    293
    294        private boolean availableRowsCached;
    295
    296        private int availableRows;
    297
    298        private boolean emptyCached;
    299
    300        private boolean empty;
    301
    302        CachingDataSource(GridDataSource delegate)
    303        {
    304            this.delegate = delegate;
    305        }
    306
    307        @Override
    308        public boolean isEmpty()
    309        {
    310            if (!emptyCached)
    311            {
    312                empty = delegate.isEmpty();
    313                emptyCached = true;
    314                if (empty)
    315                {
    316                    availableRows = 0;
    317                    availableRowsCached = true;
    318                }
    319            }
    320
    321            return empty;
    322        }
    323
    324        @Override
    325        public int getAvailableRows(int limit)
    326        {
    327            if (!availableRowsCached)
    328            {
    329                int result = delegate.getAvailableRows(limit);
    330                if (result == 0)
    331                {
    332                    empty = true;
    333                    emptyCached = true;
    334                } else {
    335                    empty = false;
    336                    emptyCached = true;
    337                }
    338                if (result < limit) {
    339                    availableRows = result;
    340                    availableRowsCached = true;
    341                }
    342                return result;
    343            } else {
    344              return Math.min(availableRows, limit);
    345            }
    346        }
    347
    348        public int getAvailableRows()
    349        {
    350            if (!availableRowsCached)
    351            {
    352                availableRows = delegate.getAvailableRows();
    353                availableRowsCached = true;
    354                if (availableRows == 0)
    355                {
    356                    empty = true;
    357                    emptyCached = true;
    358                } else {
    359                  empty = false;
    360                  emptyCached = true;
    361              }
    362            }
    363
    364            return availableRows;
    365        }
    366
    367        public void prepare(int startIndex, int endIndex, List<SortConstraint> sortConstraints)
    368        {
    369            delegate.prepare(startIndex, endIndex, sortConstraints);
    370        }
    371
    372        public Object getRowValue(int index)
    373        {
    374            return delegate.getRowValue(index);
    375        }
    376
    377        public Class getRowType()
    378        {
    379            return delegate.getRowType();
    380        }
    381    }
    382
    383    /**
    384     * Default implementation that only allows a single column to be the sort column, and stores the sort information as
    385     * persistent fields of the Grid component.
    386     */
    387    class DefaultGridSortModel implements GridSortModel
    388    {
    389        public ColumnSort getColumnSort(String columnId)
    390        {
    391            if (paginationModel == null || !TapestryInternalUtils.isEqual(columnId, paginationModel.getSortColumnId()))
    392            {
    393                return ColumnSort.UNSORTED;
    394            }
    395
    396            return getColumnSort();
    397        }
    398
    399        private ColumnSort getColumnSort()
    400        {
    401            return getSortAscending() ? ColumnSort.ASCENDING : ColumnSort.DESCENDING;
    402        }
    403
    404        public void updateSort(String columnId)
    405        {
    406            assert InternalUtils.isNonBlank(columnId);
    407
    408            if (columnId.equals(paginationModel.getSortColumnId()))
    409            {
    410                setSortAscending(!getSortAscending());
    411                return;
    412            }
    413
    414            paginationModel.setSortColumnId(columnId);
    415            setSortAscending(true);
    416        }
    417
    418        public List<SortConstraint> getSortConstraints()
    419        {
    420            // In a few limited cases we may not have yet hit the SetupRender phase, and the model may be null.
    421            // Furthermore, because the sort column id is cached, an unknown value exception must be prevented in case of dynamically created bean models (e.g. usage of the 'include' parameter)
    422            final BeanModel dataModel = getDataModel();
    423
    424            if ((paginationModel == null || paginationModel.getSortColumnId() == null) || !(dataModel.getPropertyNames().contains(TapestryInternalUtils.extractIdFromPropertyExpression(paginationModel.getSortColumnId()))))
    425            {
    426                return Collections.emptyList();
    427            }
    428
    429            PropertyModel sortModel = dataModel.getById(paginationModel.getSortColumnId());
    430
    431            SortConstraint constraint = new SortConstraint(sortModel, getColumnSort());
    432
    433            return Collections.singletonList(constraint);
    434        }
    435
    436        public void clear()
    437        {
    438            paginationModel.setSortColumnId(null);
    439            paginationModel.setSortAscending(null);
    440        }
    441    }
    442
    443    GridSortModel defaultSortModel()
    444    {
    445        return new DefaultGridSortModel();
    446    }
    447
    448    /**
    449     * Returns a {@link org.apache.tapestry5.Binding} instance that attempts to identify the model from the source
    450     * parameter (via {@link org.apache.tapestry5.grid.GridDataSource#getRowType()}. Subclasses may override to provide
    451     * a different mechanism. The returning binding is variant (not invariant).
    452     *
    453     * @see BeanModelSource#createDisplayModel(Class, org.apache.tapestry5.commons.Messages)
    454     */
    455    protected Binding defaultModel()
    456    {
    457
    458        return new AbstractBinding()
    459        {
    460            public Object get()
    461            {
    462                // Get the default row type from the data source
    463
    464                GridDataSource gridDataSource = source;
    465
    466                Class rowType = gridDataSource.getRowType();
    467
    468                if (renderTableIfEmpty || rowType == null)
    469                    throw new RuntimeException(
    470                            String.format(
    471                                    "Unable to determine the bean type for rows from %s. You should bind the model parameter explicitly.",
    472                                    gridDataSource));
    473
    474                // Properties do not have to be read/write
    475
    476                return modelSource.createDisplayModel(rowType, overrides.getOverrideMessages());
    477            }
    478
    479            /**
    480             * Returns false. This may be overkill, but it basically exists because the model is
    481             * inherently mutable and therefore may contain client-specific state and needs to be
    482             * discarded at the end of the request. If the model were immutable, then we could leave
    483             * invariant as true.
    484             */
    485            @Override
    486            public boolean isInvariant()
    487            {
    488                return false;
    489            }
    490        };
    491    }
    492
    493    static final ComponentAction<Grid> SETUP_DATA_SOURCE = new ComponentAction<Grid>()
    494    {
    495        private static final long serialVersionUID = 8545187927995722789L;
    496
    497        public void execute(Grid component)
    498        {
    499            component.setupDataSource();
    500        }
    501
    502        @Override
    503        public String toString()
    504        {
    505            return "Grid.SetupDataSource";
    506        }
    507    };
    508
    509    Object setupRender()
    510    {
    511        if (formSupport != null)
    512        {
    513            formSupport.store(this, SETUP_DATA_SOURCE);
    514        }
    515
    516        setupDataSource();
    517
    518        // If there's no rows, display the empty block placeholder.
    519
    520        return !renderTableIfEmpty && cachingSource.isEmpty() ? empty : null;
    521    }
    522
    523    void cleanupRender()
    524    {
    525        // if an inPlace Grid is rendered inside a Loop, be sure to generate a new wrapper
    526        // zone for each iteration (TAP5-2256)
    527        zone = null;
    528
    529        // If grid is rendered inside a Loop. be sure to generate a new data model for
    530        // each iteration (TAP5-2470)
    531        dataModel = null;
    532    }
    533
    534    public GridPaginationModel getDefaultPaginationModel()
    535    {
    536        if (defaultPaginationModel == null)
    537        {
    538            defaultPaginationModel = new GridPaginationModelImpl();
    539        }
    540
    541        return defaultPaginationModel;
    542    }
    543
    544    void setupDataSource()
    545    {
    546        // TAP5-34: We pass the source into the CachingDataSource now; previously
    547        // we were accessing source directly, but during submit the value wasn't
    548        // cached, and therefore access was very inefficient, and sorting was
    549        // very inconsistent during the processing of the form submission.
    550
    551        int effectiveCurrentPage = getCurrentPage();
    552
    553        int numberOfRowsRequiredToShowCurrentPage = 1 + (effectiveCurrentPage - 1) * rowsPerPage;
    554        int numberOfRowsRequiredToFillCurrentPage = effectiveCurrentPage * rowsPerPage;
    555
    556        cachingSource = new CachingDataSource(source);
    557        if (pagerPosition != GridPagerPosition.NONE)
    558        {
    559            // We're going to render the pager, so we need to determine the total number of rows anyway.
    560            // We do that eagerly here so we don't have to perform two count operations; the subsequent
    561            // ones will return a cached result
    562            cachingSource.getAvailableRows();
    563        }
    564        int availableRowsWithLimit = cachingSource.getAvailableRows(numberOfRowsRequiredToFillCurrentPage);
    565
    566        if (availableRowsWithLimit == 0)
    567            return;
    568
    569        // This captures when the number of rows has decreased, typically due to deletions.
    570
    571        if (numberOfRowsRequiredToShowCurrentPage > availableRowsWithLimit)
    572        {
    573            int maxPage = ((availableRowsWithLimit - 1) / rowsPerPage) + 1;
    574            effectiveCurrentPage = maxPage;
    575        }
    576
    577        int startIndex = (effectiveCurrentPage - 1) * rowsPerPage;
    578
    579        int endIndex = Math.min(startIndex + rowsPerPage - 1, availableRowsWithLimit - 1);
    580
    581        cachingSource.prepare(startIndex, endIndex, sortModel.getSortConstraints());
    582    }
    583
    584    Object beginRender(MarkupWriter writer)
    585    {
    586        // Skip rendering of component (template, body, etc.) when there's nothing to display.
    587        // The empty placeholder will already have rendered.
    588
    589        if (cachingSource.isEmpty())
    590            return !renderTableIfEmpty ? false : null;
    591
    592        if (inPlace && zone == null)
    593        {
    594            javaScriptSupport.require("t5/core/zone");
    595
    596            writer.element("div", "data-container-type", "zone");
    597
    598            didRenderZoneDiv = true;
    599
    600            // Through Tapestry 5.3, we had a specific id for the zone that had to be passed down to the
    601            // GridPager and etc.  That's no longer necessary, so zone will always be null or "^".  We don't
    602            // even need any special ids to be allocated!
    603            zone = "^";
    604        }
    605
    606        return null;
    607    }
    608
    609    void afterRender(MarkupWriter writer)
    610    {
    611        if (didRenderZoneDiv)
    612        {
    613            writer.end(); // div
    614            didRenderZoneDiv = false;
    615        }
    616    }
    617
    618    public BeanModel getDataModel()
    619    {
    620        if (dataModel == null)
    621        {
    622            dataModel = model;
    623
    624            BeanModelUtils.modify(dataModel, add, include, exclude, reorder);
    625        }
    626
    627        return dataModel;
    628    }
    629
    630    public int getNumberOfProperties()
    631    {
    632        return getDataModel().getPropertyNames().size();
    633    }
    634
    635    public GridDataSource getDataSource()
    636    {
    637        return cachingSource;
    638    }
    639
    640    public GridSortModel getSortModel()
    641    {
    642        return sortModel;
    643    }
    644
    645    public Object getPagerTop()
    646    {
    647        return pagerPosition.isMatchTop() ? pager : null;
    648    }
    649
    650    public Object getPagerBottom()
    651    {
    652        return pagerPosition.isMatchBottom() ? pager : null;
    653    }
    654
    655    public int getCurrentPage()
    656    {
    657        Integer currentPage = paginationModel.getCurrentPage();
    658
    659        return currentPage == null ? 1 : currentPage;
    660    }
    661
    662    public void setCurrentPage(int currentPage)
    663    {
    664        paginationModel.setCurrentPage(currentPage);
    665    }
    666
    667    private boolean getSortAscending()
    668    {
    669        Boolean sortAscending = paginationModel.getSortAscending();
    670
    671        return sortAscending != null && sortAscending.booleanValue();
    672    }
    673
    674    private void setSortAscending(boolean sortAscending)
    675    {
    676        paginationModel.setSortAscending(sortAscending);
    677    }
    678
    679    public int getRowsPerPage()
    680    {
    681        return rowsPerPage;
    682    }
    683
    684    public Object getRow()
    685    {
    686        return row;
    687    }
    688
    689    public void setRow(Object row)
    690    {
    691        this.row = row;
    692    }
    693
    694    /**
    695     * Resets the Grid to inital settings; this sets the current page to one, and
    696     * {@linkplain org.apache.tapestry5.grid.GridSortModel#clear() clears the sort model}.
    697     */
    698    public void reset()
    699    {
    700        sortModel.clear();
    701        setCurrentPage(1);
    702    }
    703
    704    /**
    705     * Event handler for inplaceupdate event triggered from nested components when an Ajax update occurs. The event
    706     * context will carry the zone, which is recorded here, to allow the Grid and its sub-components to properly
    707     * re-render themselves. Invokes
    708     * {@link org.apache.tapestry5.services.ComponentEventResultProcessor#processResultValue(Object)} passing this (the
    709     * Grid component) as the content provider for the update.
    710     */
    711    void onInPlaceUpdate() throws IOException
    712    {
    713        this.zone = "^";
    714
    715        componentEventResultProcessor.processResultValue(this);
    716    }
    717
    718    public String getClientId()
    719    {
    720        return table.getClientId();
    721    }
    722}