You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by ah...@apache.org on 2024/06/19 17:39:50 UTC

(causeway) 01/01: CAUSEWAY-3786: Add switch to activate 2.0.0 data table implementation (experimental)

This is an automated email from the ASF dual-hosted git repository.

ahuber pushed a commit to branch 3786-switch.v2.0.tables
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 3c9fab28098d77ce096c8366b3f77321f8c7b222
Author: Andi Huber <ah...@apache.org>
AuthorDate: Wed Jun 19 19:39:44 2024 +0200

    CAUSEWAY-3786: Add switch to activate 2.0.0 data table implementation
    (experimental)
---
 .../core/metamodel/object/MmSortUtils.java         |  13 +-
 .../causeway/core/metamodel/tabular/DataRow.java   |   6 +
 .../metamodel/tabular/DataTableInteractive.java    |  74 +++-
 .../tabular/internal/DataTableInternal.java        |   5 -
 .../metamodel/tabular/optimistic/DataColumnO.java  |  49 +++
 .../metamodel/tabular/optimistic/DataRowO.java     | 115 ++++++
 .../metamodel/tabular/optimistic/DataTableO.java   | 408 +++++++++++++++++++++
 .../metamodel/tabular/optimistic/_ToggleDebug.java |  44 +++
 .../models/EntityCollectionModelStandalone.java    |   6 +-
 .../interaction/act/ActionInteractionWkt.java      |  20 +-
 .../model/models/interaction/coll/DataRowWkt.java  |  54 ++-
 .../coll/{DataRowWkt.java => DataRowWktD.java}     |  13 +-
 .../coll/{DataRowWkt.java => DataRowWktO.java}     |  40 +-
 .../models/interaction/coll/DataTableModelWkt.java |  91 ++---
 ...aTableModelWkt.java => DataTableModelWktD.java} |  15 +-
 ...aTableModelWkt.java => DataTableModelWktO.java} |  77 +++-
 .../actionresponse/ActionResultResponseType.java   |  46 ++-
 .../ajaxtable/CausewayAjaxDataTable.java           | 117 +++---
 .../ajaxtable/CausewayAjaxHeadersToolbar.java      |  35 +-
 .../CollectionContentsSortableDataProvider.java    |  59 ++-
 .../ajaxtable/columns/ToggleboxColumn.java         |  17 +-
 .../wicket/ui/panels/FormExecutorDefault.java      |   2 +-
 22 files changed, 1054 insertions(+), 252 deletions(-)

diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/MmSortUtils.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/MmSortUtils.java
index 347c827cf0..42bbd3e411 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/MmSortUtils.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/MmSortUtils.java
@@ -38,7 +38,18 @@ public class MmSortUtils {
         DESCENDING;
     }
 
