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 2026/06/15 07:04:09 UTC

(causeway) 01/01: CAUSEWAY-4035: honor Qualified ValueSemantics when rendering Choices

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

andi-huber pushed a commit to branch 4035-qual.valsem.choices
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 27a0f2fa411df6b4c96521f4a765178ce4ff55bd
Author: andi-huber <ah...@apache.org>
AuthorDate: Mon Jun 15 09:03:56 2026 +0200

    CAUSEWAY-4035: honor Qualified ValueSemantics when rendering Choices
    
    Task-Url: https://issues.apache.org/jira/browse/CAUSEWAY-4035
---
 .../interactions/managed/ManagedAction.java        | 36 +-----------------
 .../core/metamodel/object/Bookmarkable.java        | 11 ++++--
 .../core/metamodel/object/ManagedObjectEmpty.java  |  4 +-
 .../core/metamodel/object/ManagedObjectEntity.java | 12 +++---
 .../metamodel/object/ManagedObjectService.java     |  6 ++-
 .../core/metamodel/object/ManagedObjectValue.java  |  6 ++-
 .../metamodel/object/ManagedObjectViewmodel.java   |  7 ++--
 .../core/metamodel/object/MmValueUtils.java        | 36 ++++++++++++------
 .../core/metamodel/object/PackedManagedObject.java |  9 +++--
 .../metamodel/objectmanager/ObjectManager.java     | 43 ++++++++++------------
 .../objectmanager/memento/ObjectMemento.java       | 19 ++++++----
 .../wicket/model/models/MultiChoiceModel.java      | 10 ++++-
 .../wicket/model/models/SingleChoiceModel.java     |  2 +-
 .../viewer/wicket/model/models/ValueModel.java     |  3 +-
 .../components/widgets/select2/ChoiceProvider.java | 20 ++++++----
 15 files changed, 114 insertions(+), 110 deletions(-)

diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/managed/ManagedAction.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/managed/ManagedAction.java
index 5b21c9409c2..dc78c2cb6ca 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/managed/ManagedAction.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/managed/ManagedAction.java
@@ -18,7 +18,6 @@
  */
 package org.apache.causeway.core.metamodel.interactions.managed;
 
-import java.io.Serializable;
 import java.util.Optional;
 
 import org.jspecify.annotations.NonNull;
@@ -38,14 +37,10 @@
 import org.apache.causeway.core.metamodel.interactions.InteractionHead;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
 import org.apache.causeway.core.metamodel.object.ManagedObjects;
-import org.apache.causeway.core.metamodel.objectmanager.ObjectManager;
-import org.apache.causeway.core.metamodel.objectmanager.memento.ObjectMemento;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectMember.AuthorizationException;
 
-import lombok.AccessLevel;
 import lombok.Getter;
