You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by ah...@apache.org on 2020/07/31 08:40:45 UTC

[isis] branch master updated: ISIS-2340: wire up property binding (new PropertyNegotiationModel)

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

ahuber pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/isis.git


The following commit(s) were added to refs/heads/master by this push:
     new 032a9d9  ISIS-2340: wire up property binding (new PropertyNegotiationModel)
032a9d9 is described below

commit 032a9d99369c4421308cf288c1f813b23cd7f135
Author: Andi Huber <ah...@apache.org>
AuthorDate: Fri Jul 31 10:40:28 2020 +0200

    ISIS-2340: wire up property binding (new PropertyNegotiationModel)
---
 .../interactions/managed/ManagedProperty.java      |  42 +++---
 .../managed/PropertyNegotiationModel.java          | 143 +++++++++++++++++++++
 .../model/interaction/InteractionDemo.java         |  19 ++-
 .../testdomain/interact/PropertyBindingTest.java   |  96 ++++++++++----
 .../testdomain/interact/SimulatedUiComponent.java  |   8 ++
 .../common/model/binding/UiComponentFactory.java   |   2 +-
 6 files changed, 259 insertions(+), 51 deletions(-)

diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/interactions/managed/ManagedProperty.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/interactions/managed/ManagedProperty.java
index e248966..712be8e 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/interactions/managed/ManagedProperty.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/interactions/managed/ManagedProperty.java
@@ -23,9 +23,10 @@ import java.util.Optional;
 import javax.annotation.Nullable;
 
 import org.apache.isis.applib.annotation.Where;
-import org.apache.isis.core.commons.binding.Bindable;
 import org.apache.isis.core.commons.binding.Observable;
 import org.apache.isis.core.commons.collections.Can;
+import org.apache.isis.core.commons.internal.binding._Observables;
+import org.apache.isis.core.commons.internal.binding._Observables.LazyObservable;
 import org.apache.isis.core.metamodel.consent.InteractionInitiatedBy;
 import org.apache.isis.core.metamodel.consent.Veto;
 import org.apache.isis.core.metamodel.spec.ManagedObject;
@@ -36,9 +37,7 @@ import lombok.Getter;
 import lombok.NonNull;
 import lombok.val;
 
