Let a Direct binding follow a dotted Path into a nested Model.
One ViewModel per Model instance keeps two Views and a cycle aligned, and a missing nested Model leaves both sides unchanged.
This commit is contained in:
@@ -4,8 +4,10 @@ package de.neitzel.fx.view;
|
|||||||
* A Direct binding declared in FXML.
|
* A Direct binding declared in FXML.
|
||||||
* <p>
|
* <p>
|
||||||
* The element names the control by {@code fx:id}, the control property, the Path, and the
|
* The element names the control by {@code fx:id}, the control property, the Path, and the
|
||||||
* direction. A Path of one name is that Attribute of this View's ViewModel. Direction values
|
* direction. A Path of one name is that Attribute of this View's ViewModel. Further names step
|
||||||
* are {@code view}, {@code model}, and {@code both}.
|
* into the ViewModel of a Complex attribute, and the direction applies to the last name.
|
||||||
|
* When that last name is complex, the control receives the Model instance behind the ViewModel.
|
||||||
|
* Direction values are {@code view}, {@code model}, and {@code both}.
|
||||||
*/
|
*/
|
||||||
public class DirectBinding {
|
public class DirectBinding {
|
||||||
|
|
||||||
|
|||||||
@@ -26,11 +26,20 @@ final class DirectBindingApplier {
|
|||||||
if (control == null) {
|
if (control == null) {
|
||||||
throw new IllegalArgumentException("No control \"" + binding.getControl() + "\"");
|
throw new IllegalArgumentException("No control \"" + binding.getControl() + "\"");
|
||||||
}
|
}
|
||||||
|
Resolved resolved = resolve(viewModel, binding.getPath());
|
||||||
|
if (resolved == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
Property<?> controlProperty = controlProperty(control, binding.getProperty());
|
Property<?> controlProperty = controlProperty(control, binding.getProperty());
|
||||||
Property<?> attribute = viewModel.property(binding.getPath());
|
Property<?> attribute = resolved.attribute;
|
||||||
if (attribute == null) {
|
if (resolved.complex) {
|
||||||
throw new IllegalArgumentException(
|
switch (binding.getDirection()) {
|
||||||
"No read-write attribute for path \"" + binding.getPath() + "\"");
|
case "both" -> alignBothComplex(controlProperty, attribute);
|
||||||
|
case "view" -> alignTowardViewComplex(controlProperty, attribute);
|
||||||
|
case "model" -> alignTowardModelComplex(controlProperty, attribute);
|
||||||
|
default -> throw new IllegalArgumentException("Not a direction: " + binding.getDirection());
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
switch (binding.getDirection()) {
|
switch (binding.getDirection()) {
|
||||||
case "both" -> alignBoth(controlProperty, attribute);
|
case "both" -> alignBoth(controlProperty, attribute);
|
||||||
@@ -40,6 +49,61 @@ final class DirectBindingApplier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Attribute a Path names, and whether that Attribute is complex.
|
||||||
|
*/
|
||||||
|
private static final class Resolved {
|
||||||
|
private final Property<?> attribute;
|
||||||
|
private final boolean complex;
|
||||||
|
|
||||||
|
private Resolved(Property<?> attribute, boolean complex) {
|
||||||
|
this.attribute = attribute;
|
||||||
|
this.complex = complex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Follows a Path to the Attribute it names.
|
||||||
|
* <p>
|
||||||
|
* One name is an Attribute of {@code viewModel}. Each earlier name steps into the ViewModel
|
||||||
|
* held by that Complex attribute. A null Complex attribute along the Path leaves the binding
|
||||||
|
* unapplied.
|
||||||
|
*
|
||||||
|
* @param viewModel the ViewModel for this View
|
||||||
|
* @param path the Path
|
||||||
|
* @return the Attribute for the last name, or {@code null} when a Complex attribute
|
||||||
|
* along the Path is null
|
||||||
|
*/
|
||||||
|
private static Resolved resolve(ViewModel viewModel, String path) {
|
||||||
|
String[] names = path.split("\\.", -1);
|
||||||
|
ViewModel current = viewModel;
|
||||||
|
for (int index = 0; index < names.length - 1; index++) {
|
||||||
|
String name = names[index];
|
||||||
|
Property<?> step = current.property(name);
|
||||||
|
if (step == null || !current.isComplex(name)) {
|
||||||
|
throw missing(path);
|
||||||
|
}
|
||||||
|
Object nested = step.getValue();
|
||||||
|
if (nested == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!(nested instanceof ViewModel child)) {
|
||||||
|
throw missing(path);
|
||||||
|
}
|
||||||
|
current = child;
|
||||||
|
}
|
||||||
|
String last = names[names.length - 1];
|
||||||
|
Property<?> attribute = current.property(last);
|
||||||
|
if (attribute == null) {
|
||||||
|
throw missing(path);
|
||||||
|
}
|
||||||
|
return new Resolved(attribute, current.isComplex(last));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IllegalArgumentException missing(String path) {
|
||||||
|
return new IllegalArgumentException("No read-write attribute for path \"" + path + "\"");
|
||||||
|
}
|
||||||
|
|
||||||
private static Property<?> controlProperty(Object control, String propertyName) {
|
private static Property<?> controlProperty(Object control, String propertyName) {
|
||||||
String methodName = propertyName + "Property";
|
String methodName = propertyName + "Property";
|
||||||
try {
|
try {
|
||||||
@@ -57,6 +121,53 @@ final class DirectBindingApplier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static void alignBothComplex(Property<?> controlProperty, Property<?> attribute) {
|
||||||
|
Property<Object> control = (Property<Object>) controlProperty;
|
||||||
|
Property<Object> source = (Property<Object>) attribute;
|
||||||
|
control.setValue(modelBehind(source.getValue()));
|
||||||
|
source.addListener((observable, oldValue, newValue) -> {
|
||||||
|
Object model = modelBehind(newValue);
|
||||||
|
if (control.getValue() != model) {
|
||||||
|
control.setValue(model);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
control.addListener((observable, oldValue, newValue) -> {
|
||||||
|
if (modelBehind(source.getValue()) == newValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
source.setValue(newValue == null ? null : ViewModels.of(newValue));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static void alignTowardViewComplex(Property<?> controlProperty, Property<?> attribute) {
|
||||||
|
Property<Object> control = (Property<Object>) controlProperty;
|
||||||
|
Property<Object> source = (Property<Object>) attribute;
|
||||||
|
control.setValue(modelBehind(source.getValue()));
|
||||||
|
source.addListener((observable, oldValue, newValue) -> control.setValue(modelBehind(newValue)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static void alignTowardModelComplex(Property<?> controlProperty, Property<?> attribute) {
|
||||||
|
Property<Object> control = (Property<Object>) controlProperty;
|
||||||
|
Property<Object> source = (Property<Object>) attribute;
|
||||||
|
control.setValue(modelBehind(source.getValue()));
|
||||||
|
control.addListener((observable, oldValue, newValue) -> {
|
||||||
|
if (modelBehind(source.getValue()) == newValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
source.setValue(newValue == null ? null : ViewModels.of(newValue));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object modelBehind(Object value) {
|
||||||
|
if (value instanceof ViewModel viewModel) {
|
||||||
|
return viewModel.model();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private static void alignBoth(Property<?> controlProperty, Property<?> attribute) {
|
private static void alignBoth(Property<?> controlProperty, Property<?> attribute) {
|
||||||
((Property<Object>) controlProperty).bindBidirectional((Property<Object>) attribute);
|
((Property<Object>) controlProperty).bindBidirectional((Property<Object>) attribute);
|
||||||
|
|||||||
@@ -18,13 +18,17 @@ 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
|
* Reuses the ViewModel already built for {@code model}, or builds one, then loads {@code fxml}
|
||||||
* declared in {@code fx:define}. Direction {@code both} keeps the control and the Attribute
|
* and applies Direct bindings declared in {@code fx:define}. 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
|
||||||
* changes, and does not push later property changes to the control. A path that is not a
|
* changes, and does not push later property changes to the control. A Path of one name is an
|
||||||
* read-write Attribute fails the load. The context is used when the document names a Controller.
|
* Attribute of this View's ViewModel. Further names step into a nested ViewModel, and the
|
||||||
|
* direction applies to the last name. When a Complex attribute along the Path is null, the
|
||||||
|
* control and the Model keep their current values. A path that is not a read-write Attribute
|
||||||
|
* fails the load. The context is used when the document names a Controller.
|
||||||
*
|
*
|
||||||
* @param fxml the FXML document
|
* @param fxml the FXML document
|
||||||
* @param model the Model instance
|
* @param model the Model instance
|
||||||
@@ -33,7 +37,7 @@ public final class ViewLoader {
|
|||||||
* @throws IllegalArgumentException when a Direct binding names a path that is not a read-write Attribute
|
* @throws IllegalArgumentException when a Direct binding names a path that is not a read-write Attribute
|
||||||
*/
|
*/
|
||||||
public static LoadedView load(URL fxml, Object model, ApplicationContext context) {
|
public static LoadedView load(URL fxml, Object model, ApplicationContext context) {
|
||||||
ViewModel viewModel = new ViewModel(model);
|
ViewModel viewModel = ViewModels.of(model);
|
||||||
LoadSession session = LoadSession.open();
|
LoadSession session = LoadSession.open();
|
||||||
try {
|
try {
|
||||||
FXMLLoader loader = new FXMLLoader(fxml);
|
FXMLLoader loader = new FXMLLoader(fxml);
|
||||||
|
|||||||
@@ -20,9 +20,14 @@ import java.beans.Introspector;
|
|||||||
import java.beans.PropertyDescriptor;
|
import java.beans.PropertyDescriptor;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The object a View binds to, built at runtime for one Model instance.
|
* The object a View binds to, built at runtime for one Model instance.
|
||||||
@@ -31,14 +36,21 @@ import java.util.Map;
|
|||||||
* property under the Attribute name. The property class follows the Attribute's declared type,
|
* property under the Attribute name. The property class follows the Attribute's declared type,
|
||||||
* including when the current value is null. String, integer, long, float, double, and boolean
|
* including when the current value is null. String, integer, long, float, double, and boolean
|
||||||
* attributes use the matching JavaFX property. Any other declared type is an object property.
|
* attributes use the matching JavaFX property. Any other declared type is an object property.
|
||||||
|
* A Complex attribute, a read-write Attribute whose declared type itself has a read-write
|
||||||
|
* Attribute, stores the ViewModel for the nested Model instance, or null when the Attribute
|
||||||
|
* is null. Strings, numbers, booleans, enums, {@link Date}, {@link Calendar}, and {@code java.time}
|
||||||
|
* values stay simple even when that type has setters. A collection is not a Complex attribute.
|
||||||
|
* A cycle, or another Attribute that reaches the same instance, reuses that ViewModel.
|
||||||
* The property is set from the Attribute when the ViewModel is created. A null number or
|
* The property is set from the Attribute when the ViewModel is created. A null number or
|
||||||
* boolean leaves the primitive property at its default. After that, a change of the property
|
* boolean leaves the primitive property at its default. After that, a change of the property
|
||||||
* writes the Attribute. A call to the entity's setter does not update the property.
|
* writes the Attribute. A Complex attribute writes the Model instance behind the ViewModel.
|
||||||
|
* A call to the entity's setter does not update the property.
|
||||||
*/
|
*/
|
||||||
public class ViewModel {
|
public class ViewModel {
|
||||||
|
|
||||||
private final Object model;
|
private final Object model;
|
||||||
private final Map<String, Property<?>> properties = new HashMap<>();
|
private final Map<String, Property<?>> properties = new HashMap<>();
|
||||||
|
private final Set<String> complexAttributes = new HashSet<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds a ViewModel for one Model instance.
|
* Builds a ViewModel for one Model instance.
|
||||||
@@ -47,6 +59,7 @@ public class ViewModel {
|
|||||||
*/
|
*/
|
||||||
ViewModel(Object model) {
|
ViewModel(Object model) {
|
||||||
this.model = model;
|
this.model = model;
|
||||||
|
ViewModels.register(model, this);
|
||||||
for (PropertyDescriptor descriptor : descriptors(model.getClass())) {
|
for (PropertyDescriptor descriptor : descriptors(model.getClass())) {
|
||||||
Method getter = descriptor.getReadMethod();
|
Method getter = descriptor.getReadMethod();
|
||||||
Method setter = descriptor.getWriteMethod();
|
Method setter = descriptor.getWriteMethod();
|
||||||
@@ -54,9 +67,19 @@ public class ViewModel {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
String name = descriptor.getName();
|
String name = descriptor.getName();
|
||||||
Property<?> property = createProperty(descriptor.getPropertyType(), read(getter, name));
|
Class<?> type = descriptor.getPropertyType();
|
||||||
watch(property, setter, name);
|
Object current = read(getter, name);
|
||||||
properties.put(name, property);
|
if (isComplex(type)) {
|
||||||
|
Property<?> property = new SimpleObjectProperty<>(
|
||||||
|
current == null ? null : ViewModels.of(current));
|
||||||
|
watchComplex(property, setter, name);
|
||||||
|
properties.put(name, property);
|
||||||
|
complexAttributes.add(name);
|
||||||
|
} else {
|
||||||
|
Property<?> property = createProperty(type, current);
|
||||||
|
watch(property, setter, name);
|
||||||
|
properties.put(name, property);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,6 +93,25 @@ public class ViewModel {
|
|||||||
return properties.get(name);
|
return properties.get(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Model instance this ViewModel was built for.
|
||||||
|
*
|
||||||
|
* @return the Model instance
|
||||||
|
*/
|
||||||
|
Object model() {
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reports whether an Attribute is complex.
|
||||||
|
*
|
||||||
|
* @param name the Attribute name
|
||||||
|
* @return {@code true} when that Attribute holds a nested ViewModel
|
||||||
|
*/
|
||||||
|
boolean isComplex(String name) {
|
||||||
|
return complexAttributes.contains(name);
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
@@ -79,6 +121,38 @@ public class ViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isComplex(Class<?> type) {
|
||||||
|
if (simpleValue(type)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (PropertyDescriptor descriptor : descriptors(type)) {
|
||||||
|
if (descriptor.getReadMethod() != null
|
||||||
|
&& descriptor.getWriteMethod() != null
|
||||||
|
&& descriptor.getPropertyType() != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean simpleValue(Class<?> type) {
|
||||||
|
if (type.isPrimitive() || type.isEnum() || type.isArray()
|
||||||
|
|| type == String.class || type == Boolean.class) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (Collection.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (Number.class.isAssignableFrom(type)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (Date.class.isAssignableFrom(type) || Calendar.class.isAssignableFrom(type)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Package packaged = type.getPackage();
|
||||||
|
return packaged != null && packaged.getName().startsWith("java.time");
|
||||||
|
}
|
||||||
|
|
||||||
private static Property<?> createProperty(Class<?> type, Object value) {
|
private static Property<?> createProperty(Class<?> type, Object value) {
|
||||||
if (type == String.class) {
|
if (type == String.class) {
|
||||||
return new SimpleStringProperty((String) value);
|
return new SimpleStringProperty((String) value);
|
||||||
@@ -153,6 +227,13 @@ public class ViewModel {
|
|||||||
property.addListener((observable, oldValue, newValue) -> write(setter, name, newValue));
|
property.addListener((observable, oldValue, newValue) -> write(setter, name, newValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void watchComplex(Property<?> property, Method setter, String name) {
|
||||||
|
property.addListener((observable, oldValue, newValue) -> {
|
||||||
|
Object written = newValue instanceof ViewModel viewModel ? viewModel.model() : newValue;
|
||||||
|
write(setter, name, written);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void write(Method setter, String name, Object value) {
|
private void write(Method setter, String name, Object value) {
|
||||||
try {
|
try {
|
||||||
setter.invoke(model, value);
|
setter.invoke(model, value);
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package de.neitzel.fx.view;
|
||||||
|
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ViewModels already built, one for each Model instance.
|
||||||
|
* <p>
|
||||||
|
* Instances are compared by identity, so two Models that are equal still get two ViewModels.
|
||||||
|
*/
|
||||||
|
final class ViewModels {
|
||||||
|
|
||||||
|
private static final Map<Object, ViewModel> BY_INSTANCE = new IdentityHashMap<>();
|
||||||
|
|
||||||
|
private ViewModels() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the ViewModel for a Model instance, building it when none exists yet.
|
||||||
|
*
|
||||||
|
* @param model the Model instance
|
||||||
|
* @return the ViewModel for that instance
|
||||||
|
*/
|
||||||
|
static ViewModel of(Object model) {
|
||||||
|
ViewModel existing = BY_INSTANCE.get(model);
|
||||||
|
if (existing != null) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
return new ViewModel(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records a ViewModel before its Attributes are walked, so a cycle reuses it.
|
||||||
|
*
|
||||||
|
* @param model the Model instance
|
||||||
|
* @param viewModel the ViewModel under construction
|
||||||
|
*/
|
||||||
|
static void register(Object model, ViewModel viewModel) {
|
||||||
|
BY_INSTANCE.put(model, viewModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
package de.neitzel.fx.view;
|
||||||
|
|
||||||
|
import de.neitzel.fx.view.fixture.Address;
|
||||||
|
import de.neitzel.fx.view.fixture.ObjectHolder;
|
||||||
|
import de.neitzel.fx.view.fixture.Person;
|
||||||
|
import de.neitzel.fx.view.fixture.Role;
|
||||||
|
import de.neitzel.injection.ApplicationContext;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
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.time.LocalDate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.GregorianCalendar;
|
||||||
|
import java.util.List;
|
||||||
|
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.assertInstanceOf;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotSame;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||||
|
import static org.junit.jupiter.api.Assertions.fail;
|
||||||
|
|
||||||
|
class ReachNestedAttributeTest {
|
||||||
|
|
||||||
|
@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 complexAttributeHoldsTheViewModelForTheNestedAddress() throws Exception {
|
||||||
|
onFx(() -> {
|
||||||
|
Person person = new Person("Ada");
|
||||||
|
person.setAddress(new Address("Main"));
|
||||||
|
URL fxml = ReachNestedAttributeTest.class.getResource("name-both-ways.fxml");
|
||||||
|
assertNotNull(fxml);
|
||||||
|
ApplicationContext context = new ApplicationContext("de.neitzel.fx.view.fixture");
|
||||||
|
LoadedView loaded = ViewLoader.load(fxml, person, context);
|
||||||
|
Object addressValue = loaded.property("address").getValue();
|
||||||
|
assertInstanceOf(ViewModel.class, addressValue);
|
||||||
|
assertEquals("Main", ((ViewModel) addressValue).property("street").getValue());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void bothWaysTextFieldReadsAndWritesTheNestedStreet() throws Exception {
|
||||||
|
onFx(() -> {
|
||||||
|
Address address = new Address("Main");
|
||||||
|
Person person = new Person("Ada");
|
||||||
|
person.setAddress(address);
|
||||||
|
URL fxml = ReachNestedAttributeTest.class.getResource("address-street-both.fxml");
|
||||||
|
assertNotNull(fxml);
|
||||||
|
ApplicationContext context = new ApplicationContext("de.neitzel.fx.view.fixture");
|
||||||
|
LoadedView loaded = ViewLoader.load(fxml, person, context);
|
||||||
|
TextField streetField = (TextField) loaded.getRoot().lookup("#streetField");
|
||||||
|
assertEquals("Main", streetField.getText());
|
||||||
|
streetField.setText("Oak");
|
||||||
|
assertEquals("Oak", address.getStreet());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void towardViewLabelFollowsTheNestedStreetAndDoesNotWrite() throws Exception {
|
||||||
|
onFx(() -> {
|
||||||
|
Address address = new Address("Main");
|
||||||
|
Person person = new Person("Ada");
|
||||||
|
person.setAddress(address);
|
||||||
|
URL fxml = ReachNestedAttributeTest.class.getResource("address-street-both-and-view.fxml");
|
||||||
|
assertNotNull(fxml);
|
||||||
|
ApplicationContext context = new ApplicationContext("de.neitzel.fx.view.fixture");
|
||||||
|
LoadedView loaded = ViewLoader.load(fxml, person, context);
|
||||||
|
TextField streetField = (TextField) loaded.getRoot().lookup("#streetField");
|
||||||
|
Label streetLabel = (Label) loaded.getRoot().lookup("#streetLabel");
|
||||||
|
assertEquals("Main", streetLabel.getText());
|
||||||
|
streetField.setText("Oak");
|
||||||
|
assertEquals("Oak", streetLabel.getText());
|
||||||
|
assertEquals("Oak", address.getStreet());
|
||||||
|
streetLabel.setText("Side");
|
||||||
|
assertEquals("Oak", address.getStreet());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void bothWaysBindingOnAComplexPathYieldsTheModelInstance() throws Exception {
|
||||||
|
onFx(() -> {
|
||||||
|
Address address = new Address("Main");
|
||||||
|
Person person = new Person("Ada");
|
||||||
|
person.setAddress(address);
|
||||||
|
URL fxml = ReachNestedAttributeTest.class.getResource("address-object.fxml");
|
||||||
|
assertNotNull(fxml);
|
||||||
|
ApplicationContext context = new ApplicationContext("de.neitzel.fx.view.fixture");
|
||||||
|
LoadedView loaded = ViewLoader.load(fxml, person, context);
|
||||||
|
ObjectHolder addressHolder = (ObjectHolder) loaded.getRoot().lookup("#addressHolder");
|
||||||
|
assertSame(address, addressHolder.getValue());
|
||||||
|
Address moved = new Address("Oak");
|
||||||
|
addressHolder.setValue(moved);
|
||||||
|
assertSame(moved, person.getAddress());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fillingAMissingAddressStoresTheViewModelAndWritesTheModel() throws Exception {
|
||||||
|
onFx(() -> {
|
||||||
|
Person person = new Person("Ada");
|
||||||
|
URL fxml = ReachNestedAttributeTest.class.getResource("address-object.fxml");
|
||||||
|
assertNotNull(fxml);
|
||||||
|
ApplicationContext context = new ApplicationContext("de.neitzel.fx.view.fixture");
|
||||||
|
LoadedView loaded = ViewLoader.load(fxml, person, context);
|
||||||
|
ObjectHolder addressHolder = (ObjectHolder) loaded.getRoot().lookup("#addressHolder");
|
||||||
|
assertNull(addressHolder.getValue());
|
||||||
|
assertNull(person.getAddress());
|
||||||
|
Address address = new Address("Main");
|
||||||
|
addressHolder.setValue(address);
|
||||||
|
assertSame(address, person.getAddress());
|
||||||
|
Object addressValue = loaded.property("address").getValue();
|
||||||
|
assertInstanceOf(ViewModel.class, addressValue);
|
||||||
|
assertEquals("Main", ((ViewModel) addressValue).property("street").getValue());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void missingAddressLeavesTheControlAndTheModelUnchanged() throws Exception {
|
||||||
|
onFx(() -> {
|
||||||
|
Person person = new Person("Ada");
|
||||||
|
URL fxml = ReachNestedAttributeTest.class.getResource("address-street-kept.fxml");
|
||||||
|
assertNotNull(fxml);
|
||||||
|
ApplicationContext context = new ApplicationContext("de.neitzel.fx.view.fixture");
|
||||||
|
LoadedView loaded = ViewLoader.load(fxml, person, context);
|
||||||
|
TextField streetField = (TextField) loaded.getRoot().lookup("#streetField");
|
||||||
|
assertEquals("kept", streetField.getText());
|
||||||
|
assertNull(person.getAddress());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nullComplexAttributeHoldsNull() throws Exception {
|
||||||
|
onFx(() -> {
|
||||||
|
Person person = new Person("Ada");
|
||||||
|
URL fxml = ReachNestedAttributeTest.class.getResource("name-both-ways.fxml");
|
||||||
|
assertNotNull(fxml);
|
||||||
|
ApplicationContext context = new ApplicationContext("de.neitzel.fx.view.fixture");
|
||||||
|
LoadedView loaded = ViewLoader.load(fxml, person, context);
|
||||||
|
assertNull(loaded.property("address").getValue());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void attributesThatReachTheSameAddressShareItsViewModel() throws Exception {
|
||||||
|
onFx(() -> {
|
||||||
|
Address shared = new Address("Main");
|
||||||
|
Person person = new Person("Ada");
|
||||||
|
person.setHome(shared);
|
||||||
|
person.setWork(shared);
|
||||||
|
URL fxml = ReachNestedAttributeTest.class.getResource("home-work-street.fxml");
|
||||||
|
assertNotNull(fxml);
|
||||||
|
ApplicationContext context = new ApplicationContext("de.neitzel.fx.view.fixture");
|
||||||
|
LoadedView loaded = ViewLoader.load(fxml, person, context);
|
||||||
|
assertSame(loaded.property("home").getValue(), loaded.property("work").getValue());
|
||||||
|
TextField homeField = (TextField) loaded.getRoot().lookup("#homeField");
|
||||||
|
TextField workField = (TextField) loaded.getRoot().lookup("#workField");
|
||||||
|
homeField.setText("Oak");
|
||||||
|
assertEquals("Oak", workField.getText());
|
||||||
|
assertEquals("Oak", shared.getStreet());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void equalAddressesKeepSeparateViewModels() throws Exception {
|
||||||
|
onFx(() -> {
|
||||||
|
Address home = new Address("Main");
|
||||||
|
Address work = new Address("Main");
|
||||||
|
assertEquals(home, work);
|
||||||
|
Person person = new Person("Ada");
|
||||||
|
person.setHome(home);
|
||||||
|
person.setWork(work);
|
||||||
|
URL fxml = ReachNestedAttributeTest.class.getResource("home-work-street.fxml");
|
||||||
|
assertNotNull(fxml);
|
||||||
|
ApplicationContext context = new ApplicationContext("de.neitzel.fx.view.fixture");
|
||||||
|
LoadedView loaded = ViewLoader.load(fxml, person, context);
|
||||||
|
assertNotSame(loaded.property("home").getValue(), loaded.property("work").getValue());
|
||||||
|
TextField homeField = (TextField) loaded.getRoot().lookup("#homeField");
|
||||||
|
TextField workField = (TextField) loaded.getRoot().lookup("#workField");
|
||||||
|
homeField.setText("Oak");
|
||||||
|
assertEquals("Oak", home.getStreet());
|
||||||
|
assertEquals("Main", work.getStreet());
|
||||||
|
assertEquals("Main", workField.getText());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aPersonWhoIsTheirOwnFriendReusesTheSameViewModel() throws Exception {
|
||||||
|
onFx(() -> {
|
||||||
|
Person person = new Person("Ada");
|
||||||
|
person.setFriend(person);
|
||||||
|
URL fxml = ReachNestedAttributeTest.class.getResource("name-both-ways.fxml");
|
||||||
|
assertNotNull(fxml);
|
||||||
|
ApplicationContext context = new ApplicationContext("de.neitzel.fx.view.fixture");
|
||||||
|
LoadedView loaded = ViewLoader.load(fxml, person, context);
|
||||||
|
assertSame(loaded.getViewModel(), loaded.property("friend").getValue());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void twoPeopleWhoPointAtEachOtherReuseTheViewModelsAlreadyBuilt() throws Exception {
|
||||||
|
onFx(() -> {
|
||||||
|
Person ada = new Person("Ada");
|
||||||
|
Person grace = new Person("Grace");
|
||||||
|
ada.setFriend(grace);
|
||||||
|
grace.setFriend(ada);
|
||||||
|
URL fxml = ReachNestedAttributeTest.class.getResource("name-both-ways.fxml");
|
||||||
|
assertNotNull(fxml);
|
||||||
|
ApplicationContext context = new ApplicationContext("de.neitzel.fx.view.fixture");
|
||||||
|
LoadedView loaded = ViewLoader.load(fxml, ada, context);
|
||||||
|
ViewModel graceModel = (ViewModel) loaded.property("friend").getValue();
|
||||||
|
assertEquals("Grace", graceModel.property("name").getValue());
|
||||||
|
assertSame(loaded.getViewModel(), graceModel.property("friend").getValue());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void enumsBooleansAndDateValuesStaySimpleValues() throws Exception {
|
||||||
|
onFx(() -> {
|
||||||
|
Date updated = new Date(0L);
|
||||||
|
Calendar hired = new GregorianCalendar(2020, Calendar.JANUARY, 2);
|
||||||
|
LocalDate born = LocalDate.of(1815, 12, 10);
|
||||||
|
Person person = new Person("Ada");
|
||||||
|
person.setActive(true);
|
||||||
|
person.setRole(Role.ADMIN);
|
||||||
|
person.setBorn(born);
|
||||||
|
person.setUpdated(updated);
|
||||||
|
person.setHired(hired);
|
||||||
|
URL fxml = ReachNestedAttributeTest.class.getResource("name-both-ways.fxml");
|
||||||
|
assertNotNull(fxml);
|
||||||
|
ApplicationContext context = new ApplicationContext("de.neitzel.fx.view.fixture");
|
||||||
|
LoadedView loaded = ViewLoader.load(fxml, person, context);
|
||||||
|
assertEquals(true, loaded.property("active").getValue());
|
||||||
|
assertSame(Role.ADMIN, loaded.property("role").getValue());
|
||||||
|
assertEquals(born, loaded.property("born").getValue());
|
||||||
|
assertSame(updated, loaded.property("updated").getValue());
|
||||||
|
assertSame(hired, loaded.property("hired").getValue());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aCollectionStaysTheCollectionRatherThanAViewModel() throws Exception {
|
||||||
|
onFx(() -> {
|
||||||
|
List<String> tags = new ArrayList<>();
|
||||||
|
tags.add("friend");
|
||||||
|
Person person = new Person("Ada");
|
||||||
|
person.setTags(tags);
|
||||||
|
URL fxml = ReachNestedAttributeTest.class.getResource("name-both-ways.fxml");
|
||||||
|
assertNotNull(fxml);
|
||||||
|
ApplicationContext context = new ApplicationContext("de.neitzel.fx.view.fixture");
|
||||||
|
LoadedView loaded = ViewLoader.load(fxml, person, context);
|
||||||
|
assertSame(tags, loaded.property("tags").getValue());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void twoLoadsOfTheSamePersonShareTheViewModel() throws Exception {
|
||||||
|
onFx(() -> {
|
||||||
|
Person person = new Person("Ada");
|
||||||
|
URL fxml = ReachNestedAttributeTest.class.getResource("name-both-ways.fxml");
|
||||||
|
assertNotNull(fxml);
|
||||||
|
ApplicationContext context = new ApplicationContext("de.neitzel.fx.view.fixture");
|
||||||
|
LoadedView first = ViewLoader.load(fxml, person, context);
|
||||||
|
LoadedView second = ViewLoader.load(fxml, person, context);
|
||||||
|
assertSame(first.getViewModel(), second.getViewModel());
|
||||||
|
TextField firstName = (TextField) first.getRoot().lookup("#nameField");
|
||||||
|
TextField secondName = (TextField) second.getRoot().lookup("#nameField");
|
||||||
|
firstName.setText("Grace");
|
||||||
|
assertEquals("Grace", secondName.getText());
|
||||||
|
assertEquals("Grace", person.getName());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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,54 @@
|
|||||||
|
package de.neitzel.fx.view.fixture;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ordinary nested model used by view-loading tests.
|
||||||
|
* <p>
|
||||||
|
* Equality follows the street, so two addresses with the same street are equal while remaining
|
||||||
|
* different instances.
|
||||||
|
*/
|
||||||
|
public class Address {
|
||||||
|
|
||||||
|
private String street;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an address with the given street.
|
||||||
|
*
|
||||||
|
* @param street the current street
|
||||||
|
*/
|
||||||
|
public Address(String street) {
|
||||||
|
this.street = street;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the street.
|
||||||
|
*
|
||||||
|
* @return the street
|
||||||
|
*/
|
||||||
|
public String getStreet() {
|
||||||
|
return street;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the street.
|
||||||
|
*
|
||||||
|
* @param street the street
|
||||||
|
*/
|
||||||
|
public void setStreet(String street) {
|
||||||
|
this.street = street;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object other) {
|
||||||
|
if (other instanceof Address address) {
|
||||||
|
return Objects.equals(street, address.street);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hashCode(street);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package de.neitzel.fx.view.fixture;
|
||||||
|
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.scene.layout.Pane;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A small control whose value is an object, used to bind a Complex attribute.
|
||||||
|
*/
|
||||||
|
public class ObjectHolder extends Pane {
|
||||||
|
|
||||||
|
private final ObjectProperty<Object> value = new SimpleObjectProperty<>(this, "value");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value property.
|
||||||
|
*
|
||||||
|
* @return the value property
|
||||||
|
*/
|
||||||
|
public ObjectProperty<Object> valueProperty() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current value.
|
||||||
|
*
|
||||||
|
* @return the value
|
||||||
|
*/
|
||||||
|
public Object getValue() {
|
||||||
|
return value.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the value.
|
||||||
|
*
|
||||||
|
* @param value the value
|
||||||
|
*/
|
||||||
|
public void setValue(Object value) {
|
||||||
|
this.value.set(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
package de.neitzel.fx.view.fixture;
|
package de.neitzel.fx.view.fixture;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ordinary model used by view-loading tests.
|
* Ordinary model used by view-loading tests.
|
||||||
*/
|
*/
|
||||||
@@ -7,6 +12,16 @@ public class Person {
|
|||||||
|
|
||||||
private String name;
|
private String name;
|
||||||
private Integer age;
|
private Integer age;
|
||||||
|
private boolean active;
|
||||||
|
private Address address;
|
||||||
|
private Address home;
|
||||||
|
private Address work;
|
||||||
|
private Person friend;
|
||||||
|
private Role role;
|
||||||
|
private LocalDate born;
|
||||||
|
private Date updated;
|
||||||
|
private Calendar hired;
|
||||||
|
private List<String> tags;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a person with the given name.
|
* Creates a person with the given name.
|
||||||
@@ -61,4 +76,184 @@ public class Person {
|
|||||||
public void setAge(Integer age) {
|
public void setAge(Integer age) {
|
||||||
this.age = age;
|
this.age = age;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the address.
|
||||||
|
*
|
||||||
|
* @return the address, or {@code null} when the person has none
|
||||||
|
*/
|
||||||
|
public Address getAddress() {
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the address.
|
||||||
|
*
|
||||||
|
* @param address the address
|
||||||
|
*/
|
||||||
|
public void setAddress(Address address) {
|
||||||
|
this.address = address;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the home address.
|
||||||
|
*
|
||||||
|
* @return the home address
|
||||||
|
*/
|
||||||
|
public Address getHome() {
|
||||||
|
return home;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the home address.
|
||||||
|
*
|
||||||
|
* @param home the home address
|
||||||
|
*/
|
||||||
|
public void setHome(Address home) {
|
||||||
|
this.home = home;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the work address.
|
||||||
|
*
|
||||||
|
* @return the work address
|
||||||
|
*/
|
||||||
|
public Address getWork() {
|
||||||
|
return work;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the work address.
|
||||||
|
*
|
||||||
|
* @param work the work address
|
||||||
|
*/
|
||||||
|
public void setWork(Address work) {
|
||||||
|
this.work = work;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the friend.
|
||||||
|
*
|
||||||
|
* @return the friend
|
||||||
|
*/
|
||||||
|
public Person getFriend() {
|
||||||
|
return friend;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the friend.
|
||||||
|
*
|
||||||
|
* @param friend the friend
|
||||||
|
*/
|
||||||
|
public void setFriend(Person friend) {
|
||||||
|
this.friend = friend;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the person is active.
|
||||||
|
*
|
||||||
|
* @return {@code true} when the person is active
|
||||||
|
*/
|
||||||
|
public boolean isActive() {
|
||||||
|
return active;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether the person is active.
|
||||||
|
*
|
||||||
|
* @param active {@code true} when the person is active
|
||||||
|
*/
|
||||||
|
public void setActive(boolean active) {
|
||||||
|
this.active = active;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the role.
|
||||||
|
*
|
||||||
|
* @return the role
|
||||||
|
*/
|
||||||
|
public Role getRole() {
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the role.
|
||||||
|
*
|
||||||
|
* @param role the role
|
||||||
|
*/
|
||||||
|
public void setRole(Role role) {
|
||||||
|
this.role = role;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the birth date.
|
||||||
|
*
|
||||||
|
* @return the birth date
|
||||||
|
*/
|
||||||
|
public LocalDate getBorn() {
|
||||||
|
return born;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the birth date.
|
||||||
|
*
|
||||||
|
* @param born the birth date
|
||||||
|
*/
|
||||||
|
public void setBorn(LocalDate born) {
|
||||||
|
this.born = born;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the last update.
|
||||||
|
*
|
||||||
|
* @return the last update
|
||||||
|
*/
|
||||||
|
public Date getUpdated() {
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the last update.
|
||||||
|
*
|
||||||
|
* @param updated the last update
|
||||||
|
*/
|
||||||
|
public void setUpdated(Date updated) {
|
||||||
|
this.updated = updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the hire date.
|
||||||
|
*
|
||||||
|
* @return the hire date
|
||||||
|
*/
|
||||||
|
public Calendar getHired() {
|
||||||
|
return hired;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the hire date.
|
||||||
|
*
|
||||||
|
* @param hired the hire date
|
||||||
|
*/
|
||||||
|
public void setHired(Calendar hired) {
|
||||||
|
this.hired = hired;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the tags.
|
||||||
|
*
|
||||||
|
* @return the tags
|
||||||
|
*/
|
||||||
|
public List<String> getTags() {
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the tags.
|
||||||
|
*
|
||||||
|
* @param tags the tags
|
||||||
|
*/
|
||||||
|
public void setTags(List<String> tags) {
|
||||||
|
this.tags = tags;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package de.neitzel.fx.view.fixture;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple enum Attribute used by view-loading tests.
|
||||||
|
*/
|
||||||
|
public enum Role {
|
||||||
|
MEMBER,
|
||||||
|
ADMIN
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.scene.layout.VBox?>
|
||||||
|
<?import de.neitzel.fx.view.BindingList?>
|
||||||
|
<?import de.neitzel.fx.view.DirectBinding?>
|
||||||
|
<?import de.neitzel.fx.view.fixture.ObjectHolder?>
|
||||||
|
|
||||||
|
<VBox xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml">
|
||||||
|
<fx:define>
|
||||||
|
<BindingList>
|
||||||
|
<DirectBinding control="addressHolder" property="value" path="address" direction="both"/>
|
||||||
|
</BindingList>
|
||||||
|
</fx:define>
|
||||||
|
<ObjectHolder fx:id="addressHolder" id="addressHolder"/>
|
||||||
|
</VBox>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?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.DirectBinding?>
|
||||||
|
|
||||||
|
<VBox xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml">
|
||||||
|
<fx:define>
|
||||||
|
<BindingList>
|
||||||
|
<DirectBinding control="streetField" property="text" path="address.street" direction="both"/>
|
||||||
|
<DirectBinding control="streetLabel" property="text" path="address.street" direction="view"/>
|
||||||
|
</BindingList>
|
||||||
|
</fx:define>
|
||||||
|
<TextField fx:id="streetField" id="streetField"/>
|
||||||
|
<Label fx:id="streetLabel" id="streetLabel"/>
|
||||||
|
</VBox>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.scene.control.TextField?>
|
||||||
|
<?import javafx.scene.layout.VBox?>
|
||||||
|
<?import de.neitzel.fx.view.BindingList?>
|
||||||
|
<?import de.neitzel.fx.view.DirectBinding?>
|
||||||
|
|
||||||
|
<VBox xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml">
|
||||||
|
<fx:define>
|
||||||
|
<BindingList>
|
||||||
|
<DirectBinding control="streetField" property="text" path="address.street" direction="both"/>
|
||||||
|
</BindingList>
|
||||||
|
</fx:define>
|
||||||
|
<TextField fx:id="streetField" id="streetField"/>
|
||||||
|
</VBox>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.scene.control.TextField?>
|
||||||
|
<?import javafx.scene.layout.VBox?>
|
||||||
|
<?import de.neitzel.fx.view.BindingList?>
|
||||||
|
<?import de.neitzel.fx.view.DirectBinding?>
|
||||||
|
|
||||||
|
<VBox xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml">
|
||||||
|
<fx:define>
|
||||||
|
<BindingList>
|
||||||
|
<DirectBinding control="streetField" property="text" path="address.street" direction="both"/>
|
||||||
|
</BindingList>
|
||||||
|
</fx:define>
|
||||||
|
<TextField fx:id="streetField" id="streetField" text="kept"/>
|
||||||
|
</VBox>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.scene.control.TextField?>
|
||||||
|
<?import javafx.scene.layout.VBox?>
|
||||||
|
<?import de.neitzel.fx.view.BindingList?>
|
||||||
|
<?import de.neitzel.fx.view.DirectBinding?>
|
||||||
|
|
||||||
|
<VBox xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml">
|
||||||
|
<fx:define>
|
||||||
|
<BindingList>
|
||||||
|
<DirectBinding control="homeField" property="text" path="home.street" direction="both"/>
|
||||||
|
<DirectBinding control="workField" property="text" path="work.street" direction="both"/>
|
||||||
|
</BindingList>
|
||||||
|
</fx:define>
|
||||||
|
<TextField fx:id="homeField" id="homeField"/>
|
||||||
|
<TextField fx:id="workField" id="workField"/>
|
||||||
|
</VBox>
|
||||||
Reference in New Issue
Block a user