-import lombok.RequiredArgsConstructor;
 import lombok.SneakyThrows;
 
 public final class ManagedAction extends ManagedMember {
@@ -167,9 +162,8 @@ public final ManagedObject invokeWithRuleChecking(
 
     private ManagedObject route(final @Nullable ManagedObject actionResult) {
 
-        if(ManagedObjects.isNullOrUnspecifiedOrEmpty(actionResult)) {
+        if(ManagedObjects.isNullOrUnspecifiedOrEmpty(actionResult))
             return ManagedObject.empty(action.getReturnType());
-        }
 
         var resultPojo = actionResult.getPojo();
         var objManager = mmc().getObjectManager();
@@ -206,32 +200,4 @@ private ServiceRegistry getServiceRegistry() {
         return mmc().getServiceRegistry();
     }
 
-    // -- MEMENTO FOR ARGUMENT LIST
-
-    public MementoForArgs getMementoForArgs(final Can<ManagedObject> args) {
-        return MementoForArgs.create(
-                getMetaModel().getMetaModelContext().getObjectManager(),
-                args);
-    }
-
-    @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
-    public static class MementoForArgs implements Serializable {
-        private static final long serialVersionUID = 1L;
-
-        static MementoForArgs create(
-                final ObjectManager objectManager,
-                final Can<ManagedObject> args) {
-            return new MementoForArgs(args.map(objectManager::mementifyElseFail));
-        }
-
-        private final Can<ObjectMemento> argsMementos;
-
-        public Can<ManagedObject> getArgumentList(final ObjectAction actionMeta) {
-            var argTypes = actionMeta.getParameterTypes();
-            var objectManager = actionMeta.getMetaModelContext().getObjectManager();
-            return argsMementos.zipMap(argTypes, (argMemento, argSpec)->
-                objectManager.demementify(argMemento));
-        }
-    }
-
 }
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/Bookmarkable.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/Bookmarkable.java
index 9a202bc84c8..226a17c80d6 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/Bookmarkable.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/Bookmarkable.java
@@ -21,12 +21,15 @@
 import java.io.Serializable;
 import java.util.Optional;
 
+import org.jspecify.annotations.Nullable;
+
 import org.apache.causeway.applib.services.bookmark.Bookmark;
 import org.apache.causeway.commons.internal.base._Casts;
 import org.apache.causeway.commons.internal.exceptions._Exceptions;
 import org.apache.causeway.core.metamodel.object.ManagedObject.Specialization.BookmarkPolicy;
 import org.apache.causeway.core.metamodel.objectmanager.ObjectManager;
 import org.apache.causeway.core.metamodel.objectmanager.memento.ObjectMemento;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectFeature;
 
 public interface Bookmarkable {
 
@@ -67,9 +70,9 @@ default Optional<Bookmark> refreshBookmark() {
      *  with an additional memorized object title, based on whether
      *  is supported and a {@link Bookmark} is available.
      */
-    Optional<ObjectMemento> getMemento();
-    default ObjectMemento getMementoElseFail() {
-        return getMemento()
+    Optional<ObjectMemento> getMemento(final @Nullable ObjectFeature feature);
+    default ObjectMemento getMementoElseFail(final @Nullable ObjectFeature feature) {
+        return getMemento(feature)
                 .orElseThrow(()->_Exceptions.illegalState("failed to create memento for %s", this));
     }
 
@@ -83,7 +86,7 @@ static interface NoBookmark extends Bookmarkable {
         @Override default boolean isBookmarkSupported() { return false; }
         @Override default Optional<Bookmark> getBookmark() { return Optional.empty(); }
         @Override default boolean isBookmarkMemoized() { return false; }
-        @Override default Optional<ObjectMemento> getMemento() { return Optional.empty(); }
+        @Override default Optional<ObjectMemento> getMemento(final @Nullable ObjectFeature feature) { return Optional.empty(); }
     }
 
     static interface BookmarkRefreshable extends Bookmarkable {
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/ManagedObjectEmpty.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/ManagedObjectEmpty.java
index 85247871e6f..75535ba0cdb 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/ManagedObjectEmpty.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/ManagedObjectEmpty.java
@@ -21,9 +21,11 @@
 import java.util.Optional;
 
 import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
 
 import org.apache.causeway.core.metamodel.objectmanager.memento.ObjectMemento;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectFeature;
 
 /**
  * (package private) specialization corresponding to {@link Specialization#EMPTY}
@@ -49,7 +51,7 @@ public String getTitle() {
     }
 
     @Override
-    public Optional<ObjectMemento> getMemento() {
+    public Optional<ObjectMemento> getMemento(final @Nullable ObjectFeature feature) {
         return Optional.of(ObjectMemento.empty(objSpec().logicalType()));
     }
 
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/ManagedObjectEntity.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/ManagedObjectEntity.java
index 9afa368826d..47327b83041 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/ManagedObjectEntity.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/ManagedObjectEntity.java
@@ -23,6 +23,7 @@
 import java.util.function.Consumer;
 
 import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
 
 import org.apache.causeway.applib.exceptions.unrecoverable.ObjectNotFoundException;
 import org.apache.causeway.applib.services.bookmark.Bookmark;
@@ -32,6 +33,7 @@
 import org.apache.causeway.core.metamodel.facets.object.title.TitleRenderRequest;
 import org.apache.causeway.core.metamodel.objectmanager.memento.ObjectMemento;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectFeature;
 
 import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
@@ -77,9 +79,9 @@ private static Optional<PhaseState> transitionAdvice(final PhaseState previous,
                 case TRANSIENT->{
                     var stayTransient = !entityState.isRemoved()
                         && !entityState.isAttached();
-                    if(stayTransient) yield Optional.empty();
-                    if(entityState.hasOid()) yield Optional.of(PhaseState.BOOKMARKED);
-                    if(entityState.isTransientOrRemoved()) yield Optional.of(PhaseState.REMOVED);
+                    if(stayTransient){yield Optional.empty();}
+                    if(entityState.hasOid()){yield Optional.of(PhaseState.BOOKMARKED);}
+                    if(entityState.isTransientOrRemoved()){yield Optional.of(PhaseState.REMOVED);}
                     yield Optional.empty();
                 }
                 case BOOKMARKED->entityState.isTransientOrRemoved()
@@ -112,8 +114,8 @@ public String getTitle() {
     }
 
     @Override
-    public Optional<ObjectMemento> getMemento() {
-        return Optional.ofNullable(ObjectMemento.singularOrEmpty(this));
+    public Optional<ObjectMemento> getMemento(final @Nullable ObjectFeature feature) {
+        return Optional.ofNullable(ObjectMemento.singularOrEmpty(feature, this));
     }
 
     @Override
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/ManagedObjectService.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/ManagedObjectService.java
index 49382dbc5fa..040007f0cd1 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/ManagedObjectService.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/ManagedObjectService.java
@@ -22,12 +22,14 @@
 import java.util.Optional;
 
 import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
 
 import org.apache.causeway.applib.services.bookmark.Bookmark;
 import org.apache.causeway.commons.internal.assertions._Assert;
 import org.apache.causeway.core.metamodel.facets.object.title.TitleRenderRequest;
 import org.apache.causeway.core.metamodel.objectmanager.memento.ObjectMemento;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectFeature;
 
 /**
  * (package private) specialization corresponding to {@link Specialization#SERVICE}
@@ -58,8 +60,8 @@ public boolean isBookmarkMemoized() {
     }
 
     @Override
-    public Optional<ObjectMemento> getMemento() {
-        return ObjectMemento.singular(this);
+    public Optional<ObjectMemento> getMemento(final @Nullable ObjectFeature feature) {
+        return ObjectMemento.singular(feature, this);
     }
 
     @Override
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/ManagedObjectValue.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/ManagedObjectValue.java
index 6eb7d92736d..eb7444cae6e 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/ManagedObjectValue.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/ManagedObjectValue.java
@@ -22,6 +22,7 @@
 import java.util.Optional;
 
 import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
 
 import org.apache.causeway.applib.services.bookmark.Bookmark;
 import org.apache.causeway.commons.internal.assertions._Assert;
@@ -31,6 +32,7 @@
 import org.apache.causeway.core.metamodel.facets.object.value.ValueSerializer.Format;
 import org.apache.causeway.core.metamodel.objectmanager.memento.ObjectMemento;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectFeature;
 
 /**
  * (package private) specialization corresponding to {@link Specialization#VALUE}
@@ -61,8 +63,8 @@ public boolean isBookmarkMemoized() {
     }
 
     @Override
-    public Optional<ObjectMemento> getMemento() {
-        return ObjectMemento.singular(this);
+    public Optional<ObjectMemento> getMemento(final @Nullable ObjectFeature feature) {
+        return ObjectMemento.singular(feature, this);
     }
 
     @Override
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/ManagedObjectViewmodel.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/ManagedObjectViewmodel.java
index cd8c52f889e..b86dbd70413 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/ManagedObjectViewmodel.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/ManagedObjectViewmodel.java
@@ -35,6 +35,7 @@
 import org.apache.causeway.core.metamodel.facets.object.title.TitleRenderRequest;
 import org.apache.causeway.core.metamodel.objectmanager.memento.ObjectMemento;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectFeature;
 
 /**
  * (package private) specialization corresponding to {@link Specialization#VIEWMODEL}
@@ -57,7 +58,7 @@ record ManagedObjectViewmodel(
         this(
             objSpec,
             new TransientObjectRef<>(pojo),
-            new TransientObjectRef<TransactionId>(null),
+            new TransientObjectRef<>(null),
             null);
         bookmarkIfKnown.ifPresent(bookmarkLazy::set);
     }
@@ -77,8 +78,8 @@ record ManagedObjectViewmodel(
     }
 
     @Override
-    public Optional<ObjectMemento> getMemento() {
-        return ObjectMemento.singular(this);
+    public Optional<ObjectMemento> getMemento(final @Nullable ObjectFeature feature) {
+        return ObjectMemento.singular(feature, this);
     }
 
     @Override
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/MmValueUtils.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/MmValueUtils.java
index 67e786980a9..a333f5198bd 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/MmValueUtils.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/MmValueUtils.java
@@ -20,6 +20,7 @@
 
 import java.util.Optional;
 
+import org.jspecify.annotations.NonNull;
 import org.jspecify.annotations.Nullable;
 
 import org.apache.causeway.applib.value.semantics.Renderer;
@@ -31,7 +32,6 @@
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectFeature;
 
-import org.jspecify.annotations.NonNull;
 import lombok.experimental.UtilityClass;
 
 @UtilityClass
@@ -55,25 +55,39 @@ public Optional<ValueSemanticsProvider.Context> createValueSemanticsContext(
 
     // -- RENDERER
 
+    public String titleStringForValueType(
+            final @Nullable ObjectFeature feature,
+            final @Nullable ManagedObject mo) {
+        return render(feature, mo, Renderer::titlePresentation);
+    }
+
     public String htmlStringForValueType(
             final @Nullable ObjectFeature feature,
-            final @Nullable ManagedObject adapter) {
+            final @Nullable ManagedObject mo) {
+        return render(feature, mo, Renderer::htmlPresentation);
+    }
+
+    private interface RenderingFun {
+        String render(Renderer<Object> renderer, ValueSemanticsProvider.Context context, Object pojo);
+    }
 
-        if(!ManagedObjects.isSpecified(adapter)) return "";
+    private String render(
+            final @Nullable ObjectFeature feature,
+            final @Nullable ManagedObject mo,
+            final RenderingFun renderingFun) {
+        if(!ManagedObjects.isSpecified(mo)) return "";
 
-        var spec = adapter.objSpec();
+        var spec = mo.objSpec();
         var valueFacet = spec.valueFacet().orElse(null);
-        if(valueFacet==null) {
-            return String.format("missing ValueFacet %s", spec.getCorrespondingClass());
-        }
+        if(valueFacet==null)
+            return "missing ValueFacet %s".formatted(spec.getCorrespondingClass());
 
         @SuppressWarnings("unchecked")
         var renderer = (Renderer<Object>) valueFacet.selectRendererForFeature(feature).orElse(null);
-        if(renderer==null) {
-            return String.format("missing Renderer %s", spec.getCorrespondingClass());
-        }
+        if(renderer==null)
+            return "missing Renderer %s".formatted(spec.getCorrespondingClass());
 
-        return renderer.htmlPresentation(valueFacet.createValueSemanticsContext(feature), adapter.getPojo());
+        return renderingFun.render(renderer, valueFacet.createValueSemanticsContext(feature), mo.getPojo());
     }
 
     // -- TEMPORAL SUPPORT
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/PackedManagedObject.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/PackedManagedObject.java
index ca167ce767b..9dcb3d9d433 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/PackedManagedObject.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/PackedManagedObject.java
@@ -29,6 +29,7 @@
 import org.apache.causeway.commons.internal.assertions._Assert;
 import org.apache.causeway.core.metamodel.objectmanager.memento.ObjectMemento;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectFeature;
 
 /**
  * 'Collection' of {@link ManagedObject}s.
@@ -54,8 +55,8 @@ public PackedManagedObject(
     @Override
     public String getTitle() {
         return nonScalar.stream()
-                    .map(ManagedObject::getTitle)
-                    .collect(Collectors.joining(","));
+            .map(ManagedObject::getTitle)
+            .collect(Collectors.joining(","));
     }
 
     @Override
@@ -67,9 +68,9 @@ public Object getPojo() {
     }
 
     @Override
-    public Optional<ObjectMemento> getMemento() {
+    public Optional<ObjectMemento> getMemento(final @Nullable ObjectFeature feature) {
         var listOfMementos = nonScalar.stream()
-            .map(scalar->scalar.getMementoElseFail())
+            .map(mo->mo.getMementoElseFail(feature))
             .collect(Collectors.toCollection(ArrayList::new)); // ArrayList is serializable
         var memento = ObjectMemento.packed(
             logicalType(),
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/objectmanager/ObjectManager.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/objectmanager/ObjectManager.java
index 5fd4cadb48a..a81385486f5 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/objectmanager/ObjectManager.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/objectmanager/ObjectManager.java
@@ -23,6 +23,7 @@
 
 import jakarta.inject.Named;
 
+import org.jspecify.annotations.NonNull;
 import org.jspecify.annotations.Nullable;
 
 import org.apache.causeway.applib.annotation.Programmatic;
@@ -45,10 +46,9 @@
 import org.apache.causeway.core.metamodel.objectmanager.memento.ObjectMemento;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectActionParameter;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectFeature;
 import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
 
-import org.jspecify.annotations.NonNull;
-
 /**
  * Bundles all domain object state related responsibilities:<br>
  * - object creation ... init defaults <br>
@@ -112,9 +112,8 @@ public ManagedObject loadObject(final ProtoObject objectLoadRequest) {
      * Supports alias lookup.
      */
     public Optional<ManagedObject> loadObject(final @Nullable Bookmark bookmark) {
-        if(bookmark==null) {
+        if(bookmark==null)
             return Optional.empty();
-        }
         var specLoader = getMetaModelContext().getSpecificationLoader();
         return ProtoObject.resolve(specLoader, bookmark)
                 .map(this::loadObject);
@@ -203,40 +202,37 @@ public ManagedObject adapt(
             final @NonNull Supplier<ObjectSpecification> fallbackElementType) {
         if(pojo==null) {
             ObjectSpecification objectSpecification = fallbackElementType.get();
-            if (objectSpecification.isSingular()) {
+            if (objectSpecification.isSingular())
                 return ManagedObject.empty(objectSpecification);
-            }
             // best we can do?
             return ManagedObject.unspecified();
         }
-        if(pojo instanceof ManagedObject) {
+        if(pojo instanceof ManagedObject)
             // yet ignoring any bookmarking policy, assuming this is not required here
             return (ManagedObject) pojo;
-        }
         // could be any pojo, even of a type, that is vetoed for introspection (spec==null)
         var spec = specForType(pojo.getClass()).orElse(null);
-        if(spec==null) {
+        if(spec==null)
             // best we can do?
             return ManagedObject.unspecified();
-        }
         return spec.isSingular()
-                ? ManagedObject.adaptSingular(spec, pojo)
-                : ManagedObject.packed(
-                        spec.explicitElementSpec().orElseGet(fallbackElementType),
-                        _NullSafe.streamAutodetect(pojo)
-                        .map(element->adapt(element))
-                        .collect(Can.toCan()));
+            ? ManagedObject.adaptSingular(spec, pojo)
+            : ManagedObject.packed(
+                    spec.explicitElementSpec().orElseGet(fallbackElementType),
+                    _NullSafe.streamAutodetect(pojo)
+                    .map(this::adapt)
+                    .collect(Can.toCan()));
     }
 
     // -- OBJECT MEMENTOS
 
-    public Optional<ObjectMemento> mementify(final @Nullable ManagedObject object) {
+    public Optional<ObjectMemento> mementify(final @Nullable ObjectFeature feature, final @Nullable ManagedObject object) {
         return Optional.ofNullable(object)
-        .flatMap(ManagedObject::getMemento);
+            .flatMap(mo->mo.getMemento(feature));
     }
 
-    public ObjectMemento mementifyElseFail(final @NonNull ManagedObject object) {
-        return object.getMemento()
+    public ObjectMemento mementifyElseFail(final @Nullable ObjectFeature feature, final @NonNull ManagedObject object) {
+        return object.getMemento(feature)
                 .orElseThrow(()->
                     _Exceptions.unrecoverable("failed to create memento for  %s", object.objSpec()));
     }
@@ -246,11 +242,10 @@ public ManagedObject demementify(final @Nullable ObjectMemento memento) {
         var spec = mmc.getSpecificationLoader()
                         .specForLogicalType(memento.logicalType())
                 .orElse(null);
-        if(spec==null) {
+        if(spec==null)
             return memento.isEmpty()
-                    ? ManagedObject.unspecified()
-                    : null;
-        }
+                ? ManagedObject.unspecified()
+                : null;
         return objectDementifier().handleElseFail(new MementoRecreateRequest(spec, memento));
     }
 
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/objectmanager/memento/ObjectMemento.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/objectmanager/memento/ObjectMemento.java
index 7ac1bcba8a3..31e0f07e7f9 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/objectmanager/memento/ObjectMemento.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/objectmanager/memento/ObjectMemento.java
@@ -40,6 +40,7 @@
 import org.apache.causeway.core.metamodel.object.MmHintUtils;
 import org.apache.causeway.core.metamodel.object.MmTitleUtils;
 import org.apache.causeway.core.metamodel.object.MmValueUtils;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectFeature;
 
 /**
  * @since 2.0
@@ -67,6 +68,7 @@ static ObjectMemento empty(final LogicalType logicalType) {
     }
 
     static Optional<ObjectMemento> singular(
+            final @Nullable ObjectFeature feature,
             final @Nullable ManagedObject mo) {
         if(ManagedObjects.isNullOrUnspecifiedOrEmpty(mo))
             return Optional.empty();
@@ -83,16 +85,18 @@ static Optional<ObjectMemento> singular(
                         + "nor has 'encodable' semantics, nor is (Serializable || Externalizable)"
                         .formatted(spec));
 
+        //FIXME honor qualified value semantics
         var title = mo.getTranslationService().translate(TranslationContext.empty(), MmTitleUtils.titleOf(mo));
+        System.err.println("title: %s".formatted(title));
 
         var prerenderedHtml = spec.isValue()
             ? "<span>%s</span>".formatted(
-                    MmValueUtils.htmlStringForValueType(null, mo)) // currently only the default value semantics is supported
+                    MmValueUtils.htmlStringForValueType(feature, mo))
             : "<span>%s %s</span>".formatted(
                     mo.getObjectRenderService().iconToHtml(mo.getIcon(IconSize.SMALL), IconSize.SMALL),
                     title);
 
-        return Optional.ofNullable(new ObjectMementoSingular(
+        return Optional.of(new ObjectMementoSingular(
                 mo.logicalType(),
                 MmHintUtils.bookmarkElseFail(mo),
                 title,
@@ -102,11 +106,12 @@ static Optional<ObjectMemento> singular(
      * returns null for null
      */
     @Nullable static ObjectMemento singularOrEmpty(
-            final @Nullable ManagedObject adapter) {
-        MmAssertionUtils.assertPojoIsScalar(adapter);
-        return singular(adapter)
-            .orElseGet(()->ManagedObjects.isSpecified(adapter)
-                    ? ObjectMemento.empty(adapter.logicalType())
+            final @Nullable ObjectFeature feature,
+            final @Nullable ManagedObject mo) {
+        MmAssertionUtils.assertPojoIsScalar(mo);
+        return singular(feature, mo)
+            .orElseGet(()->ManagedObjects.isSpecified(mo)
+                    ? ObjectMemento.empty(mo.logicalType())
                     : null);
     }
     static ObjectMemento packed(
diff --git a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/MultiChoiceModel.java b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/MultiChoiceModel.java
index d6109f98494..c420eadd540 100644
--- a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/MultiChoiceModel.java
+++ b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/MultiChoiceModel.java
@@ -54,8 +54,8 @@ public ArrayList<ObjectMemento> getObject() {
         log.debug("getObject() as unpackedValue {}", unpackedValues);
 
         var mementos = unpackedValues.stream()
-        .map(ManagedObject::getMementoElseFail)
-        .collect(Collectors.toCollection(()->new ArrayList<ObjectMemento>()));
+            .map(this::objectMemento)
+            .collect(Collectors.toCollection(ArrayList::new));
 
         log.debug("getObject() as unpackedMemento {}", mementos);
         return mementos;
@@ -74,4 +74,10 @@ public void detach() {
         attributeModel.detach();
     }
 
+    // -- HELPER
+
+    private ObjectMemento objectMemento(final ManagedObject mo) {
+        return mo.getMementoElseFail(attributeModel().getMetaModel());
+    }
+
 }
diff --git a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/SingleChoiceModel.java b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/SingleChoiceModel.java
index 38387d0aceb..03917055cfd 100644
--- a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/SingleChoiceModel.java
+++ b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/SingleChoiceModel.java
@@ -46,7 +46,7 @@ public ObjectMemento getObject() {
                 propertyModel.getUiProperty().detach();
             }
         }
-        return pendingValue().getValue().getValue().getMemento().orElseThrow();
+        return pendingValue().getValue().getValue().getMemento(attributeModel.getMetaModel()).orElseThrow();
     }
 
     @Override
diff --git a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/ValueModel.java b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/ValueModel.java
index 5bfd8b99f6d..bb95d11740d 100644
--- a/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/ValueModel.java
+++ b/viewers/wicket/model/src/main/java/org/apache/causeway/viewer/wicket/model/models/ValueModel.java
@@ -19,6 +19,7 @@
 package org.apache.causeway.viewer.wicket.model.models;
 
 import java.io.Serializable;
+
 import org.apache.wicket.model.IModel;
 import org.jspecify.annotations.NonNull;
 
@@ -51,7 +52,7 @@ public ValueModel(
             final @NonNull ObjectMember objectMember,
             final @NonNull ManagedObject managedObject) {
         this(objectMember,
-            managedObject.getMemento().orElseThrow(),
+            managedObject.getMemento(objectMember).orElseThrow(),
             new TransientObjectRef<>(managedObject));
     }
 
diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/select2/ChoiceProvider.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/select2/ChoiceProvider.java
index 4b57528c2a6..66b6a3c5303 100644
--- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/select2/ChoiceProvider.java
+++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/select2/ChoiceProvider.java
@@ -76,8 +76,8 @@ public void query(
      */
     public Collection<ObjectMemento> toChoices(final Collection<String> ids) {
         return _NullSafe.stream(ids)
-                .map(this::mementoFromId)
-                .collect(Collectors.toList());
+            .map(this::mementoFromId)
+            .collect(Collectors.toList());
     }
 
     // -- UTIL
@@ -85,7 +85,7 @@ public Collection<ObjectMemento> toChoices(final Collection<String> ids) {
     /** adapter method */
     org.wicketstuff.select2.ChoiceProvider<ObjectMemento> toSelect2ChoiceProvider() {
         var delegate = this;
-        return new org.wicketstuff.select2.ChoiceProvider<ObjectMemento>() {
+        return new org.wicketstuff.select2.ChoiceProvider<>() {
             private static final long serialVersionUID = 1L;
 
             @Override public Collection<ObjectMemento> toChoices(final Collection<String> ids) {
@@ -105,16 +105,20 @@ org.wicketstuff.select2.ChoiceProvider<ObjectMemento> toSelect2ChoiceProvider()
 
     // -- HELPER
 
+    private ObjectMemento objectMemento(final ManagedObject mo) {
+        return mo.getMementoElseFail(attributeModel().getMetaModel());
+    }
+
     private Can<ObjectMemento> queryAll() {
         return attributeModel().getChoices() // must not return detached entities
-                .map(ManagedObject::getMementoElseFail);
+                .map(this::objectMemento);
     }
 
     private Can<ObjectMemento> queryWithAutoCompleteUsingObjectSpecification(final String term) {
         var autoCompleteAdapters = Facets
                 .autoCompleteExecute(attributeModel().getElementType(), term);
         return autoCompleteAdapters
-                .map(ManagedObject::getMementoElseFail);
+                .map(this::objectMemento);
     }
 
     private Can<ObjectMemento> queryWithAutoComplete(final String term) {
@@ -123,7 +127,7 @@ private Can<ObjectMemento> queryWithAutoComplete(final String term) {
                 ? ((UiParameter)attributeModel).getParameterNegotiationModel().getParamValues()
                 : Can.<ManagedObject>empty();
         var pendingArgMementos = pendingArgs
-                .map(ManagedObject::getMementoElseFail);
+                .map(this::objectMemento);
 
         if(attributeModel.isParameter()) {
             // recover any pendingArgs
@@ -136,8 +140,8 @@ private Can<ObjectMemento> queryWithAutoComplete(final String term) {
         }
 
         return attributeModel
-                .getAutoComplete(term)
-                .map(ManagedObject::getMementoElseFail);
+            .getAutoComplete(term)
+            .map(this::objectMemento);
     }
 
     private Can<ManagedObject> reconstructPendingArgs(