-public final class ManagedProperty 
-extends ManagedMember 
-implements ManagedValue {
+public final class ManagedProperty extends ManagedMember {
     
     // -- FACTORIES
     
@@ -68,6 +67,7 @@ implements ManagedValue {
             final @NonNull Where where) {
         super(owner, where);
         this.property = property;
+        observablePropValue = _Observables.forFactory(this::reassessPropertyValue);
     }
 
     @Override
@@ -119,6 +119,7 @@ implements ManagedValue {
 
         ManagedObject managedObject = property.set(getOwner(), proposedNewValue, InteractionInitiatedBy.USER);
         setOwner(managedObject);
+        observablePropValue.invalidate();
         return Optional.empty();
     }
 
@@ -127,7 +128,7 @@ implements ManagedValue {
      * If visibility is vetoed, returns an empty but specified ManagedObject.
      * @return the property value as to be used by the UI for representation
      */
-    public ManagedObject getPropertyValue() {
+    private ManagedObject reassessPropertyValue() {
         val property = getProperty();
         val owner = getOwner();
         
@@ -136,29 +137,24 @@ implements ManagedValue {
             ? property.get(owner, InteractionInitiatedBy.USER)
             : ManagedObject.empty(property.getSpecification());
     }
+    
+    
+    // -- NEGOTIATION
 
-    @Override
-    public Bindable<ManagedObject> getValue() {
-        // TODO Auto-generated method stub
-        return null;
-    }
-
-    @Override
-    public Observable<String> getValidationMessage() {
-        // TODO Auto-generated method stub
-        return null;
+    public PropertyNegotiationModel startNegotiation() {
+        return new PropertyNegotiationModel(this);
     }
 
-    @Override
-    public Bindable<String> getSearchArgument() {
-        // TODO Auto-generated method stub
-        return null;
+    // -- BINDING
+    
+    @NonNull private final LazyObservable<ManagedObject> observablePropValue;
+    
+    public Observable<ManagedObject> getValue() {
+        return observablePropValue;
     }
 
-    @Override
-    public Observable<Can<ManagedObject>> getChoices() {
-        // TODO Auto-generated method stub
-        return null;
+    public ManagedObject getPropertyValue() {
+        return getValue().getValue();
     }
     
     
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/interactions/managed/PropertyNegotiationModel.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/interactions/managed/PropertyNegotiationModel.java
new file mode 100644
index 0000000..72de403
--- /dev/null
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/interactions/managed/PropertyNegotiationModel.java
@@ -0,0 +1,143 @@
+/*
+ *  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.isis.core.metamodel.interactions.managed;
+
+import org.apache.isis.core.commons.binding.Bindable;
+import org.apache.isis.core.commons.binding.Observable;
+import org.apache.isis.core.commons.collections.Can;
+import org.apache.isis.core.commons.internal.binding._BindableAbstract;
+import org.apache.isis.core.commons.internal.binding._Bindables;
+import org.apache.isis.core.commons.internal.binding._Observables;
+import org.apache.isis.core.commons.internal.binding._Observables.LazyObservable;
+import org.apache.isis.core.metamodel.consent.InteractionInitiatedBy;
+import org.apache.isis.core.metamodel.spec.ManagedObject;
+import org.apache.isis.core.metamodel.spec.ManagedObjects;
+import org.apache.isis.core.metamodel.spec.ObjectSpecification;
+
+import lombok.NonNull;
+import lombok.val;
+
+public class PropertyNegotiationModel implements ManagedValue {
+    
+    @NonNull private final _BindableAbstract<ManagedObject> proposedValue;
+    @NonNull private final LazyObservable<String> validation;
+    @NonNull private final _BindableAbstract<String> searchArgument;
+    @NonNull private final LazyObservable<Can<ManagedObject>> choices;
+    
+    @NonNull private final ManagedProperty managedProperty;
+    
+    PropertyNegotiationModel(ManagedProperty managedProperty) {
+        this.managedProperty = managedProperty;
+        val property = managedProperty.getMember();
+        
+        validationFeedbackActive = _Bindables.forValue(false);
+        
+        val currentValue = managedProperty.getPropertyValue();
+        val defaultValue = ManagedObjects.isNullOrUnspecifiedOrEmpty(currentValue)
+            ? property.getDefault(managedProperty.getOwner())
+            : currentValue;
+        
+        proposedValue = _Bindables.forValue(defaultValue);
+        proposedValue.addListener((e,o,n)->{
+            invalidateChoicesAndValidation();
+        });
+        
+        // has either autoComplete, choices, or none
+        choices = property.hasAutoComplete()
+        ? _Observables.forFactory(()->
+            property.getAutoComplete(
+                    managedProperty.getOwner(),
+                    getSearchArgument().getValue(), 
+                    InteractionInitiatedBy.USER))
+        : property.hasChoices() 
+            ? _Observables.forFactory(()->
+                property.getChoices(managedProperty.getOwner(), InteractionInitiatedBy.USER))
+            : _Observables.forFactory(Can::empty);
+
+        // if has autoComplete, then activate the search argument        
+        searchArgument = _Bindables.forValue(null);
+        if(property.hasAutoComplete()) {
+            searchArgument.addListener((e,o,n)->{
+                choices.invalidate();
+            });
+        }
+        
+        // validate this parameter, but only when validationFeedback has been activated
+        validation = _Observables.forFactory(()->
+            isValidationFeedbackActive()
+            ? managedProperty.checkValidity(getValue().getValue())
+                    .map(InteractionVeto::getReason)
+                    .orElse(null)
+            : (String)null); 
+    }
+
+    @Override
+    public ObjectSpecification getSpecification() {
+        return managedProperty.getSpecification();
+    }
+    
+    @Override
+    public Bindable<ManagedObject> getValue() {
+        return proposedValue;
+    }
+
+    @Override
+    public Observable<String> getValidationMessage() {
+        return validation;
+    }
+
+    @Override
+    public Bindable<String> getSearchArgument() {
+        return searchArgument;
+    }
+
+    @Override
+    public Observable<Can<ManagedObject>> getChoices() {
+        return choices;
+    }
+    
+    // -- VALIDATION
+    
+    @NonNull private final _BindableAbstract<Boolean> validationFeedbackActive;
+    
+    /**
+     * Whether validation feedback is activated. Activates once user attempts to 'submit' an action.
+     */
+    @NonNull public Observable<Boolean> getObservableValidationFeedbackActive() {
+        return validationFeedbackActive;
+    }
+    
+    private boolean isValidationFeedbackActive() {
+        return validationFeedbackActive.getValue();
+    }
+    
+    public void invalidateChoicesAndValidation() {
+        choices.invalidate();
+        validation.invalidate();
+    }
+    
+    // -- SUBMISSION
+
+    public void submit() {
+        managedProperty.modifyProperty(getValue().getValue());
+    }
+
+
+
+}
diff --git a/examples/smoketests/stable/src/main/java/org/apache/isis/testdomain/model/interaction/InteractionDemo.java b/examples/smoketests/stable/src/main/java/org/apache/isis/testdomain/model/interaction/InteractionDemo.java
index 20a995d..7793ada 100644
--- a/examples/smoketests/stable/src/main/java/org/apache/isis/testdomain/model/interaction/InteractionDemo.java
+++ b/examples/smoketests/stable/src/main/java/org/apache/isis/testdomain/model/interaction/InteractionDemo.java
@@ -34,6 +34,7 @@ import org.apache.isis.applib.annotation.LabelPosition;
 import org.apache.isis.applib.annotation.Nature;
 import org.apache.isis.applib.annotation.Property;
 import org.apache.isis.applib.annotation.PropertyLayout;
+import org.apache.isis.core.commons.internal.base._Strings;
 import org.apache.isis.extensions.modelannotation.applib.annotation.Model;
 
 import lombok.Getter;
@@ -50,9 +51,11 @@ public class InteractionDemo {
             editingDisabledReason = "Disabled for demonstration.")
     @XmlElement @Getter @Setter private String stringDisabled;
     
+    @Model public List<String> autoCompleteStringDisabled(String search) { return null;}
+    
     @Property
     @PropertyLayout(multiLine=3, labelPosition = LabelPosition.TOP)
-    @XmlElement @Getter @Setter private String stringMultiline;
+    @XmlElement @Getter @Setter private String stringMultiline = "initial";
 
     // verify, all the parameter supporting methods get picked up
     
@@ -60,9 +63,17 @@ public class InteractionDemo {
     @Model public String disableStringMultiline() { return null; }                           
     @Model public String validateStringMultiline(String proposeValue) { return null; }
     @Model public String defaultStringMultiline() { return "default"; }
-    @Model public String[] choicesStringMultiline() { return new String[] {"Hello", "World"}; }          
-    @Model public List<String> autoCompleteStringMultiline(String search) { 
-        return Stream.of(choicesStringMultiline())
+    @Model public String[] choicesStringMultiline() { return new String[] {"Hello", "World"}; }
+    
+    // -- auto search tests
+    
+    @Property
+    @XmlElement @Getter @Setter private String string2 = "initial";
+    
+    @Model public List<String> autoCompleteString2(String search) { 
+        return _Strings.isEmpty(search)
+                ? null
+                : Stream.of(choicesStringMultiline())
                 .filter(s->s.contains(search))
                 .collect(Collectors.toList()); 
     }
diff --git a/examples/smoketests/stable/src/test/java/org/apache/isis/testdomain/interact/PropertyBindingTest.java b/examples/smoketests/stable/src/test/java/org/apache/isis/testdomain/interact/PropertyBindingTest.java
index 0865daf..54d1a7b 100644
--- a/examples/smoketests/stable/src/test/java/org/apache/isis/testdomain/interact/PropertyBindingTest.java
+++ b/examples/smoketests/stable/src/test/java/org/apache/isis/testdomain/interact/PropertyBindingTest.java
@@ -1,13 +1,18 @@
 package org.apache.isis.testdomain.interact;
 
 import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.test.context.TestPropertySource;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import org.apache.isis.applib.annotation.Where;
 import org.apache.isis.core.config.presets.IsisPresets;
+import org.apache.isis.core.metamodel.interactions.managed.PropertyNegotiationModel;
 import org.apache.isis.testdomain.Smoketest;
 import org.apache.isis.testdomain.conf.Configuration_headless;
 import org.apache.isis.testdomain.model.interaction.Configuration_usingInteractionDomain;
@@ -32,50 +37,95 @@ import lombok.val;
 })
 class PropertyBindingTest extends InteractionTestAbstract {
 
+    PropertyNegotiationModel proposalA;
+    PropertyNegotiationModel proposalB;
+    
+    SimulatedUiComponent uiPropStringMultiline;
+    
+    // UI components for the new value negotiation dialog 
     SimulatedUiComponent uiPropA;
+    SimulatedUiChoices uiPropAChoices;
+    SimulatedUiAutoComplete uiPropBAutoComplete;
     
     @BeforeEach
     void setUpSimulatedUi() {
 
-        val propertyInteraction = startPropertyInteractionOn(InteractionDemo.class, "stringMultiline", Where.OBJECT_FORMS);  
-
-        assertTrue(propertyInteraction.getManagedProperty().isPresent(), "prop is expected to be editable");
+        val propertyInteractionA = startPropertyInteractionOn(InteractionDemo.class, "stringMultiline", Where.OBJECT_FORMS);  
+        assertTrue(propertyInteractionA.getManagedProperty().isPresent(), "prop is expected to be editable");
+        
+        val propertyInteractionB = startPropertyInteractionOn(InteractionDemo.class, "string2", Where.OBJECT_FORMS);  
+        assertTrue(propertyInteractionB.getManagedProperty().isPresent(), "prop is expected to be editable");
         
-        val managedProperty = propertyInteraction.getManagedProperty().get();
+        val managedPropA = propertyInteractionA.getManagedProperty().get();
+        this.proposalA = managedPropA.startNegotiation();
+        
+        val managedPropB = propertyInteractionB.getManagedProperty().get();
+        this.proposalB = managedPropB.startNegotiation();
         
         // setting up and binding all the simulated UI components
         
+        uiPropStringMultiline = new SimulatedUiComponent();
         uiPropA = new SimulatedUiComponent();
+        uiPropAChoices = new SimulatedUiChoices();
+        uiPropBAutoComplete = new SimulatedUiAutoComplete();
+        
+        uiPropStringMultiline.bind(managedPropA);
         
-        uiPropA.bind(managedProperty);
+        uiPropA.bind(proposalA);
+        uiPropAChoices.bind(proposalA);
+        uiPropBAutoComplete.bind(proposalB);
         
-        //TODO verify that initial defaults are as expected
+        // verify that initial defaults are as expected
         
-//        assertEquals(1, uiParamA.getValue().getPojo());
+        assertEquals("initial", uiPropStringMultiline.getValue().getPojo());
+        assertEquals("initial", uiPropA.getValue().getPojo());
         
-        //TODO verify that initial choices are as expected
+        // verify that initial choices are as expected
         
-//        assertComponentWiseUnwrappedEquals(NumberRange.POSITITVE.numbers(), uiParamA.getChoices());
-//        assertComponentWiseUnwrappedEquals(NumberRange.NEGATIVE.numbers(), uiParamB.getChoices());
-//        assertEmpty(uiParamC.getChoices()); // empty because the search argument is also empty
+        assertTrue(managedPropA.getMember().hasChoices());
+        assertComponentWiseUnwrappedEquals(new String[] {"Hello", "World"}, uiPropAChoices.getChoices());
         
-        //TODO verify that initial validation messages are all empty, 
+        assertTrue(managedPropB.getMember().hasAutoComplete());
+        assertComponentWiseUnwrappedEquals(new String[] {}, uiPropBAutoComplete.getChoices());
+        
+        // verify that initial validation messages are all empty, 
         // because we don't validate anything until a user initiated submit attempt occurs 
         
-//        assertEmpty(uiParamRangeA.getValidationMessage());
-//        assertEmpty(uiParamRangeB.getValidationMessage());
-//        assertEmpty(uiParamRangeC.getValidationMessage());
-//        
-//        assertEmpty(uiParamA.getValidationMessage());
-//        assertEmpty(uiParamB.getValidationMessage());
-//        assertEmpty(uiParamC.getValidationMessage());
-//        
-//        assertEmpty(uiSubmit.getValidationMessage());
+        assertEmpty(uiPropA.getValidationMessage());
+        
+        // verify that validation feedback is not active
+        
+        assertFalse(proposalA.getObservableValidationFeedbackActive().getValue());
+        assertFalse(proposalB.getObservableValidationFeedbackActive().getValue());
+        
+    }
+    
+    @Test
+    void propA_whenChanging_shouldPropagteNewValue() {
+
+        uiPropA.simulateValueChange("Hi");
+        assertEquals("Hi", proposalA.getValue().getValue().getPojo());
+
+        assertNull(proposalA.getValidationMessage().getValue());
+        proposalA.submit();
+        assertEquals("Hi", uiPropStringMultiline.getValue().getPojo());
+    }
+    
+    @Test
+    void propB_whenSettingSearchArgument_shouldProvideChoices() {
         
-        //TODO verify that validation feedback is not active
+        // verify that changing the search argument fires change event
+        assertDoesIncrement(
+                uiPropBAutoComplete::getChoiceBoxUpdateEventCount,
+                ()->uiPropBAutoComplete.setSimulatedSearchArgument("H")); // select "Hello"
+                        
         
-//        assertFalse(pendingArgs.getObservableValidationFeedbackActive().getValue());
+        // verify that no additional changes are triggered
+        assertDoesNotIncrement(        
+                uiPropBAutoComplete::getChoiceBoxUpdateEventCount,
+                ()->assertComponentWiseUnwrappedEquals(new String[] {"Hello"}, uiPropBAutoComplete.getChoices()));
         
+        // TODO such a change might set or clear propA validation message once validation feedback is active
     }
 
     
diff --git a/examples/smoketests/stable/src/test/java/org/apache/isis/testdomain/interact/SimulatedUiComponent.java b/examples/smoketests/stable/src/test/java/org/apache/isis/testdomain/interact/SimulatedUiComponent.java
index 8eca303..d6085eb 100644
--- a/examples/smoketests/stable/src/test/java/org/apache/isis/testdomain/interact/SimulatedUiComponent.java
+++ b/examples/smoketests/stable/src/test/java/org/apache/isis/testdomain/interact/SimulatedUiComponent.java
@@ -20,6 +20,7 @@ package org.apache.isis.testdomain.interact;
 
 import org.apache.isis.core.commons.binding.Bindable;
 import org.apache.isis.core.commons.internal.binding._Bindables;
+import org.apache.isis.core.metamodel.interactions.managed.ManagedProperty;
 import org.apache.isis.core.metamodel.interactions.managed.ManagedValue;
 import org.apache.isis.core.metamodel.spec.ManagedObject;
 import org.apache.isis.core.metamodel.spec.ObjectSpecification;
@@ -36,6 +37,11 @@ public class SimulatedUiComponent extends HasValueValidation {
         super.bind(managedValue);
     }
 
+    /** just listening, no validation */
+    public void bind(ManagedProperty managedProperty) {
+        value.bind(managedProperty.getValue());
+    }
+    
     public void simulateValueChange(Object newValue) {
         value.setValue(ManagedObject.of(valueSpec, newValue));
     }
@@ -45,4 +51,6 @@ public class SimulatedUiComponent extends HasValueValidation {
     }
 
     
+
+    
 }
diff --git a/viewers/common/src/main/java/org/apache/isis/viewer/common/model/binding/UiComponentFactory.java b/viewers/common/src/main/java/org/apache/isis/viewer/common/model/binding/UiComponentFactory.java
index 0225f57..44e9cfe 100644
--- a/viewers/common/src/main/java/org/apache/isis/viewer/common/model/binding/UiComponentFactory.java
+++ b/viewers/common/src/main/java/org/apache/isis/viewer/common/model/binding/UiComponentFactory.java
@@ -136,7 +136,7 @@ public interface UiComponentFactory<B, C> {
             val managedProperty = (ManagedProperty)managedFeature;
             //TODO do a type check before the cast, so we can throw a more detailed exception
             // that is, given type must be assignable from the actual pojo type 
-            return Optional.ofNullable(managedProperty.getPropertyValue())
+            return Optional.ofNullable(managedProperty.getValue().getValue())
                     .filter(_Predicates.not(ManagedObjects::isNullOrUnspecifiedOrEmpty))
                     .map(ManagedObject::getPojo)
                     .map(type::cast);