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);