Let a label show a Controller method and follow other values in the View.
The method does not write the Model, and a change to the label itself does not run the method again. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package de.neitzel.fx.view;
|
package de.neitzel.fx.view;
|
||||||
|
|
||||||
import javafx.beans.DefaultProperty;
|
import javafx.beans.DefaultProperty;
|
||||||
|
import javafx.scene.Parent;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -9,13 +10,13 @@ import java.util.Map;
|
|||||||
/**
|
/**
|
||||||
* The list of binding elements declared inside {@code fx:define}.
|
* The list of binding elements declared inside {@code fx:define}.
|
||||||
* <p>
|
* <p>
|
||||||
* The list is not a scene-graph child. FXML adds each {@link DirectBinding} child to
|
* The list is not a scene-graph child. FXML adds each {@link DirectBinding} or
|
||||||
* {@link #getElements()}.
|
* {@link ComputedBinding} child to {@link #getElements()}.
|
||||||
*/
|
*/
|
||||||
@DefaultProperty("elements")
|
@DefaultProperty("elements")
|
||||||
public class BindingList {
|
public class BindingList {
|
||||||
|
|
||||||
private final List<DirectBinding> elements = new ArrayList<>();
|
private final List<Object> elements = new ArrayList<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a list and registers it with the load in progress.
|
* Creates a list and registers it with the load in progress.
|
||||||
@@ -25,25 +26,34 @@ public class BindingList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the Direct bindings in this list.
|
* Returns the binding elements in this list.
|
||||||
* <p>
|
* <p>
|
||||||
* FXML appends child elements to the returned list.
|
* FXML appends child elements to the returned list. A child is a {@link DirectBinding} or a
|
||||||
|
* {@link ComputedBinding}.
|
||||||
*
|
*
|
||||||
* @return the mutable list of Direct bindings
|
* @return the mutable list of binding elements
|
||||||
*/
|
*/
|
||||||
public List<DirectBinding> getElements() {
|
public List<Object> getElements() {
|
||||||
return elements;
|
return elements;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies every Direct binding in this list.
|
* Applies every binding element in this list.
|
||||||
*
|
*
|
||||||
* @param namespace objects addressed by {@code fx:id}
|
* @param namespace objects addressed by {@code fx:id}
|
||||||
* @param viewModel the ViewModel for this View
|
* @param viewModel the ViewModel for this View
|
||||||
|
* @param controller the Controller for this View, or {@code null} when the document names none
|
||||||
|
* @param root the scene-graph root of this View
|
||||||
*/
|
*/
|
||||||
void apply(Map<String, Object> namespace, ViewModel viewModel) {
|
void apply(Map<String, Object> namespace, ViewModel viewModel, Object controller, Parent root) {
|
||||||
for (DirectBinding binding : elements) {
|
for (Object element : elements) {
|
||||||
DirectBindingApplier.apply(namespace, viewModel, binding);
|
if (element instanceof DirectBinding directBinding) {
|
||||||
|
DirectBindingApplier.apply(namespace, viewModel, directBinding);
|
||||||
|
} else if (element instanceof ComputedBinding computedBinding) {
|
||||||
|
ComputedBindingApplier.apply(namespace, viewModel, controller, root, computedBinding);
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Not a binding element: " + element);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package de.neitzel.fx.view;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Computed binding declared in FXML.
|
||||||
|
* <p>
|
||||||
|
* The element names the control by {@code fx:id}, the control property, and a no-argument
|
||||||
|
* method on the Controller. The method value uses a {@code #method} prefix, such as
|
||||||
|
* {@code #workingAge}. The method may read the Controller's fields. Its result is written to
|
||||||
|
* the control property. The method does not write the Model.
|
||||||
|
*/
|
||||||
|
public class ComputedBinding {
|
||||||
|
|
||||||
|
private String control;
|
||||||
|
private String property;
|
||||||
|
private String method;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@code fx:id} of the control.
|
||||||
|
*
|
||||||
|
* @param control the control id
|
||||||
|
*/
|
||||||
|
public void setControl(String control) {
|
||||||
|
this.control = control;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the control property name, such as {@code text}.
|
||||||
|
*
|
||||||
|
* @param property the property name
|
||||||
|
*/
|
||||||
|
public void setProperty(String property) {
|
||||||
|
this.property = property;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the Controller method. A {@code #method} value names a no-argument method.
|
||||||
|
*
|
||||||
|
* @param method the method value, such as {@code #workingAge}
|
||||||
|
*/
|
||||||
|
public void setMethod(String method) {
|
||||||
|
this.method = method;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@code fx:id} of the control.
|
||||||
|
*
|
||||||
|
* @return the control id
|
||||||
|
*/
|
||||||
|
public String getControl() {
|
||||||
|
return control;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the control property name.
|
||||||
|
*
|
||||||
|
* @return the property name
|
||||||
|
*/
|
||||||
|
public String getProperty() {
|
||||||
|
return property;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Controller method value.
|
||||||
|
*
|
||||||
|
* @return the method value, such as {@code #workingAge}
|
||||||
|
*/
|
||||||
|
public String getMethod() {
|
||||||
|
return method;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
package de.neitzel.fx.view;
|
||||||
|
|
||||||
|
import javafx.beans.property.Property;
|
||||||
|
import javafx.beans.value.ChangeListener;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.Parent;
|
||||||
|
import javafx.scene.control.Control;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a Controller method result into one control property and keeps it current.
|
||||||
|
*/
|
||||||
|
final class ComputedBindingApplier {
|
||||||
|
|
||||||
|
private ComputedBindingApplier() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies one Computed binding.
|
||||||
|
* <p>
|
||||||
|
* Calls the Controller method and writes the result to the control property. The method
|
||||||
|
* runs again when a JavaFX property changes on a control in this View or on this View's
|
||||||
|
* ViewModel, other than the target property. Writing the target does not call the method
|
||||||
|
* again, so the write does not loop. The Model is not written.
|
||||||
|
*
|
||||||
|
* @param namespace objects addressed by {@code fx:id}
|
||||||
|
* @param viewModel the ViewModel for this View
|
||||||
|
* @param controller the Controller for this View
|
||||||
|
* @param root the scene-graph root of this View
|
||||||
|
* @param binding the declaration
|
||||||
|
*/
|
||||||
|
static void apply(
|
||||||
|
Map<String, Object> namespace,
|
||||||
|
ViewModel viewModel,
|
||||||
|
Object controller,
|
||||||
|
Parent root,
|
||||||
|
ComputedBinding binding) {
|
||||||
|
Object control = namespace.get(binding.getControl());
|
||||||
|
if (control == null) {
|
||||||
|
throw new IllegalArgumentException("No control \"" + binding.getControl() + "\"");
|
||||||
|
}
|
||||||
|
Property<Object> target = controlProperty(control, binding.getProperty());
|
||||||
|
Runnable refresh = refresh(controller, binding.getMethod(), target);
|
||||||
|
refresh.run();
|
||||||
|
watchControls(root, target, refresh);
|
||||||
|
viewModel.addAttributeListener((observable, oldValue, newValue) -> refresh.run());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Runnable refresh(Object controller, String methodValue, Property<Object> target) {
|
||||||
|
boolean[] running = {false};
|
||||||
|
return () -> {
|
||||||
|
if (running[0]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
running[0] = true;
|
||||||
|
try {
|
||||||
|
target.setValue(invoke(controller, methodValue));
|
||||||
|
} finally {
|
||||||
|
running[0] = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void watchControls(Parent root, Property<?> target, Runnable refresh) {
|
||||||
|
watchNode(root, target, refresh);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void watchNode(Node node, Property<?> target, Runnable refresh) {
|
||||||
|
if (node instanceof Control control) {
|
||||||
|
watchProperties(control, target, refresh);
|
||||||
|
}
|
||||||
|
if (node instanceof Parent parent) {
|
||||||
|
for (Node child : parent.getChildrenUnmodifiable()) {
|
||||||
|
watchNode(child, target, refresh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static void watchProperties(Object bean, Property<?> target, Runnable refresh) {
|
||||||
|
ChangeListener<Object> listener = (observable, oldValue, newValue) -> refresh.run();
|
||||||
|
for (Method method : bean.getClass().getMethods()) {
|
||||||
|
if (method.getParameterCount() != 0 || !method.getName().endsWith("Property")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!ObservableValue.class.isAssignableFrom(method.getReturnType())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Object value = readProperty(bean, method);
|
||||||
|
if (value instanceof ObservableValue<?> observable && observable != target) {
|
||||||
|
((ObservableValue<Object>) observable).addListener(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object readProperty(Object bean, Method method) {
|
||||||
|
try {
|
||||||
|
return method.invoke(bean);
|
||||||
|
} catch (IllegalAccessException | InvocationTargetException failure) {
|
||||||
|
throw new IllegalStateException("Could not read property " + method.getName(), failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static Property<Object> controlProperty(Object control, String propertyName) {
|
||||||
|
String methodName = propertyName + "Property";
|
||||||
|
try {
|
||||||
|
Method method = control.getClass().getMethod(methodName);
|
||||||
|
Object value = method.invoke(control);
|
||||||
|
if (!(value instanceof Property<?> property)) {
|
||||||
|
throw new IllegalArgumentException(methodName + " did not return a property");
|
||||||
|
}
|
||||||
|
return (Property<Object>) property;
|
||||||
|
} catch (NoSuchMethodException failure) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"No property " + propertyName + " on " + control.getClass().getName(), failure);
|
||||||
|
} catch (IllegalAccessException | InvocationTargetException failure) {
|
||||||
|
throw new IllegalStateException("Could not read property " + propertyName, failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object invoke(Object controller, String methodValue) {
|
||||||
|
if (controller == null) {
|
||||||
|
throw new IllegalArgumentException("No controller for " + methodValue);
|
||||||
|
}
|
||||||
|
String name = methodName(methodValue);
|
||||||
|
try {
|
||||||
|
Method method = controller.getClass().getMethod(name);
|
||||||
|
return method.invoke(controller);
|
||||||
|
} catch (NoSuchMethodException failure) {
|
||||||
|
throw new IllegalArgumentException("No computed method \"" + methodValue + "\"", failure);
|
||||||
|
} catch (IllegalAccessException | InvocationTargetException failure) {
|
||||||
|
throw new IllegalStateException("Could not call " + methodValue, failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String methodName(String methodValue) {
|
||||||
|
if (methodValue != null && methodValue.startsWith("#")) {
|
||||||
|
return methodValue.substring(1);
|
||||||
|
}
|
||||||
|
return methodValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,8 +18,9 @@ public final class ViewLoader {
|
|||||||
/**
|
/**
|
||||||
* Loads a View for one Model instance.
|
* Loads a View for one Model instance.
|
||||||
* <p>
|
* <p>
|
||||||
* Builds a ViewModel for {@code model}, loads {@code fxml}, and applies Direct bindings
|
* Builds a ViewModel for {@code model}, loads {@code fxml}, and applies binding elements
|
||||||
* declared in {@code fx:define}. Direction {@code both} keeps the control and the Attribute
|
* declared in {@code fx:define}. A Computed binding writes a Controller method result to a
|
||||||
|
* control and does not write the Model. Direction {@code both} keeps the control and the Attribute
|
||||||
* aligned, so editing the control writes the Attribute immediately. Direction {@code view}
|
* aligned, so editing the control writes the Attribute immediately. Direction {@code view}
|
||||||
* shows the Attribute and follows later property changes without writing the Attribute back.
|
* shows the Attribute and follows later property changes without writing the Attribute back.
|
||||||
* Direction {@code model} shows the current value, writes the Attribute when the control
|
* Direction {@code model} shows the current value, writes the Attribute when the control
|
||||||
@@ -43,7 +44,7 @@ public final class ViewLoader {
|
|||||||
throw new IllegalArgumentException("View root must be a parent");
|
throw new IllegalArgumentException("View root must be a parent");
|
||||||
}
|
}
|
||||||
for (BindingList bindingList : session.bindingLists()) {
|
for (BindingList bindingList : session.bindingLists()) {
|
||||||
bindingList.apply(loader.getNamespace(), viewModel);
|
bindingList.apply(loader.getNamespace(), viewModel, loader.getController(), root);
|
||||||
}
|
}
|
||||||
return new LoadedView(root, viewModel);
|
return new LoadedView(root, viewModel);
|
||||||
} catch (IOException failure) {
|
} catch (IOException failure) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import javafx.beans.property.SimpleIntegerProperty;
|
|||||||
import javafx.beans.property.SimpleLongProperty;
|
import javafx.beans.property.SimpleLongProperty;
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
|
import javafx.beans.value.ChangeListener;
|
||||||
|
|
||||||
import java.beans.BeanInfo;
|
import java.beans.BeanInfo;
|
||||||
import java.beans.IntrospectionException;
|
import java.beans.IntrospectionException;
|
||||||
@@ -70,6 +71,18 @@ public class ViewModel {
|
|||||||
return properties.get(name);
|
return properties.get(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls {@code listener} when any Attribute property changes.
|
||||||
|
*
|
||||||
|
* @param listener the listener
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
void addAttributeListener(ChangeListener<Object> listener) {
|
||||||
|
for (Property<?> property : properties.values()) {
|
||||||
|
((Property<Object>) property).addListener(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static List<PropertyDescriptor> descriptors(Class<?> type) {
|
private static List<PropertyDescriptor> descriptors(Class<?> type) {
|
||||||
try {
|
try {
|
||||||
BeanInfo info = Introspector.getBeanInfo(type, Object.class);
|
BeanInfo info = Introspector.getBeanInfo(type, Object.class);
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
package de.neitzel.fx.view;
|
||||||
|
|
||||||
|
import de.neitzel.fx.view.fixture.Person;
|
||||||
|
import de.neitzel.injection.ApplicationContext;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.beans.property.Property;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.fail;
|
||||||
|
|
||||||
|
class ComputeAValueTest {
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void startJavaFx() throws InterruptedException {
|
||||||
|
CountDownLatch started = new CountDownLatch(1);
|
||||||
|
try {
|
||||||
|
Platform.startup(started::countDown);
|
||||||
|
} catch (IllegalStateException alreadyStarted) {
|
||||||
|
started.countDown();
|
||||||
|
}
|
||||||
|
if (!started.await(10, TimeUnit.SECONDS)) {
|
||||||
|
fail("JavaFX toolkit did not start");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void computedBindingShowsTheMethodResultAndLeavesTheModelUnchanged() throws Exception {
|
||||||
|
onFx(() -> {
|
||||||
|
Person person = new Person("Ada");
|
||||||
|
person.setAge(36);
|
||||||
|
URL fxml = ComputeAValueTest.class.getResource("working-age.fxml");
|
||||||
|
assertNotNull(fxml);
|
||||||
|
ApplicationContext context = new ApplicationContext("de.neitzel.fx.view.computed");
|
||||||
|
LoadedView loaded = ViewLoader.load(fxml, person, context);
|
||||||
|
Label workingAgeLabel = (Label) loaded.getRoot().lookup("#workingAgeLabel");
|
||||||
|
assertEquals("initial-age", workingAgeLabel.getText());
|
||||||
|
assertEquals("Ada", person.getName());
|
||||||
|
assertEquals(36, person.getAge());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void typingInAnotherTextFieldUpdatesTheComputedLabel() throws Exception {
|
||||||
|
onFx(() -> {
|
||||||
|
Person person = new Person("Ada");
|
||||||
|
URL fxml = ComputeAValueTest.class.getResource("working-age.fxml");
|
||||||
|
assertNotNull(fxml);
|
||||||
|
ApplicationContext context = new ApplicationContext("de.neitzel.fx.view.computed");
|
||||||
|
LoadedView loaded = ViewLoader.load(fxml, person, context);
|
||||||
|
TextField nameField = (TextField) loaded.getRoot().lookup("#nameField");
|
||||||
|
Label workingAgeLabel = (Label) loaded.getRoot().lookup("#workingAgeLabel");
|
||||||
|
nameField.setText("Grace");
|
||||||
|
assertEquals("typed-age", workingAgeLabel.getText());
|
||||||
|
assertEquals("Grace", person.getName());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void settingAViewModelPropertyUpdatesTheComputedLabel() throws Exception {
|
||||||
|
onFx(() -> {
|
||||||
|
Person person = new Person("Ada");
|
||||||
|
URL fxml = ComputeAValueTest.class.getResource("working-age.fxml");
|
||||||
|
assertNotNull(fxml);
|
||||||
|
ApplicationContext context = new ApplicationContext("de.neitzel.fx.view.computed");
|
||||||
|
LoadedView loaded = ViewLoader.load(fxml, person, context);
|
||||||
|
Label workingAgeLabel = (Label) loaded.getRoot().lookup("#workingAgeLabel");
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Property<String> name = (Property<String>) loaded.property("name");
|
||||||
|
name.setValue("Edsger");
|
||||||
|
assertEquals("model-age", workingAgeLabel.getText());
|
||||||
|
assertEquals("Edsger", person.getName());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void changingTheComputedLabelDoesNotRunTheMethodAgain() throws Exception {
|
||||||
|
onFx(() -> {
|
||||||
|
Person person = new Person("Ada");
|
||||||
|
URL fxml = ComputeAValueTest.class.getResource("working-age.fxml");
|
||||||
|
assertNotNull(fxml);
|
||||||
|
ApplicationContext context = new ApplicationContext("de.neitzel.fx.view.computed");
|
||||||
|
LoadedView loaded = ViewLoader.load(fxml, person, context);
|
||||||
|
Label workingAgeLabel = (Label) loaded.getRoot().lookup("#workingAgeLabel");
|
||||||
|
assertEquals("initial-age", workingAgeLabel.getText());
|
||||||
|
workingAgeLabel.setText("sentinel");
|
||||||
|
assertEquals("sentinel", workingAgeLabel.getText());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void onFx(FxAction action) throws Exception {
|
||||||
|
if (Platform.isFxApplicationThread()) {
|
||||||
|
action.run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AtomicReference<Throwable> thrown = new AtomicReference<>();
|
||||||
|
CountDownLatch done = new CountDownLatch(1);
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
try {
|
||||||
|
action.run();
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
thrown.set(failure);
|
||||||
|
} finally {
|
||||||
|
done.countDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!done.await(10, TimeUnit.SECONDS)) {
|
||||||
|
fail("JavaFX thread did not finish");
|
||||||
|
}
|
||||||
|
Throwable failure = thrown.get();
|
||||||
|
if (failure instanceof Exception exception) {
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
if (failure instanceof Error error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
if (failure != null) {
|
||||||
|
throw new AssertionError(failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
private interface FxAction {
|
||||||
|
void run() throws Exception;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package de.neitzel.fx.view.computed;
|
||||||
|
|
||||||
|
import jakarta.inject.Singleton;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller that derives a label from the name field and does not write the Model.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
public class WorkingAgeController {
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private TextField nameField;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a literal chosen from the current name field.
|
||||||
|
*
|
||||||
|
* @return the value written to the label
|
||||||
|
*/
|
||||||
|
public String workingAge() {
|
||||||
|
if ("Ada".equals(nameField.getText())) {
|
||||||
|
return "initial-age";
|
||||||
|
}
|
||||||
|
if ("Grace".equals(nameField.getText())) {
|
||||||
|
return "typed-age";
|
||||||
|
}
|
||||||
|
return "model-age";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.scene.control.Label?>
|
||||||
|
<?import javafx.scene.control.TextField?>
|
||||||
|
<?import javafx.scene.layout.VBox?>
|
||||||
|
<?import de.neitzel.fx.view.BindingList?>
|
||||||
|
<?import de.neitzel.fx.view.ComputedBinding?>
|
||||||
|
<?import de.neitzel.fx.view.DirectBinding?>
|
||||||
|
|
||||||
|
<VBox xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"
|
||||||
|
fx:controller="de.neitzel.fx.view.computed.WorkingAgeController">
|
||||||
|
<fx:define>
|
||||||
|
<BindingList>
|
||||||
|
<DirectBinding control="nameField" property="text" path="name" direction="both"/>
|
||||||
|
<ComputedBinding control="workingAgeLabel" property="text" method="#workingAge"/>
|
||||||
|
</BindingList>
|
||||||
|
</fx:define>
|
||||||
|
<TextField fx:id="nameField" id="nameField"/>
|
||||||
|
<Label fx:id="workingAgeLabel" id="workingAgeLabel"/>
|
||||||
|
</VBox>
|
||||||
Reference in New Issue
Block a user