-    public Comparator<ManagedObject> orderingBy(final ObjectAssociation sortProperty, final SortDirection sortDirection) {
+    @Deprecated // use the enum variant instead
+    public Comparator<ManagedObject> orderingBy(
+            final ObjectAssociation sortProperty,
+            final boolean isAscending) {
+        return orderingBy(sortProperty, isAscending
+                ? SortDirection.ASCENDING
+                : SortDirection.DESCENDING);
+    }
+
+    public Comparator<ManagedObject> orderingBy(
+            final ObjectAssociation sortProperty,
+            final SortDirection sortDirection) {
         final Comparator<ManagedObject> comparator = (sortDirection != SortDirection.DESCENDING)
                 ? NATURAL_NULL_FIRST
                 : NATURAL_NULL_FIRST.reversed();
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/DataRow.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/DataRow.java
index 6829d6344e..0e14fef46c 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/DataRow.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/DataRow.java
@@ -19,10 +19,12 @@
 package org.apache.causeway.core.metamodel.tabular;
 
 import java.util.Optional;
+import java.util.UUID;
 
 import org.apache.causeway.applib.services.filter.CollectionFilterService.Tokens;
 import org.apache.causeway.commons.binding.Bindable;
 import org.apache.causeway.commons.collections.Can;
+import org.apache.causeway.commons.internal.exceptions._Exceptions;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
 
 import lombok.NonNull;
@@ -65,5 +67,9 @@ public interface DataRow {
     Can<ManagedObject> getCellElementsForColumn(@NonNull String columnId);
     Optional<Tokens> getFilterTokens();
 
+    @Deprecated // used by OPTIMISTIC data table
+    default UUID getUuid() {
+        throw _Exceptions.unsupportedOperation();
+    }
 
 }
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/DataTableInteractive.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/DataTableInteractive.java
index 9c364a9257..b04b1d7b50 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/DataTableInteractive.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/DataTableInteractive.java
@@ -22,43 +22,98 @@ import java.io.Serializable;
 import java.util.Comparator;
 import java.util.Optional;
 import java.util.Set;
+import java.util.UUID;
 
 import org.apache.causeway.applib.annotation.TableDecorator;
 import org.apache.causeway.applib.annotation.Where;
 import org.apache.causeway.commons.binding.Bindable;
 import org.apache.causeway.commons.binding.Observable;
 import org.apache.causeway.commons.collections.Can;
+import org.apache.causeway.commons.internal.exceptions._Exceptions;
 import org.apache.causeway.core.metamodel.interactions.managed.ActionInteraction;
 import org.apache.causeway.core.metamodel.interactions.managed.ManagedAction;
+import org.apache.causeway.core.metamodel.interactions.managed.ManagedAction.MementoForArgs;
 import org.apache.causeway.core.metamodel.interactions.managed.ManagedCollection;
 import org.apache.causeway.core.metamodel.interactions.managed.ManagedMember;
 import org.apache.causeway.core.metamodel.interactions.managed.MultiselectChoices;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
 import org.apache.causeway.core.metamodel.object.MmSortUtils;
+import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectMember;
 import org.apache.causeway.core.metamodel.tabular.internal.DataTableInternal;
+import org.apache.causeway.core.metamodel.tabular.optimistic.DataTableO;
 import org.apache.causeway.core.metamodel.tabular.simple.DataTable;
 
 import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.NonNull;
 import lombok.RequiredArgsConstructor;
 import lombok.val;
 
 public interface DataTableInteractive extends MultiselectChoices {
 
+    public enum TableImplementation {
+        DEFAULT,
+        OPTIMISTIC;
+
+        public boolean isOptimistic() { return this == OPTIMISTIC;}
+
+        private final static String PROPERTY_NAME_MODEL_SELECT = "causeway.metamodel.dataTableModelSelect";
+        private static boolean isOptimisticSelected() {
+            return "OPTIMISTIC".equalsIgnoreCase(System.getenv(PROPERTY_NAME_MODEL_SELECT))
+                    || "OPTIMISTIC".equalsIgnoreCase(System.getProperty(PROPERTY_NAME_MODEL_SELECT));
+        }
+
+        @Getter(lazy=true)
+        private final static TableImplementation selected = isOptimisticSelected() ? OPTIMISTIC : DEFAULT;
+    }
+
     // -- FACTORIES
 
     public static DataTableInteractive empty(final ManagedMember managedMember, final Where where) {
-        return DataTableInternal.empty(managedMember, where);
+        switch (TableImplementation.getSelected()) {
+        case OPTIMISTIC:
+            return DataTableO.empty(managedMember, where);
+        case DEFAULT:
+        default:
+            return DataTableInternal.empty(managedMember, where);
+        }
     }
 
     public static DataTableInteractive forCollection(final ManagedCollection managedCollection) {
-        return DataTableInternal.forCollection(managedCollection);
+        switch (TableImplementation.getSelected()) {
+        case OPTIMISTIC:
+            return DataTableO.forCollection(managedCollection);
+        case DEFAULT:
+        default:
+            return DataTableInternal.forCollection(managedCollection);
+        }
+    }
+
+    public static DataTableInteractive forAction(
+            final ManagedAction managedAction,
+            final ManagedObject actionResult) {
+        switch (TableImplementation.getSelected()) {
+        case OPTIMISTIC:
+            throw _Exceptions.unexpectedCodeReach();
+        case DEFAULT:
+        default:
+            return DataTableInternal.forAction(managedAction, actionResult);
+        }
     }
 
+    @Deprecated // args only required for TableImplementation.OPTIMISTIC
     public static DataTableInteractive forAction(
             final ManagedAction managedAction,
+            final @NonNull Can<ManagedObject> args,
             final ManagedObject actionResult) {
-        return DataTableInternal.forAction(managedAction, actionResult);
+        switch (TableImplementation.getSelected()) {
+        case OPTIMISTIC:
+            return DataTableO.forAction(managedAction, args, actionResult);
+        case DEFAULT:
+        default:
+            return DataTableInternal.forAction(managedAction, actionResult);
+        }
     }
 
     // --
@@ -91,6 +146,9 @@ public interface DataTableInteractive extends MultiselectChoices {
 
     ObjectMember getMetaModel();
     Optional<TableDecorator> getTableDecoratorIfAny();
+    default ObjectSpecification getElementType() {
+        return getMetaModel().getElementType();
+    }
 
     // -- ASSOCIATED ACTION
 
@@ -110,6 +168,11 @@ public interface DataTableInteractive extends MultiselectChoices {
      */
     Optional<DataRow> lookupDataRow(int rowIndex);
 
+    @Deprecated // used by OPTIMISTIC data table
+    default Optional<? extends DataRow> lookupDataRow(final @NonNull UUID uuid) {
+        throw _Exceptions.unsupportedOperation();
+    }
+
     // -- PAGING
 
     int getPageSize(int pageSizeDefault);
@@ -133,6 +196,11 @@ public interface DataTableInteractive extends MultiselectChoices {
 
     DataTableMemento createMemento();
 
+    @Deprecated // used by OPTIMISTIC data table
+    default DataTableMemento createMemento(final MementoForArgs mementoForArgs) {
+        throw _Exceptions.unsupportedOperation();
+    }
+
     // -- FILTER SUPPORT
 
     Bindable<String> getSearchArgument();
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/internal/DataTableInternal.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/internal/DataTableInternal.java
index f696e4a88a..43fba90271 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/internal/DataTableInternal.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/internal/DataTableInternal.java
@@ -47,7 +47,6 @@ import org.apache.causeway.core.metamodel.interactions.managed.ManagedMember;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
 import org.apache.causeway.core.metamodel.object.ManagedObjects;
 import org.apache.causeway.core.metamodel.object.PackedManagedObject;
-import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectMember;
 import org.apache.causeway.core.metamodel.tabular.DataColumn;
 import org.apache.causeway.core.metamodel.tabular.DataRow;
@@ -228,10 +227,6 @@ implements DataTableInteractive {
         return managedMember.getMetaModel();
     }
 
-    public ObjectSpecification getElementType() {
-        return getMetaModel().getElementType();
-    }
-
     @Override
     public Optional<DataRow> lookupDataRow(final int rowIndex) {
         return getDataRows().getValue().get(rowIndex)
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/optimistic/DataColumnO.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/optimistic/DataColumnO.java
new file mode 100644
index 0000000000..e6c5a3c447
--- /dev/null
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/optimistic/DataColumnO.java
@@ -0,0 +1,49 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.causeway.core.metamodel.tabular.optimistic;
+
+import java.util.Optional;
+
+import org.apache.causeway.commons.internal.binding._Observables;
+import org.apache.causeway.commons.internal.binding._Observables.LazyObservable;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation;
+import org.apache.causeway.core.metamodel.tabular.DataColumn;
+
+import lombok.Getter;
+import lombok.NonNull;
+
+public class DataColumnO implements DataColumn {
+
+    @Getter private final @NonNull String columnId;
+    @Getter private final @NonNull ObjectAssociation associationMetaModel;
+    @Getter private final @NonNull LazyObservable<String> columnFriendlyName;
+    @Getter private final @NonNull LazyObservable<Optional<String>> columnDescription;
+
+    public DataColumnO(final DataTableO parentTable, final ObjectAssociation associationMetaModel) {
+        this.associationMetaModel = associationMetaModel;
+        this.columnId = associationMetaModel.getId();
+
+        columnFriendlyName = _Observables.lazy(()->
+            associationMetaModel.getCanonicalFriendlyName());
+
+        columnDescription = _Observables.lazy(()->
+            associationMetaModel.getCanonicalDescription());
+    }
+
+}
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/optimistic/DataRowO.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/optimistic/DataRowO.java
new file mode 100644
index 0000000000..185d2d652c
--- /dev/null
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/optimistic/DataRowO.java
@@ -0,0 +1,115 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.causeway.core.metamodel.tabular.optimistic;
+
+import java.util.Optional;
+import java.util.UUID;
+
+import org.apache.causeway.applib.services.filter.CollectionFilterService.Tokens;
+import org.apache.causeway.commons.collections.Can;
+import org.apache.causeway.commons.internal.binding._Bindables;
+import org.apache.causeway.commons.internal.binding._Bindables.BooleanBindable;
+import org.apache.causeway.core.metamodel.object.ManagedObject;
+import org.apache.causeway.core.metamodel.object.ManagedObjects;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation;
+import org.apache.causeway.core.metamodel.tabular.DataColumn;
+import org.apache.causeway.core.metamodel.tabular.DataRow;
+
+import lombok.Getter;
+import lombok.NonNull;
+
+public class DataRowO implements DataRow {
+
+    @Getter private final UUID uuid = UUID.randomUUID(); // in support of client side sorting
+    private final ManagedObject rowElement;
+    @Getter private final BooleanBindable selectToggle;
+    @Getter private final DataTableO parentTable;
+
+    public DataRowO(
+            final @NonNull DataTableO parentTable,
+            final @NonNull ManagedObject rowElement) {
+        this.parentTable = parentTable;
+        this.rowElement = rowElement;
+
+        selectToggle = _Bindables.forBoolean(false);
+        selectToggle.addListener((e,o,n)->{
+
+            //_ToggleDebug.onSelectRowToggle(rowElement, o, n, parentTable.isToggleAllEvent.get());
+
+            if(parentTable.isToggleAllEvent.get()) {
+                return;
+            }
+            parentTable.getDataRowsSelected().invalidate();
+            // in any case, if we have a toggle state change, clear the toggle all bindable
+            parentTable.clearToggleAll();
+        });
+
+    }
+
+    @Override
+    public ManagedObject getRowElement() {
+        return rowElement;
+    }
+
+    @Override
+    public Optional<DataColumn> lookupColumnById(final @NonNull String columnId) {
+        return parentTable.getDataColumns().getValue().stream()
+                .filter(dataColumn->dataColumn.getColumnId().equals(columnId))
+                .findFirst();
+    }
+
+    /**
+     * Can be none, one or many per table cell.
+     */
+    public Can<ManagedObject> getCellElementsForColumn(final @NonNull DataColumnO column) {
+        final ObjectAssociation assoc = column.getAssociationMetaModel();
+        return assoc.getSpecialization().fold(
+                property->Can.of(property.get(getRowElement())),
+                collection->ManagedObjects.unpack(collection.get(getRowElement())));
+    }
+
+    /**
+     * Can be none, one or many per table cell. (returns empty Can if column not found)
+     */
+    @Override
+    public Can<ManagedObject> getCellElementsForColumn(final @NonNull String columnId) {
+        return lookupColumnById(columnId)
+                .map(this::getCellElementsForColumn)
+                .orElseGet(Can::empty);
+    }
+
+    @Override
+    public int getRowIndex() {
+        // TODO Auto-generated method stub
+        return 0;
+    }
+
+    @Override
+    public Can<ManagedObject> getCellElementsForColumn(@NonNull final DataColumn column) {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    @Override
+    public Optional<Tokens> getFilterTokens() {
+        // TODO Auto-generated method stub
+        return Optional.empty();
+    }
+
+}
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/optimistic/DataTableO.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/optimistic/DataTableO.java
new file mode 100644
index 0000000000..cff89f3b59
--- /dev/null
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/optimistic/DataTableO.java
@@ -0,0 +1,408 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.causeway.core.metamodel.tabular.optimistic;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Supplier;
+
+import org.springframework.lang.Nullable;
+
+import org.apache.causeway.applib.Identifier;
+import org.apache.causeway.applib.annotation.TableDecorator;
+import org.apache.causeway.applib.annotation.Where;
+import org.apache.causeway.commons.binding.Bindable;
+import org.apache.causeway.commons.binding.Observable;
+import org.apache.causeway.commons.collections.Can;
+import org.apache.causeway.commons.internal.binding._BindableAbstract;
+import org.apache.causeway.commons.internal.binding._Bindables;
+import org.apache.causeway.commons.internal.binding._Observables;
+import org.apache.causeway.commons.internal.binding._Observables.LazyObservable;
+import org.apache.causeway.commons.internal.collections._Maps;
+import org.apache.causeway.commons.internal.exceptions._Exceptions;
+import org.apache.causeway.core.metamodel.consent.InteractionInitiatedBy;
+import org.apache.causeway.core.metamodel.consent.InteractionResult;
+import org.apache.causeway.core.metamodel.context.MetaModelContext;
+import org.apache.causeway.core.metamodel.interactions.InteractionHead;
+import org.apache.causeway.core.metamodel.interactions.InteractionUtils;
+import org.apache.causeway.core.metamodel.interactions.ObjectVisibilityContext;
+import org.apache.causeway.core.metamodel.interactions.VisibilityContext;
+import org.apache.causeway.core.metamodel.interactions.managed.ActionInteraction;
+import org.apache.causeway.core.metamodel.interactions.managed.CollectionInteraction;
+import org.apache.causeway.core.metamodel.interactions.managed.ManagedAction;
+import org.apache.causeway.core.metamodel.interactions.managed.ManagedAction.MementoForArgs;
+import org.apache.causeway.core.metamodel.interactions.managed.ManagedCollection;
+import org.apache.causeway.core.metamodel.interactions.managed.ManagedMember;
+import org.apache.causeway.core.metamodel.object.ManagedObject;
+import org.apache.causeway.core.metamodel.object.PackedManagedObject;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectMember;
+import org.apache.causeway.core.metamodel.tabular.DataColumn;
+import org.apache.causeway.core.metamodel.tabular.DataRow;
+import org.apache.causeway.core.metamodel.tabular.DataTableInteractive;
+import org.apache.causeway.core.metamodel.tabular.DataTableMemento;
+import org.apache.causeway.core.metamodel.tabular.simple.DataTable;
+
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import lombok.val;
+
+public class DataTableO
+implements DataTableInteractive {
+
+    // -- FACTORIES
+
+    public static DataTableO empty(final ManagedMember managedMember, final Where where) {
+        return new DataTableO(managedMember, where, Can::empty);
+    }
+
+    public static DataTableO forCollection(
+            final ManagedCollection managedCollection) {
+        return new DataTableO(managedCollection, managedCollection.getWhere(), ()->
+            managedCollection
+            .streamElements()
+            .collect(Can.toCan()));
+    }
+
+    public static DataTableO forAction(
+            final ManagedAction managedAction,
+            final Can<ManagedObject> args,
+            final ManagedObject actionResult) {
+
+        if(actionResult==null) {
+            new DataTableO(managedAction, managedAction.getWhere(), Can::empty);
+        }
+        if(!(actionResult instanceof PackedManagedObject)) {
+            throw _Exceptions.unexpectedCodeReach();
+        }
+
+        val elements = ((PackedManagedObject)actionResult).unpack();
+        elements.forEach(ManagedObject::getBookmark);
+
+        return new DataTableO(managedAction, managedAction.getWhere(),
+                ()->elements);
+    }
+
+    // -- CONSTRUCTION
+
+    // as this is a layer of abstraction, don't expose via getter
+    final @NonNull ManagedMember managedMember;
+    final @NonNull Where where;
+
+    @Getter private final @NonNull LazyObservable<Can<ManagedObject>> dataElements;
+    @Getter private final @NonNull _BindableAbstract<String> searchArgument; // filter the data rows
+    @Getter private final @NonNull LazyObservable<Can<DataRow>> dataRowsFiltered;
+    @Getter private final @NonNull LazyObservable<Can<DataRow>> dataRowsSelected;
+    @Getter private final _BindableAbstract<Boolean> selectAllToggle;
+
+    @Getter private final @NonNull LazyObservable<Can<DataColumn>> dataColumns;
+    @Getter private final @NonNull LazyObservable<String> title;
+
+    private DataTableO(
+            // we need access to the owner in support of imperative title and referenced column detection
+            final ManagedMember managedMember,
+            final Where where,
+            final Supplier<Can<ManagedObject>> elementSupplier) {
+
+        this.managedMember = managedMember;
+        this.where = where;
+
+        //dataElements = _Observables.lazy(elementSupplier);
+        dataElements = _Observables.lazy(()->elementSupplier.get().map(
+            MetaModelContext.instanceElseFail()::injectServicesInto));
+
+        searchArgument = _Bindables.forValue(null);
+
+        dataRowsFiltered = _Observables.lazy(()->
+            dataElements.getValue().stream()
+                //XXX future extension: filter by searchArgument
+                .filter(this::ignoreHidden)
+                .sorted(managedMember.getMetaModel().getElementComparator()
+                        .orElseGet(()->(a, b)->0)) // else don't sort (no-op comparator for streams)
+                .map(domainObject->new DataRowO(this, domainObject))
+                .collect(Can.toCan()));
+
+        dataRowsSelected = _Observables.lazy(()->
+            dataRowsFiltered.getValue().stream()
+            .filter(dataRow->dataRow.getSelectToggle().getValue().booleanValue())
+            .collect(Can.toCan()));
+
+        selectAllToggle = _Bindables.forValue(Boolean.FALSE);
+        selectAllToggle.addListener((e,o,isAllOn)->{
+            //_Debug.onClearToggleAll(o, isAllOn, isClearToggleAllEvent.get());
+            if(isClearToggleAllEvent.get()) {
+                return;
+            }
+            dataRowsSelected.invalidate();
+            try {
+                isToggleAllEvent.set(true);
+                dataRowsFiltered.getValue().forEach(dataRow->dataRow.getSelectToggle().setValue(isAllOn));
+            } finally {
+                isToggleAllEvent.set(false);
+            }
+        });
+
+        searchArgument.addListener((e,o,n)->{
+            dataRowsFiltered.invalidate();
+            dataRowsSelected.invalidate();
+        });
+
+        dataColumns = _Observables.lazy(()->
+            managedMember.getElementType()
+            .streamAssociationsForColumnRendering(managedMember.getIdentifier(), managedMember.getOwner())
+            .map(assoc->new DataColumnO(this, assoc))
+            .collect(Can.toCan()));
+
+        //XXX future extension: the title could dynamically reflect the number of elements selected
+        //eg... 5 Orders selected
+        title = _Observables.lazy(()->
+            managedMember
+            .getFriendlyName());
+    }
+
+    @Override
+    public int getPageSize(final int pageSizeDefault) {
+        return getMetaModel().getPageSize().orElse(pageSizeDefault);
+    }
+
+    @Override
+    public Optional<TableDecorator> getTableDecoratorIfAny() {
+        return getMetaModel().getTableDecorator();
+    }
+
+    /**
+     * Count filtered data rows.
+     */
+    public int getElementCount() {
+        return dataRowsFiltered.getValue().size();
+    }
+
+    @Override
+    public ObjectMember getMetaModel() {
+        return managedMember.getMetaModel();
+    }
+
+    private final Map<UUID, Optional<DataRowO>> dataRowByUuidLookupCache = _Maps.newConcurrentHashMap();
+    @Override
+    public Optional<DataRowO> lookupDataRow(final @NonNull UUID uuid) {
+        // lookup can be safely cached
+        return dataRowByUuidLookupCache.computeIfAbsent(uuid, __->getDataRowsFiltered().getValue().stream()
+                .map(DataRowO.class::cast)
+                .filter(dr->dr.getUuid().equals(uuid))
+                .findFirst());
+    }
+
+    // -- TOGGLE ALL
+
+    final AtomicBoolean isToggleAllEvent = new AtomicBoolean();
+    private final AtomicBoolean isClearToggleAllEvent = new AtomicBoolean();
+    public void clearToggleAll() {
+        try {
+            isClearToggleAllEvent.set(true);
+            selectAllToggle.setValue(Boolean.FALSE);
+        } finally {
+            isClearToggleAllEvent.set(false);
+        }
+    }
+
+    // -- DATA ROW VISIBILITY
+
+    private boolean ignoreHidden(final ManagedObject adapter) {
+        final InteractionResult visibleResult =
+                InteractionUtils.isVisibleResult(
+                        adapter.getSpecification(),
+                        createVisibleInteractionContext(adapter));
+        return visibleResult.isNotVetoing();
+    }
+
+    private VisibilityContext createVisibleInteractionContext(final ManagedObject objectAdapter) {
+        return new ObjectVisibilityContext(
+                InteractionHead.regular(objectAdapter),
+                objectAdapter.getSpecification().getFeatureIdentifier(),
+                InteractionInitiatedBy.USER,
+                Where.ALL_TABLES,
+                InteractionUtils.renderPolicy(objectAdapter));
+    }
+
+    // -- ASSOCIATED ACTION WITH MULTI SELECT
+
+    @Override
+    public Can<ManagedObject> getSelected() {
+      return getDataRowsSelected()
+                .getValue()
+                .map(DataRow::getRowElement);
+    }
+
+    @Override
+    public ActionInteraction startAssociatedActionInteraction(final String actionId, final Where where) {
+        val featureId = managedMember.getIdentifier();
+        if(!featureId.getType().isPropertyOrCollection()) {
+            return ActionInteraction.empty(String.format("[no such collection %s; instead got %s;"
+                    + "(while searching for an associated action %s)]",
+                    featureId,
+                    featureId.getType(),
+                    actionId));
+        }
+        return ActionInteraction.startWithMultiselect(managedMember.getOwner(), actionId, where, this);
+    }
+
+    // -- EXPORT
+
+    @Override
+    public DataTable export() {
+        return new DataTable(getElementType(),
+                getTitle().getValue(),
+                getDataColumns().getValue()
+                    .map(DataColumn::getAssociationMetaModel),
+                getDataRowsFiltered().getValue()
+                    .stream()
+                    .map(dr->dr.getRowElement())
+                    .collect(Can.toCan()));
+    }
+
+    // -- MEMENTO
+
+    public Memento getMemento(final @Nullable ManagedAction.MementoForArgs argsMemento) {
+        return Memento.create(this, argsMemento);
+    }
+
+    /**
+     * Recreation from given 'bookmarkable' {@link ManagedObject} (owner),
+     * without triggering domain events.
+     * Either originates from a <i>Collection</i> or an <i>Action</i>'s
+     * non-scalar result.
+     * <p>
+     * In the <i>Action</i> case, requires the <i>Action</i>'s arguments
+     * for reconstruction.
+     * <p>
+     * Responsibility for recreation of the owner is with the caller
+     * to allow for simpler object graph reconstruction (shared owner).
+     * <p>
+     * However, we keep track of the argument list here.
+     */
+    @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
+    public static class Memento implements DataTableMemento {
+        private static final long serialVersionUID = 1L;
+
+        static Memento create(
+                final @Nullable DataTableO table,
+                final @Nullable MementoForArgs argsMemento) {
+            val managedMember = table.managedMember;
+
+            return new Memento(
+                    managedMember.getIdentifier(),
+                    table.where,
+                    argsMemento);
+        }
+
+        private final Identifier featureId;
+        private final Where where;
+        private final MementoForArgs argsMemento;
+
+        @Override
+        public DataTableO getDataTableModel(final ManagedObject owner) {
+
+            if(owner.getPojo()==null) {
+                // owner (if entity) might have been deleted
+                throw _Exceptions.illegalArgument("cannot recreate from memento for deleted object");
+            }
+
+            val memberId = featureId.getMemberLogicalName();
+
+            if(featureId.getType().isPropertyOrCollection()) {
+                // bypass domain events
+                val collInteraction = CollectionInteraction.start(owner, memberId, where);
+                val managedColl = collInteraction.getManagedCollection().orElseThrow();
+                // invocation bypassing domain events (pass-through)
+                return new DataTableO(managedColl, where, ()->
+                    managedColl.streamElements(InteractionInitiatedBy.PASS_THROUGH).collect(Can.toCan()));
+            }
+            val actionInteraction = ActionInteraction.start(owner, memberId, where);
+            val managedAction = actionInteraction.getManagedActionElseFail();
+            val args = argsMemento.getArgumentList(managedAction.getMetaModel());
+            // invocation bypassing domain events (pass-through)
+            val actionResult = managedAction.invoke(args, InteractionInitiatedBy.PASS_THROUGH)
+                    .getSuccessElseFail();
+            return forAction(managedAction, args, actionResult);
+        }
+
+        @Override
+        public void setupBindings(final DataTableInteractive tableInteractive) {
+            // TODO Auto-generated method stub
+
+        }
+    }
+
+    @Override
+    public Observable<Can<DataRow>> getDataRowsFilteredAndSorted() {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    @Override
+    public int getFilteredElementCount() {
+        // TODO Auto-generated method stub
+        return 0;
+    }
+
+    @Override
+    public Optional<DataRow> lookupDataRow(final int rowIndex) {
+        // TODO Auto-generated method stub
+        return Optional.empty();
+    }
+
+    @Override
+    public Bindable<ColumnSort> getColumnSort() {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    @Override
+    public void doProgrammaticToggle(final Runnable runnable) {
+        // TODO Auto-generated method stub
+
+    }
+
+    @Override
+    public Set<Integer> getSelectedRowIndexes() {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    @Override
+    public DataTableMemento createMemento() {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    @Override
+    public boolean isSearchSupported() {
+        return false;
+    }
+
+    @Override
+    public String getSearchPromptPlaceholderText() {
+        return "";
+    }
+
+}
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/optimistic/_ToggleDebug.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/optimistic/_ToggleDebug.java
new file mode 100644
index 0000000000..8b44d806c0
--- /dev/null
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/optimistic/_ToggleDebug.java
@@ -0,0 +1,44 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.causeway.core.metamodel.tabular.optimistic;
+
+import org.apache.causeway.core.metamodel.object.ManagedObject;
+
+class _ToggleDebug {
+
+    public static void onSelectRowToggle(
+            final ManagedObject rowElement,
+            final Boolean old,
+            final Boolean _new,
+            final boolean isToggleAllEvent) {
+
+        System.err.printf("onSelectRowToggle: %s %b->%b (toggle-all: %b)%n",
+                ""+rowElement.getPojo(), old, _new, isToggleAllEvent);
+    }
+
+    public static void onClearToggleAll(
+            final Boolean old,
+            final Boolean isAllOn,
+            final boolean isClearToggleAllEvent) {
+
+        System.err.printf("clear-all: %b->%b (clear-all: %b)%n",
+                old, isAllOn, isClearToggleAllEvent);
+    }
+
+}
diff --git a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/EntityCollectionModelStandalone.java b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/EntityCollectionModelStandalone.java
index ad8693e087..461a3813a0 100644
--- a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/EntityCollectionModelStandalone.java
+++ b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/EntityCollectionModelStandalone.java
@@ -18,6 +18,8 @@
  */
 package org.apache.causeway.viewer.wicket.model.models;
 
+import org.apache.causeway.commons.collections.Can;
+import org.apache.causeway.core.metamodel.object.ManagedObject;
 import org.apache.causeway.core.metamodel.object.PackedManagedObject;
 import org.apache.causeway.viewer.wicket.model.models.interaction.BookmarkedObjectWkt;
 import org.apache.causeway.viewer.wicket.model.models.interaction.coll.DataTableModelWkt;
@@ -34,7 +36,8 @@ extends EntityCollectionModelAbstract {
 
     public static EntityCollectionModelStandalone forActionModel(
             final @NonNull PackedManagedObject collectionAsAdapter,
-            final @NonNull ActionModel actionModel) {
+            final @NonNull ActionModel actionModel,
+            final @NonNull Can<ManagedObject> args) {
 
         val action = actionModel.getAction();
 
@@ -43,6 +46,7 @@ extends EntityCollectionModelAbstract {
                         BookmarkedObjectWkt
                             .ofAdapter(actionModel.getParentObject()),
                         action,
+                        args,
                         collectionAsAdapter));
     }
 
diff --git a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/act/ActionInteractionWkt.java b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/act/ActionInteractionWkt.java
index 380fbeefb0..dc5c29409b 100644
--- a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/act/ActionInteractionWkt.java
+++ b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/act/ActionInteractionWkt.java
@@ -39,6 +39,7 @@ import org.apache.causeway.core.metamodel.interactions.managed.PendingParamsSnap
 import org.apache.causeway.core.metamodel.object.ManagedObjects;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
 import org.apache.causeway.core.metamodel.spec.feature.memento.ActionMemento;
+import org.apache.causeway.core.metamodel.tabular.DataTableInteractive.TableImplementation;
 import org.apache.causeway.viewer.wicket.model.models.EntityCollectionModel;
 import org.apache.causeway.viewer.wicket.model.models.InlinePromptContext;
 import org.apache.causeway.viewer.wicket.model.models.ScalarParameterModel;
@@ -256,21 +257,30 @@ extends HasBookmarkedOwnerAbstract<ActionInteraction> {
      */
     private transient ParameterNegotiationModel parameterNegotiationModel;
     private PendingParamsSnapshot pendingParamsSnapshot;
+
     /**
      * Start and transiently memoize a new {@link ParameterNegotiationModel}.
      */
     private ParameterNegotiationModel startParameterNegotiationModel() {
-        var tableModel = actionInteraction().getManagedAction().map(ManagedAction::getMultiselectChoices);
-        if(tableModel!=null) {
-            // possibly uses outdated tableModel, if any select toggle, filtering or sorting has happened
-            // hence detach(), so we invalidate the current actionInteraction() and force recreation
-            detach();
+        switch (TableImplementation.getSelected()) {
+        case DEFAULT: {
+            var tableModel = actionInteraction().getManagedAction().map(ManagedAction::getMultiselectChoices);
+            if(tableModel!=null) {
+                // possibly uses outdated tableModel, if any select toggle, filtering or sorting has happened
+                // hence detach(), so we invalidate the current actionInteraction() and force recreation
+                detach();
+            }
+            break;
+        }
+        default:
+            break;
         }
         this.parameterNegotiationModel = actionInteraction().startParameterNegotiation()
                 .orElseThrow(()->_Exceptions.noSuchElement(memberId));
         this.pendingParamsSnapshot = parameterNegotiationModel.createSnapshotModel();
         return parameterNegotiationModel;
     }
+
     /**
      * [CAUSEWAY-3649] safe guard against access to the model while it is not attached
      */
diff --git a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/coll/DataRowWkt.java b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/coll/DataRowWkt.java
index 3c7c7c053c..738190ec00 100644
--- a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/coll/DataRowWkt.java
+++ b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/coll/DataRowWkt.java
@@ -19,52 +19,46 @@
 package org.apache.causeway.viewer.wicket.model.models.interaction.coll;
 
 import java.util.Optional;
+import java.util.UUID;
 
-import org.apache.wicket.model.ChainingModel;
 import org.apache.wicket.model.IModel;
 
+import org.apache.causeway.commons.internal.exceptions._Exceptions;
 import org.apache.causeway.core.metamodel.tabular.DataRow;
 import org.apache.causeway.core.metamodel.tabular.DataTableInteractive;
+import org.apache.causeway.core.metamodel.tabular.DataTableInteractive.TableImplementation;
 
-import lombok.Getter;
+public interface DataRowWkt extends IModel<DataRow>{
 
-public class DataRowWkt
-extends ChainingModel<DataRow> {
-
-    private static final long serialVersionUID = 1L;
-
-    public static DataRowWkt chain(
+    static DataRowWkt chain(
             final IModel<DataTableInteractive> dataTableModelHolder,
             final DataRow dataRow) {
-        return new DataRowWkt(dataTableModelHolder, dataRow);
-    }
 
-    @Getter private final int rowIndex;
-
-    private DataRowWkt(
-            final IModel<DataTableInteractive> dataTableModelHolder,
-            final DataRow dataRow) {
-        super(dataTableModelHolder);
-        this.rowIndex = dataRow.getRowIndex();
+        switch (TableImplementation.getSelected()) {
+        case OPTIMISTIC:
+            return DataRowWktO.chain(dataTableModelHolder, dataRow);
+        case DEFAULT:
+        default:
+            return DataRowWktD.chain(dataTableModelHolder, dataRow);
+        }
     }
 
-    @Override
-    public final DataRow getObject() {
-        return dataRow().orElse(null);
-    }
+    int getRowIndex();
 
-    public Optional<DataRow> dataRow() {
-        return dataTableModel().lookupDataRow(rowIndex);
-    }
+    @Override
+    DataRow getObject();
 
-    public boolean hasMemoizedDataRow() {
-        return ((DataTableModelWkt) super.getTarget()).isAttached();
-    }
+    Optional<DataRow> dataRow();
 
-    // -- HELPER
+    boolean hasMemoizedDataRow();
 
-    private DataTableInteractive dataTableModel() {
-        return ((DataTableModelWkt) super.getTarget()).getObject();
+    @Deprecated // used by OPTIMISTIC data table
+    default UUID getUuid() {
+        throw _Exceptions.unsupportedOperation();
+    }
+    @Deprecated // used by OPTIMISTIC data table
+    default DataRowToggleWkt getDataRowToggle() {
+        throw _Exceptions.unsupportedOperation();
     }
 
 }
diff --git a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/coll/DataRowWkt.java b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/coll/DataRowWktD.java
similarity index 89%
copy from viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/coll/DataRowWkt.java
copy to viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/coll/DataRowWktD.java
index 3c7c7c053c..4ebd31cd41 100644
--- a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/coll/DataRowWkt.java
+++ b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/coll/DataRowWktD.java
@@ -28,20 +28,21 @@ import org.apache.causeway.core.metamodel.tabular.DataTableInteractive;
 
 import lombok.Getter;
 
-public class DataRowWkt
-extends ChainingModel<DataRow> {
+public class DataRowWktD
+extends ChainingModel<DataRow>
+implements DataRowWkt {
 
     private static final long serialVersionUID = 1L;
 
-    public static DataRowWkt chain(
+    public static DataRowWktD chain(
             final IModel<DataTableInteractive> dataTableModelHolder,
             final DataRow dataRow) {
-        return new DataRowWkt(dataTableModelHolder, dataRow);
+        return new DataRowWktD(dataTableModelHolder, dataRow);
     }
 
     @Getter private final int rowIndex;
 
-    private DataRowWkt(
+    private DataRowWktD(
             final IModel<DataTableInteractive> dataTableModelHolder,
             final DataRow dataRow) {
         super(dataTableModelHolder);
@@ -53,10 +54,12 @@ extends ChainingModel<DataRow> {
         return dataRow().orElse(null);
     }
 
+    @Override
     public Optional<DataRow> dataRow() {
         return dataTableModel().lookupDataRow(rowIndex);
     }
 
+    @Override
     public boolean hasMemoizedDataRow() {
         return ((DataTableModelWkt) super.getTarget()).isAttached();
     }
diff --git a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/coll/DataRowWkt.java b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/coll/DataRowWktO.java
similarity index 60%
copy from viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/coll/DataRowWkt.java
copy to viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/coll/DataRowWktO.java
index 3c7c7c053c..35497b6376 100644
--- a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/coll/DataRowWkt.java
+++ b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/coll/DataRowWktO.java
@@ -19,51 +19,73 @@
 package org.apache.causeway.viewer.wicket.model.models.interaction.coll;
 
 import java.util.Optional;
+import java.util.UUID;
 
 import org.apache.wicket.model.ChainingModel;
 import org.apache.wicket.model.IModel;
 
 import org.apache.causeway.core.metamodel.tabular.DataRow;
 import org.apache.causeway.core.metamodel.tabular.DataTableInteractive;
+import org.apache.causeway.viewer.wicket.model.util.PageUtils;
 
 import lombok.Getter;
+import lombok.NonNull;
 
-public class DataRowWkt
-extends ChainingModel<DataRow> {
+public class DataRowWktO
+extends ChainingModel<DataRow>
+implements DataRowWkt {
 
     private static final long serialVersionUID = 1L;
 
-    public static DataRowWkt chain(
+    public static DataRowWktO chain(
             final IModel<DataTableInteractive> dataTableModelHolder,
             final DataRow dataRow) {
-        return new DataRowWkt(dataTableModelHolder, dataRow);
+        return new DataRowWktO(dataTableModelHolder, dataRow);
     }
 
     @Getter private final int rowIndex;
+    @Getter private final @NonNull UUID uuid; // in support of table sorting
+    @Getter private final @NonNull DataRowToggleWkt dataRowToggle;
 
-    private DataRowWkt(
+    private transient DataRow dataRow;
+
+    private DataRowWktO(
             final IModel<DataTableInteractive> dataTableModelHolder,
             final DataRow dataRow) {
         super(dataTableModelHolder);
+        this.dataRow = dataRow;
         this.rowIndex = dataRow.getRowIndex();
+        this.uuid = dataRow.getUuid();
+        this.dataRowToggle = new DataRowToggleWkt(this);
     }
 
     @Override
     public final DataRow getObject() {
-        return dataRow().orElse(null);
+        if(dataRow==null) {
+            dataRow = getDataTableModel().lookupDataRow(uuid)
+                    .orElse(null);
+            if(dataRow==null) {
+                // [CAUSEWAY-3005] UI out of sync with model: reload page
+                PageUtils.pageReload();
+            }
+        }
+        return dataRow;
     }
 
+    @Override
     public Optional<DataRow> dataRow() {
-        return dataTableModel().lookupDataRow(rowIndex);
+        return Optional.ofNullable(getObject());
     }
 
+    @Override
     public boolean hasMemoizedDataRow() {
-        return ((DataTableModelWkt) super.getTarget()).isAttached();
+        return dataRow!=null
+                || getDataTableModel().lookupDataRow(uuid).isPresent();
     }
 
     // -- HELPER
 
-    private DataTableInteractive dataTableModel() {
+    private DataTableInteractive getDataTableModel() {
         return ((DataTableModelWkt) super.getTarget()).getObject();
     }
 
diff --git a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/coll/DataTableModelWkt.java b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/coll/DataTableModelWkt.java
index 55bf6c9cb9..83360e113a 100644
--- a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/coll/DataTableModelWkt.java
+++ b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/coll/DataTableModelWkt.java
@@ -18,23 +18,22 @@
  */
 package org.apache.causeway.viewer.wicket.model.models.interaction.coll;
 
+import org.apache.wicket.model.IModel;
+
 import org.apache.causeway.applib.Identifier;
-import org.apache.causeway.applib.annotation.Where;
-import org.apache.causeway.core.metamodel.interactions.managed.ManagedAction;
-import org.apache.causeway.core.metamodel.interactions.managed.ManagedCollection;
+import org.apache.causeway.commons.collections.Can;
+import org.apache.causeway.commons.internal.exceptions._Exceptions;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
 import org.apache.causeway.core.metamodel.spec.feature.OneToManyAssociation;
 import org.apache.causeway.core.metamodel.tabular.DataTableInteractive;
-import org.apache.causeway.core.metamodel.tabular.DataTableMemento;
+import org.apache.causeway.core.metamodel.tabular.DataTableInteractive.TableImplementation;
 import org.apache.causeway.viewer.commons.model.object.HasUiParentObject;
 import org.apache.causeway.viewer.commons.model.object.UiObject;
 import org.apache.causeway.viewer.wicket.model.models.interaction.BookmarkedObjectWkt;
 import org.apache.causeway.viewer.wicket.model.models.interaction.HasBookmarkedOwnerAbstract;
 
-import lombok.Getter;
 import lombok.NonNull;
-import lombok.val;
 
 /**
  * Bound to a BookmarkedObjectWkt, with the {@link DataTableInteractive}
@@ -46,9 +45,9 @@ import lombok.val;
  *
  * @see HasBookmarkedOwnerAbstract
  */
-public class DataTableModelWkt
-extends HasBookmarkedOwnerAbstract<DataTableInteractive>
-implements
+public interface DataTableModelWkt
+extends
+    IModel<DataTableInteractive>,
     HasUiParentObject<UiObject> {
 
     // -- FACTORIES
@@ -57,57 +56,43 @@ implements
             final @NonNull BookmarkedObjectWkt bookmarkedObjectModel,
             final @NonNull ObjectAction actMetaModel,
             final @NonNull ManagedObject actionResult) {
+        switch (TableImplementation.getSelected()) {
+        case OPTIMISTIC:
+            throw _Exceptions.unexpectedCodeReach();
+        case DEFAULT:
+        default:
+            return DataTableModelWktD.forActionModel(bookmarkedObjectModel, actMetaModel, actionResult);
+        }
+    }
 
-        val managedAction = ManagedAction
-                .of(bookmarkedObjectModel.getObject(), actMetaModel, Where.NOT_SPECIFIED);
-        val tableInteractive = DataTableInteractive.forAction(
-                managedAction,
-                actionResult);
-        return new DataTableModelWkt(
-                bookmarkedObjectModel, actMetaModel.getFeatureIdentifier(), tableInteractive);
+    public static DataTableModelWkt forActionModel(
+            final @NonNull BookmarkedObjectWkt bookmarkedObjectModel,
+            final @NonNull ObjectAction actMetaModel,
+            final @NonNull Can<ManagedObject> args,
+            final @NonNull ManagedObject actionResult) {
+        switch (TableImplementation.getSelected()) {
+        case OPTIMISTIC:
+            return DataTableModelWktO.forActionModel(bookmarkedObjectModel, actMetaModel, args, actionResult);
+        case DEFAULT:
+        default:
+            return DataTableModelWktD.forActionModel(bookmarkedObjectModel, actMetaModel, actionResult);
+        }
     }
 
     public static @NonNull DataTableModelWkt forCollection(
             final @NonNull BookmarkedObjectWkt bookmarkedObjectModel,
             final @NonNull OneToManyAssociation collMetaModel) {
-
-        val tableInteractive = DataTableInteractive.forCollection(
-                ManagedCollection
-                .of(bookmarkedObjectModel.getObject(), collMetaModel, Where.NOT_SPECIFIED));
-        return new DataTableModelWkt(
-                bookmarkedObjectModel, collMetaModel.getFeatureIdentifier(), tableInteractive);
-    }
-
-    // -- CONSTRUCTION
-
-    private static final long serialVersionUID = 1L;
-
-    @Getter private final Identifier featureIdentifier;
-    private final DataTableMemento tableMemento;
-
-    private DataTableModelWkt(
-            final BookmarkedObjectWkt bookmarkedObject,
-            final Identifier featureIdentifier,
-            final DataTableInteractive tableInteractive) {
-        super(bookmarkedObject);
-        this.featureIdentifier = featureIdentifier;
-        this.tableMemento = tableInteractive.createMemento();
-        setObject(tableInteractive); // memoize
-        tableMemento.setupBindings(tableInteractive);
+        switch (TableImplementation.getSelected()) {
+        case OPTIMISTIC:
+            return DataTableModelWktO.forCollection(bookmarkedObjectModel, collMetaModel);
+        case DEFAULT:
+        default:
+            return DataTableModelWktD.forCollection(bookmarkedObjectModel, collMetaModel);
+        }
     }
 
-    // --
-
-    @Override
-    public UiObject getParentUiModel() {
-        return ()->super.getBookmarkedOwner();
-    }
-
-    @Override
-    protected DataTableInteractive load() {
-        val tableInteractive = tableMemento.getDataTableModel(getBookmarkedOwner());
-        tableMemento.setupBindings(tableInteractive);
-        return tableInteractive;
-    }
+    Identifier getFeatureIdentifier();
+    boolean isAttached();
+    ManagedObject getBookmarkedOwner();
 
 }
diff --git a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/coll/DataTableModelWkt.java b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/coll/DataTableModelWktD.java
similarity index 92%
copy from viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/coll/DataTableModelWkt.java
copy to viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/coll/DataTableModelWktD.java
index 55bf6c9cb9..82b0a8a9f9 100644
--- a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/coll/DataTableModelWkt.java
+++ b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/coll/DataTableModelWktD.java
@@ -27,7 +27,6 @@ import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
 import org.apache.causeway.core.metamodel.spec.feature.OneToManyAssociation;
 import org.apache.causeway.core.metamodel.tabular.DataTableInteractive;
 import org.apache.causeway.core.metamodel.tabular.DataTableMemento;
-import org.apache.causeway.viewer.commons.model.object.HasUiParentObject;
 import org.apache.causeway.viewer.commons.model.object.UiObject;
 import org.apache.causeway.viewer.wicket.model.models.interaction.BookmarkedObjectWkt;
 import org.apache.causeway.viewer.wicket.model.models.interaction.HasBookmarkedOwnerAbstract;
@@ -46,14 +45,14 @@ import lombok.val;
  *
  * @see HasBookmarkedOwnerAbstract
  */
-public class DataTableModelWkt
+class DataTableModelWktD
 extends HasBookmarkedOwnerAbstract<DataTableInteractive>
 implements
-    HasUiParentObject<UiObject> {
+    DataTableModelWkt {
 
     // -- FACTORIES
 
-    public static DataTableModelWkt forActionModel(
+    public static DataTableModelWktD forActionModel(
             final @NonNull BookmarkedObjectWkt bookmarkedObjectModel,
             final @NonNull ObjectAction actMetaModel,
             final @NonNull ManagedObject actionResult) {
@@ -63,18 +62,18 @@ implements
         val tableInteractive = DataTableInteractive.forAction(
                 managedAction,
                 actionResult);
-        return new DataTableModelWkt(
+        return new DataTableModelWktD(
                 bookmarkedObjectModel, actMetaModel.getFeatureIdentifier(), tableInteractive);
     }
 
-    public static @NonNull DataTableModelWkt forCollection(
+    public static @NonNull DataTableModelWktD forCollection(
             final @NonNull BookmarkedObjectWkt bookmarkedObjectModel,
             final @NonNull OneToManyAssociation collMetaModel) {
 
         val tableInteractive = DataTableInteractive.forCollection(
                 ManagedCollection
                 .of(bookmarkedObjectModel.getObject(), collMetaModel, Where.NOT_SPECIFIED));
-        return new DataTableModelWkt(
+        return new DataTableModelWktD(
                 bookmarkedObjectModel, collMetaModel.getFeatureIdentifier(), tableInteractive);
     }
 
@@ -85,7 +84,7 @@ implements
     @Getter private final Identifier featureIdentifier;
     private final DataTableMemento tableMemento;
 
-    private DataTableModelWkt(
+    private DataTableModelWktD(
             final BookmarkedObjectWkt bookmarkedObject,
             final Identifier featureIdentifier,
             final DataTableInteractive tableInteractive) {
diff --git a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/coll/DataTableModelWkt.java b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/coll/DataTableModelWktO.java
similarity index 64%
copy from viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/coll/DataTableModelWkt.java
copy to viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/coll/DataTableModelWktO.java
index 55bf6c9cb9..89d808a469 100644
--- a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/coll/DataTableModelWkt.java
+++ b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/interaction/coll/DataTableModelWktO.java
@@ -20,6 +20,7 @@ package org.apache.causeway.viewer.wicket.model.models.interaction.coll;
 
 import org.apache.causeway.applib.Identifier;
 import org.apache.causeway.applib.annotation.Where;
+import org.apache.causeway.commons.collections.Can;
 import org.apache.causeway.core.metamodel.interactions.managed.ManagedAction;
 import org.apache.causeway.core.metamodel.interactions.managed.ManagedCollection;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
@@ -27,7 +28,6 @@ import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
 import org.apache.causeway.core.metamodel.spec.feature.OneToManyAssociation;
 import org.apache.causeway.core.metamodel.tabular.DataTableInteractive;
 import org.apache.causeway.core.metamodel.tabular.DataTableMemento;
-import org.apache.causeway.viewer.commons.model.object.HasUiParentObject;
 import org.apache.causeway.viewer.commons.model.object.UiObject;
 import org.apache.causeway.viewer.wicket.model.models.interaction.BookmarkedObjectWkt;
 import org.apache.causeway.viewer.wicket.model.models.interaction.HasBookmarkedOwnerAbstract;
@@ -46,36 +46,53 @@ import lombok.val;
  *
  * @see HasBookmarkedOwnerAbstract
  */
-public class DataTableModelWkt
+class DataTableModelWktO
 extends HasBookmarkedOwnerAbstract<DataTableInteractive>
 implements
-    HasUiParentObject<UiObject> {
+    DataTableModelWkt {
 
     // -- FACTORIES
 
-    public static DataTableModelWkt forActionModel(
+    public static DataTableModelWktO forActionModel(
             final @NonNull BookmarkedObjectWkt bookmarkedObjectModel,
             final @NonNull ObjectAction actMetaModel,
+            final @NonNull Can<ManagedObject> args,
             final @NonNull ManagedObject actionResult) {
 
         val managedAction = ManagedAction
                 .of(bookmarkedObjectModel.getObject(), actMetaModel, Where.NOT_SPECIFIED);
-        val tableInteractive = DataTableInteractive.forAction(
+
+        val table = DataTableInteractive.forAction(
                 managedAction,
+                args,
                 actionResult);
-        return new DataTableModelWkt(
-                bookmarkedObjectModel, actMetaModel.getFeatureIdentifier(), tableInteractive);
+
+        val tableMemento = table.createMemento(managedAction.getMementoForArgs(args));
+
+        val model = new DataTableModelWktO(
+                bookmarkedObjectModel, actMetaModel.getFeatureIdentifier(), tableMemento);
+
+        model.setObject(table); // memoize
+
+        return model;
     }
 
-    public static @NonNull DataTableModelWkt forCollection(
+    public static @NonNull DataTableModelWktO forCollection(
             final @NonNull BookmarkedObjectWkt bookmarkedObjectModel,
             final @NonNull OneToManyAssociation collMetaModel) {
 
-        val tableInteractive = DataTableInteractive.forCollection(
+        val table = DataTableInteractive.forCollection(
                 ManagedCollection
                 .of(bookmarkedObjectModel.getObject(), collMetaModel, Where.NOT_SPECIFIED));
-        return new DataTableModelWkt(
-                bookmarkedObjectModel, collMetaModel.getFeatureIdentifier(), tableInteractive);
+
+        val tableMemento = table.createMemento(null);
+
+        val model = new DataTableModelWktO(
+                bookmarkedObjectModel, collMetaModel.getFeatureIdentifier(), tableMemento);
+
+        model.setObject(table); // memoize
+
+        return model;
     }
 
     // -- CONSTRUCTION
@@ -85,15 +102,13 @@ implements
     @Getter private final Identifier featureIdentifier;
     private final DataTableMemento tableMemento;
 
-    private DataTableModelWkt(
+    private DataTableModelWktO(
             final BookmarkedObjectWkt bookmarkedObject,
             final Identifier featureIdentifier,
-            final DataTableInteractive tableInteractive) {
+            final DataTableMemento tableMemento) {
         super(bookmarkedObject);
         this.featureIdentifier = featureIdentifier;
-        this.tableMemento = tableInteractive.createMemento();
-        setObject(tableInteractive); // memoize
-        tableMemento.setupBindings(tableInteractive);
+        this.tableMemento = tableMemento;
     }
 
     // --
@@ -105,9 +120,33 @@ implements
 
     @Override
     protected DataTableInteractive load() {
-        val tableInteractive = tableMemento.getDataTableModel(getBookmarkedOwner());
-        tableMemento.setupBindings(tableInteractive);
-        return tableInteractive;
+        val dataTableModel = tableMemento.getDataTableModel(getBookmarkedOwner());
+        return dataTableModel;
+    }
+
+    @Override
+    public final void detach() {
+        if(isDataTableModelDetachable()) {
+            // at time of writing breaks object deletion on JDO, see [CAUSEWAY-3530]
+            super.detach();
+        }
+        //FIXME[CAUSEWAY-3522]
+        // perhaps instead call bookmarkedObjectModel().detach();
+        // or add custom HOLLOW flag and mark the bookmarkedObjectModel() hollow
     }
 
+    // -- HELPER
+
+    private final static String PROPERTY_NAME_MODEL_REUSE = "causeway.viewer.wicket.dataTableModelReuse";
+    /**
+     * when set to false, forces detach
+     * @deprecated remove this switch once we have a fix
+     */
+    @Deprecated
+    private static boolean isDataTableModelDetachable() {
+        return "false".equalsIgnoreCase(System.getenv(PROPERTY_NAME_MODEL_REUSE))
+                || "false".equalsIgnoreCase(System.getProperty(PROPERTY_NAME_MODEL_REUSE));
+    }
+
+
 }
diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/actionresponse/ActionResultResponseType.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/actionresponse/ActionResultResponseType.java
index 0ea92ed682..6153f8f7f2 100644
--- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/actionresponse/ActionResultResponseType.java
+++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/actionresponse/ActionResultResponseType.java
@@ -31,6 +31,7 @@ import org.apache.causeway.applib.value.Blob;
 import org.apache.causeway.applib.value.Clob;
 import org.apache.causeway.applib.value.LocalResourcePath;
 import org.apache.causeway.applib.value.OpenUrlStrategy;
+import org.apache.causeway.commons.collections.Can;
 import org.apache.causeway.commons.internal.assertions._Assert;
 import org.apache.causeway.commons.internal.base._Casts;
 import org.apache.causeway.commons.internal.base._NullSafe;
@@ -62,7 +63,8 @@ public enum ActionResultResponseType {
         public ActionResultResponse interpretResult(
                 final ActionModel actionModel,
                 final AjaxRequestTarget target,
-                final @NonNull ManagedObject resultAdapter) {
+                final @NonNull ManagedObject resultAdapter,
+                final Can<ManagedObject> args) {
             determineScalarAdapter(actionModel.getMetaModelContext(), resultAdapter); // intercepts collections
             return toEntityPage(resultAdapter);
         }
@@ -79,11 +81,12 @@ public enum ActionResultResponseType {
         public ActionResultResponse interpretResult(
                 final ActionModel actionModel,
                 final AjaxRequestTarget target,
-                final @NonNull ManagedObject resultAdapter) {
+                final @NonNull ManagedObject resultAdapter,
+                final Can<ManagedObject> args) {
             _Assert.assertTrue(resultAdapter instanceof PackedManagedObject);
 
             final var collectionModel = EntityCollectionModelStandalone
-                    .forActionModel((PackedManagedObject)resultAdapter, actionModel);
+                    .forActionModel((PackedManagedObject)resultAdapter, actionModel, args);
             return ActionResultResponse.toPage(
                     StandaloneCollectionPage.class, new StandaloneCollectionPage(collectionModel));
         }
@@ -96,7 +99,8 @@ public enum ActionResultResponseType {
         public ActionResultResponse interpretResult(
                 final ActionModel actionModel,
                 final AjaxRequestTarget target,
-                final @NonNull ManagedObject resultAdapter) {
+                final @NonNull ManagedObject resultAdapter,
+                final Can<ManagedObject> args) {
             final var valueModel = ValueModel.of(actionModel.getAction(), resultAdapter);
             valueModel.setActionHint(actionModel);
             final var valuePage = new ValuePage(valueModel);
@@ -108,7 +112,8 @@ public enum ActionResultResponseType {
         public ActionResultResponse interpretResult(
                 final ActionModel actionModel,
                 final AjaxRequestTarget target,
-                final @NonNull ManagedObject resultAdapter) {
+                final @NonNull ManagedObject resultAdapter,
+                final Can<ManagedObject> args) {
             final Object value = resultAdapter.getPojo();
             IRequestHandler handler =
                     _DownloadHandler.downloadHandler(actionModel.getAction(), value);
@@ -120,7 +125,8 @@ public enum ActionResultResponseType {
         public ActionResultResponse interpretResult(
                 final ActionModel actionModel,
                 final AjaxRequestTarget target,
-                final @NonNull ManagedObject resultAdapter) {
+                final @NonNull ManagedObject resultAdapter,
+                final Can<ManagedObject> args) {
             final Object value = resultAdapter.getPojo();
             IRequestHandler handler =
                     _DownloadHandler.downloadHandler(actionModel.getAction(), value);
@@ -132,7 +138,8 @@ public enum ActionResultResponseType {
         public ActionResultResponse interpretResult(
                 final ActionModel actionModel,
                 final AjaxRequestTarget target,
-                final @NonNull ManagedObject resultAdapter) {
+                final @NonNull ManagedObject resultAdapter,
+                final Can<ManagedObject> args) {
             final LocalResourcePath localResPath = (LocalResourcePath)resultAdapter.getPojo();
             final var webAppContextPath = actionModel.getMetaModelContext().getWebAppContextPath();
             return ActionResultResponse
@@ -144,7 +151,8 @@ public enum ActionResultResponseType {
         public ActionResultResponse interpretResult(
                 final ActionModel actionModel,
                 final AjaxRequestTarget target,
-                final @NonNull ManagedObject resultAdapter) {
+                final @NonNull ManagedObject resultAdapter,
+                final Can<ManagedObject> args) {
             // open URL server-side redirect
             final LocalResourcePath localResPath = (LocalResourcePath)resultAdapter.getPojo();
             final var webAppContextPath = actionModel.getMetaModelContext().getWebAppContextPath();
@@ -157,7 +165,8 @@ public enum ActionResultResponseType {
         public ActionResultResponse interpretResult(
                 final ActionModel actionModel,
                 final AjaxRequestTarget target,
-                final @NonNull ManagedObject resultAdapter) {
+                final @NonNull ManagedObject resultAdapter,
+                final Can<ManagedObject> args) {
             final URL url = (URL)resultAdapter.getPojo();
             return ActionResultResponse
                     .openUrlInBrowser(target, url.toString(), OpenUrlStrategy.NEW_WINDOW); // default behavior
@@ -168,7 +177,8 @@ public enum ActionResultResponseType {
         public ActionResultResponse interpretResult(
                 final ActionModel actionModel,
                 final AjaxRequestTarget target,
-                final @NonNull ManagedObject resultAdapter) {
+                final @NonNull ManagedObject resultAdapter,
+                final Can<ManagedObject> args) {
             // open URL server-side redirect
             final Object value = resultAdapter.getPojo();
             final var webAppContextPath = actionModel.getMetaModelContext().getWebAppContextPath();
@@ -182,7 +192,8 @@ public enum ActionResultResponseType {
         public ActionResultResponse interpretResult(
                 final ActionModel actionModel,
                 final AjaxRequestTarget target,
-                final @Nullable ManagedObject resultAdapter) {
+                final @Nullable ManagedObject resultAdapter, // arg is not used
+                final Can<ManagedObject> args) {
             final VoidModel voidModel = new VoidModel();
             voidModel.setActionHint(actionModel);
             return ActionResultResponse.toPage(VoidReturnPage.class, new VoidReturnPage(voidModel));
@@ -196,7 +207,8 @@ public enum ActionResultResponseType {
         public ActionResultResponse interpretResult(
                 final ActionModel actionModel,
                 final AjaxRequestTarget target,
-                final @Nullable ManagedObject resultAdapter) {
+                final @Nullable ManagedObject resultAdapter, // arg is not used
+                final Can<ManagedObject> args) {
             val currentPage = PageRequestHandlerTracker.getLastHandler(RequestCycle.get()).getPage();
             val pageClass = currentPage.getClass();
             return ActionResultResponse.toPage(PageRedirectRequest.forPage(pageClass, _Casts.uncheckedCast(currentPage)));
@@ -207,7 +219,8 @@ public enum ActionResultResponseType {
         public ActionResultResponse interpretResult(
                 final ActionModel actionModel,
                 final AjaxRequestTarget target,
-                final @Nullable ManagedObject resultAdapter) {
+                final @Nullable ManagedObject resultAdapter, // arg is not used
+                final Can<ManagedObject> args) {
             val signInPage = actionModel.getMetaModelContext()
                     .lookupServiceElseFail(PageClassRegistry.class)
                     .getPageClass(PageType.SIGN_IN);
@@ -217,7 +230,7 @@ public enum ActionResultResponseType {
     };
 
     public abstract ActionResultResponse interpretResult(
-            ActionModel model, AjaxRequestTarget target, ManagedObject resultAdapter);
+            ActionModel model, AjaxRequestTarget target, ManagedObject resultAdapter, Can<ManagedObject> args);
 
     /**
      * Only overridden for {@link ActionResultResponseType#OBJECT object}
@@ -231,7 +244,8 @@ public enum ActionResultResponseType {
     public static ActionResultResponse determineAndInterpretResult(
             final ActionModel model,
             final AjaxRequestTarget targetIfAny,
-            final @Nullable ManagedObject resultAdapter) {
+            final @Nullable ManagedObject resultAdapter,
+            final Can<ManagedObject> args) {
 
         /*
          * XXX won't implement CAUSEWAY-3372 (reload on void action result)
@@ -247,7 +261,7 @@ public enum ActionResultResponseType {
 
         val typeAndAdapter = determineFor(resultAdapter, mapAbsentResultTo, targetIfAny);
         return typeAndAdapter.type // mapped to 'mapAbsentResultTo' if adapter is unspecified or null
-                .interpretResult(model, targetIfAny, typeAndAdapter.resultAdapter);
+                .interpretResult(model, targetIfAny, typeAndAdapter.resultAdapter, args);
     }
 
     public static ActionResultResponse toEntityPage(final @NonNull ManagedObject entityOrViewmodel) {
diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/CausewayAjaxDataTable.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/CausewayAjaxDataTable.java
index 240a458b40..d3ccbc5099 100644
--- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/CausewayAjaxDataTable.java
+++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/CausewayAjaxDataTable.java
@@ -18,21 +18,30 @@
  */
 package org.apache.causeway.viewer.wicket.ui.components.collectioncontents.ajaxtable;
 
+import java.io.Serializable;
+import java.util.Iterator;
 import java.util.List;
+import java.util.UUID;
 
 import org.apache.wicket.ajax.AjaxRequestTarget;
 import org.apache.wicket.extensions.markup.html.repeater.data.sort.SortOrder;
 import org.apache.wicket.extensions.markup.html.repeater.data.table.DataTable;
 import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn;
 import org.apache.wicket.extensions.markup.html.repeater.data.table.NoRecordsToolbar;
+import org.apache.wicket.markup.repeater.IItemFactory;
+import org.apache.wicket.markup.repeater.IItemReuseStrategy;
 import org.apache.wicket.markup.repeater.Item;
 import org.apache.wicket.markup.repeater.data.IDataProvider;
 import org.apache.wicket.model.IModel;
 
+import org.apache.causeway.commons.internal.base._Casts;
+import org.apache.causeway.commons.internal.collections._Maps;
 import org.apache.causeway.core.metamodel.object.ManagedObjects;
 import org.apache.causeway.core.metamodel.tabular.DataRow;
+import org.apache.causeway.core.metamodel.tabular.DataTableInteractive;
 import org.apache.causeway.viewer.wicket.model.hints.UiHintContainer;
 import org.apache.causeway.viewer.wicket.model.models.UiObjectWkt;
+import org.apache.causeway.viewer.wicket.model.models.interaction.coll.DataRowWkt;
 import org.apache.causeway.viewer.wicket.ui.components.collectioncontents.ajaxtable.columns.ToggleboxColumn;
 import org.apache.causeway.viewer.wicket.ui.util.Wkt;
 
@@ -62,8 +71,9 @@ public class CausewayAjaxDataTable extends DataTable<DataRow, String> {
         this.toggleboxColumn = toggleboxColumn;
         setOutputMarkupId(true);
         setVersioned(false);
-        //[CAUSEWAY-3772] optimization reinstate? though I have no clue what that is doing
-        //setItemReuseStrategy((IItemReuseStrategy & Serializable) CausewayAjaxDataTable::itemReuseStrategyWithCast);
+        if(DataTableInteractive.TableImplementation.getSelected().isOptimistic()) {
+            setItemReuseStrategy((IItemReuseStrategy & Serializable) CausewayAjaxDataTable::itemReuseStrategyWithCast);
+        }
     }
 
     public void setPageNumberHintAndBroadcast(final AjaxRequestTarget target) {
@@ -137,58 +147,57 @@ public class CausewayAjaxDataTable extends DataTable<DataRow, String> {
         return rowElement.getSpecification().getCssClass(rowElement);
     }
 
-//    private static Iterator<Item<DataRow>> itemReuseStrategy(
-//            final IItemFactory<DataRow> factory,
-//            final Iterator<IModel<DataRow>> newModels,
-//            final Iterator<Item<DataRow>> existingItems) {
-//
-//        val itemByRowIndex = _Maps.<Integer, Item<DataRow>>newHashMap();
-//        existingItems.forEachRemaining(item->{
-//            val model = item.getModel();
-//            if(model instanceof DataRowWkt) {
-//                val dataRowWkt = (DataRowWkt)item.getModel();
-//                itemByRowIndex.put(dataRowWkt.getRowIndex(), item);
-//            }
-//        });
-//
-//        return new Iterator<Item<DataRow>>() {
-//            private int index = 0;
-//
-//            @Override
-//            public boolean hasNext() {
-//                return newModels.hasNext();
-//            }
-//
-//            @Override
-//            public Item<DataRow> next() {
-//                final DataRowWkt newModel = (DataRowWkt)newModels.next();
-//                final Item<DataRow> oldItem = itemByRowIndex.get(newModel.getRowIndex());
-//
-//                final IModel<DataRow> model2 = oldItem != null
-//                        ? oldItem.getModel()
-//                        : newModel;
-//                return factory.newItem(index++, model2);
-//            }
-//
-//            @Override
-//            public void remove() {
-//                throw new UnsupportedOperationException();
-//            }
-//
-//        };
-//
-//    }
-//
-//    @SuppressWarnings("unused")
-//    private static <T> Iterator<Item<T>> itemReuseStrategyWithCast(
-//            final IItemFactory<T> factory,
-//            final Iterator<IModel<T>> newModels,
-//            final Iterator<Item<T>> existingItems) {
-//        return _Casts.uncheckedCast(itemReuseStrategy(
-//                _Casts.uncheckedCast(factory),
-//                _Casts.uncheckedCast(newModels),
-//                _Casts.uncheckedCast(existingItems)));
-//    }
+    private static Iterator<Item<DataRow>> itemReuseStrategy(
+            final IItemFactory<DataRow> factory,
+            final Iterator<IModel<DataRow>> newModels,
+            final Iterator<Item<DataRow>> existingItems) {
+
+        val itemByUuid = _Maps.<UUID, Item<DataRow>>newHashMap();
+        existingItems.forEachRemaining(item->{
+            val model = item.getModel();
+            if(model instanceof DataRowWkt) {
+                val dataRowWkt = (DataRowWkt)item.getModel();
+                itemByUuid.put(dataRowWkt.getUuid(), item);
+            }
+        });
+
+        return new Iterator<Item<DataRow>>() {
+            private int index = 0;
+
+            @Override
+            public boolean hasNext() {
+                return newModels.hasNext();
+            }
+
+            @Override
+            public Item<DataRow> next() {
+                final DataRowWkt newModel = (DataRowWkt)newModels.next();
+                final Item<DataRow> oldItem = itemByUuid.get(newModel.getUuid());
+
+                final IModel<DataRow> model2 = oldItem != null
+                        ? oldItem.getModel()
+                        : newModel;
+                return factory.newItem(index++, model2);
+            }
+
+            @Override
+            public void remove() {
+                throw new UnsupportedOperationException();
+            }
+
+        };
+
+    }
+
+    private static <T> Iterator<Item<T>> itemReuseStrategyWithCast(
+            final IItemFactory<T> factory,
+            final Iterator<IModel<T>> newModels,
+            final Iterator<Item<T>> existingItems) {
+        return _Casts.uncheckedCast(itemReuseStrategy(
+                _Casts.uncheckedCast(factory),
+                _Casts.uncheckedCast(newModels),
+                _Casts.uncheckedCast(existingItems)));
+    }
 
     private void honorHints() {
         headersToolbar.honourSortOrderHints();
diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/CausewayAjaxHeadersToolbar.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/CausewayAjaxHeadersToolbar.java
index 3bc744e8a9..6388d6ddc1 100644
--- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/CausewayAjaxHeadersToolbar.java
+++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/CausewayAjaxHeadersToolbar.java
@@ -26,7 +26,6 @@ import org.apache.wicket.markup.html.WebMarkupContainer;
 import org.apache.causeway.commons.internal.base._Casts;
 import org.apache.causeway.viewer.wicket.model.hints.UiHintContainer;
 import org.apache.causeway.viewer.wicket.model.models.UiObjectWkt;
-import org.apache.causeway.viewer.wicket.ui.util.Wkt;
 
 /**
  * Adapted from Wicket's own {@link AjaxFallbackHeadersToolbar}.
@@ -35,16 +34,16 @@ public class CausewayAjaxHeadersToolbar
 extends CausewayAjaxHeadersToolbarAbstract<String> {
 
     private static final long serialVersionUID = 1L;
-    private final CollectionContentsSortableDataProvider singleSortStateLocator;
+    private final CollectionContentsSortableDataProvider stateLocator;
     private CausewayAjaxDataTable table;
 
     public CausewayAjaxHeadersToolbar(
             final CausewayAjaxDataTable table,
-            final CollectionContentsSortableDataProvider singleSortStateLocator) {
-        super(table, _Casts.uncheckedCast(singleSortStateLocator));
+            final CollectionContentsSortableDataProvider stateLocator) {
+        super(table, _Casts.uncheckedCast(stateLocator));
         this.table = table;
-        this.singleSortStateLocator = singleSortStateLocator;
-        Wkt.ajaxEnable(table);
+        table.setOutputMarkupId(true);
+        this.stateLocator = stateLocator;
     }
 
     @Override
@@ -52,20 +51,36 @@ extends CausewayAjaxHeadersToolbarAbstract<String> {
         super.onInitialize();
     }
 
+    // //////////////////////////////////////
+
     @Override
     protected WebMarkupContainer newSortableHeader(final String borderId, final String property,
             final ISortStateLocator<String> locator) {
-        return new CausewayAjaxFallbackOrderByBorder<String>(borderId, table, property, locator);
+        return new CausewayAjaxFallbackOrderByBorder<String>(borderId, table, property, locator/*, getAjaxCallListener()*/);
     }
 
+    //    /**
+    //     * Returns a decorator that will be used to decorate ajax links used in sortable headers
+    //     *
+    //     * @return decorator or null for none
+    //     */
+    //    protected IAjaxCallListener getAjaxCallListener()
+    //    {
+    //        return null;
+    //    }
+
+    // //////////////////////////////////////
+
     void honourSortOrderHints() {
-        var uiHintContainer = getUiHintContainer();
-        if(uiHintContainer == null) return;
+        UiHintContainer uiHintContainer = getUiHintContainer();
+        if(uiHintContainer == null) {
+            return;
+        }
 
         for (SortOrder sortOrder : SortOrder.values()) {
             String property = uiHintContainer.getHint(table, sortOrder.name());
             if(property != null) {
-                singleSortStateLocator.getSortState().setPropertySortOrder(property, sortOrder);
+                stateLocator.getSortState().setPropertySortOrder(property, sortOrder);
             }
         }
     }
diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/CollectionContentsSortableDataProvider.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/CollectionContentsSortableDataProvider.java
index 3cb32edc62..e69cca6341 100644
--- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/CollectionContentsSortableDataProvider.java
+++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/CollectionContentsSortableDataProvider.java
@@ -20,24 +20,24 @@ package org.apache.causeway.viewer.wicket.ui.components.collectioncontents.ajaxt
 
 import java.util.Iterator;
 import java.util.Optional;
-import java.util.OptionalInt;
 
 import org.apache.wicket.extensions.ajax.markup.html.repeater.data.table.AjaxFallbackDefaultDataTable;
 import org.apache.wicket.extensions.markup.html.repeater.util.SortParam;
 import org.apache.wicket.extensions.markup.html.repeater.util.SortableDataProvider;
 import org.apache.wicket.model.IModel;
 
-import org.springframework.lang.Nullable;
-
 import org.apache.causeway.applib.annotation.TableDecorator;
+import org.apache.causeway.commons.collections.Can;
 import org.apache.causeway.core.metamodel.facets.object.tabledec.TableDecoratorFacet;
+import org.apache.causeway.core.metamodel.object.ManagedObject;
+import org.apache.causeway.core.metamodel.object.ManagedObjects;
 import org.apache.causeway.core.metamodel.object.MmSortUtils;
+import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
 import org.apache.causeway.core.metamodel.tabular.DataRow;
 import org.apache.causeway.core.metamodel.tabular.DataTableInteractive;
 import org.apache.causeway.viewer.wicket.model.models.EntityCollectionModelAbstract;
 import org.apache.causeway.viewer.wicket.model.models.interaction.coll.DataRowWkt;
 
-import lombok.NonNull;
 import lombok.val;
 
 /**
@@ -79,39 +79,38 @@ extends SortableDataProvider<DataRow, String> {
 
     @Override
     public Iterator<DataRow> iterator(final long skip, final long limit) {
-        var dataTable = getDataTableModel();
-        // honor (single) column sort (if any)
-        dataTable.getColumnSort().setValue(columnSort().orElse(null));
-        return dataTable.getDataRowsFilteredAndSorted().getValue()
-                .iterator(Math.toIntExact(skip), Math.toIntExact(limit));
+        val visibleRows = getDataTableModel().getDataRowsFilteredAndSorted().getValue();
+        return sorted(visibleRows).iterator(Math.toIntExact(skip), Math.toIntExact(limit));
     }
 
     // -- HELPER
 
-    private Optional<DataTableInteractive.ColumnSort> columnSort() {
-        val sortParam = getSort();
-        return lookupColumnIndexFor(sortParam).stream()
-                .mapToObj(columnIndex->
-                    new DataTableInteractive.ColumnSort(columnIndex, sortDirection(sortParam)))
-                .findFirst();
-    }
-
-    private OptionalInt lookupColumnIndexFor(final @Nullable SortParam<String> sortParam) {
-        if(sortParam==null) return OptionalInt.empty();
-        int columnIndex = 0;
-        for(var column : getDataTableModel().getDataColumns().getValue()) {
-            if(column.getAssociationMetaModel().getId().equals(sortParam.getProperty())) {
-                return OptionalInt.of(columnIndex);
-            }
-            ++columnIndex;
+    private Can<DataRow> sorted(final Can<DataRow> dataRows) {
+        val sort = getSort();
+        val sortProperty = lookupPropertyFor(sort).orElse(null);
+        if(sortProperty != null) {
+            val objComparator = MmSortUtils.orderingBy(sortProperty, sort.isAscending());
+            return dataRows.sorted((a, b)-> {
+                ManagedObject managedObjectA = a.getRowElement();
+                if(managedObjectA.getSpecialization().isViewmodel()) {
+                    // make sure any referenced entities are made live if currently hollow
+                    ManagedObjects.refreshViewmodel(managedObjectA, /*bookmark supplier*/ null);
+                }
+                ManagedObject managedObjectB = b.getRowElement();
+                if(managedObjectA.getSpecialization().isViewmodel()) {
+                    // make sure any referenced entities are made live if currently hollow
+                    ManagedObjects.refreshViewmodel(managedObjectB, /*bookmark supplier*/ null);
+                }
+                return objComparator.compare(managedObjectA, managedObjectB);
+            });
         }
-        return OptionalInt.empty();
+        return dataRows;
     }
 
-    private static MmSortUtils.SortDirection sortDirection(final @NonNull SortParam<String> sortParam) {
-        return sortParam.isAscending()
-                ? MmSortUtils.SortDirection.ASCENDING
-                : MmSortUtils.SortDirection.DESCENDING;
+    private Optional<OneToOneAssociation> lookupPropertyFor(final SortParam<String> sort) {
+        return Optional.ofNullable(sort)
+        .map(SortParam::getProperty)
+        .flatMap(getDataTableModel().getElementType()::getProperty);
     }
 
 }
\ No newline at end of file
diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/ToggleboxColumn.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/ToggleboxColumn.java
index de0862a1a8..fc2bfa382f 100644
--- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/ToggleboxColumn.java
+++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/ToggleboxColumn.java
@@ -26,8 +26,10 @@ import org.apache.wicket.model.IModel;
 
 import org.apache.causeway.commons.internal.collections._Lists;
 import org.apache.causeway.core.metamodel.tabular.DataTableInteractive;
+import org.apache.causeway.core.metamodel.tabular.DataTableInteractive.TableImplementation;
 import org.apache.causeway.viewer.wicket.model.models.interaction.coll.DataRowToggleWkt;
 import org.apache.causeway.viewer.wicket.model.models.interaction.coll.DataRowWkt;
+import org.apache.causeway.viewer.wicket.model.models.interaction.coll.DataRowWktO;
 import org.apache.causeway.viewer.wicket.ui.components.widgets.checkbox.ContainedToggleboxPanel;
 import org.apache.causeway.viewer.wicket.ui.util.Wkt;
 
@@ -61,7 +63,9 @@ extends GenericColumnAbstract {
 
     @Override
     protected Component createCellComponent(final String componentId, final DataRowWkt dataRowWkt) {
-        val dataRowToggle = new DataRowToggleWkt(dataRowWkt);
+        final IModel<Boolean> dataRowToggle = TableImplementation.getSelected().isOptimistic()
+                ? ((DataRowWktO)dataRowWkt).getDataRowToggle()
+                : new DataRowToggleWkt(dataRowWkt);
         val rowToggle = new ContainedToggleboxPanel(componentId, dataRowToggle);
         rowToggles.add(rowToggle);
         return rowToggle.setOutputMarkupId(true);
@@ -72,7 +76,9 @@ extends GenericColumnAbstract {
         val bulkToggle = new ContainedToggleboxPanel(
                 componentId,
                 new BulkToggleWkt(dataTableModelHolder),
-                    this::onBulkUpdate);
+                TableImplementation.getSelected().isOptimistic()
+                ? this::onBulkUpdateOptimistic
+                : this::onBulkUpdate);
         Wkt.cssAppend(bulkToggle, "togglebox-column");
         return bulkToggle;
     }
@@ -91,4 +97,11 @@ extends GenericColumnAbstract {
 
     }
 
+    private void onBulkUpdateOptimistic(final Boolean isChecked, final AjaxRequestTarget target) {
+        val bulkToggle = BulkToggle.valueOf(isChecked);
+        for (ContainedToggleboxPanel rowToggle : rowToggles) {
+            rowToggle.set(bulkToggle, target);
+        }
+    }
+
 }
\ No newline at end of file
diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/panels/FormExecutorDefault.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/panels/FormExecutorDefault.java
index dfdecb59f1..bbbea14947 100644
--- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/panels/FormExecutorDefault.java
+++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/panels/FormExecutorDefault.java
@@ -161,7 +161,7 @@ implements FormExecutor, HasCommonContext {
             //XXX triggers ManagedObject.getBookmarkRefreshed()
             val resultResponse = actionOrPropertyModel.fold(
                     act->ActionResultResponseType
-                            .determineAndInterpretResult(act, ajaxTarget, resultAdapter),
+                            .determineAndInterpretResult(act, ajaxTarget, resultAdapter, act.snapshotArgs()),
                     prop->ActionResultResponseType
                             .toEntityPage(resultAdapter));