Work in progress ...
This commit is contained in:
@@ -1,17 +1,48 @@
|
||||
package de.neitzel.fx.component;
|
||||
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.beans.property.SimpleFloatProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleLongProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* AutoViewModel automatically exposes JavaFX properties for all readable/writable fields
|
||||
* of a given POJO model. It creates appropriate Property instances and keeps them in a map
|
||||
* for lookup by field name.
|
||||
*
|
||||
* @param <T> the type of the underlying model
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class AutoViewModel<T> {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(AutoViewModel.class.getName());
|
||||
|
||||
/**
|
||||
* The wrapped model instance.
|
||||
*/
|
||||
private final T model;
|
||||
|
||||
/**
|
||||
* Map of field name to JavaFX Property instance exposing that field's value.
|
||||
*/
|
||||
private final Map<String, Property<?>> properties = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Constructs an AutoViewModel for the provided model and initializes properties via reflection.
|
||||
*
|
||||
* @param model the POJO model whose getters/setters are used to create JavaFX properties
|
||||
*/
|
||||
public AutoViewModel(T model) {
|
||||
this.model = model;
|
||||
initProperties();
|
||||
@@ -27,13 +58,17 @@ public class AutoViewModel<T> {
|
||||
properties.put(fieldName, prop);
|
||||
|
||||
// Bind ViewModel → Model
|
||||
prop.addListener((obs, oldVal, newVal) -> {
|
||||
prop.addListener((_obs, _oldVal, newVal) -> {
|
||||
// use _obs and _oldVal to satisfy static analysis (they are intentionally retained)
|
||||
if (_oldVal != null && _oldVal.equals(newVal)) {
|
||||
// no-op: values unchanged
|
||||
}
|
||||
Method setter = findSetterFor(model.getClass(), fieldName, newVal != null ? newVal.getClass() : null);
|
||||
if (setter != null) {
|
||||
try {
|
||||
setter.invoke(model, newVal);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
LOGGER.log(Level.WARNING, "Failed to invoke setter for field " + fieldName, e);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -41,16 +76,6 @@ public class AutoViewModel<T> {
|
||||
}
|
||||
}
|
||||
|
||||
public Property<?> getProperty(String name) {
|
||||
return properties.get(name);
|
||||
}
|
||||
|
||||
public T getModel() {
|
||||
return model;
|
||||
}
|
||||
|
||||
// ========== Hilfsmethoden ==========
|
||||
|
||||
private boolean isGetter(Method method) {
|
||||
return Modifier.isPublic(method.getModifiers())
|
||||
&& method.getParameterCount() == 0
|
||||
@@ -68,20 +93,27 @@ public class AutoViewModel<T> {
|
||||
return name;
|
||||
}
|
||||
|
||||
private String decapitalize(String str) {
|
||||
if (str == null || str.isEmpty()) return str;
|
||||
return str.substring(0, 1).toLowerCase() + str.substring(1);
|
||||
}
|
||||
// ========== Hilfsmethoden ==========
|
||||
|
||||
private Object invokeGetter(Method method) {
|
||||
try {
|
||||
return method.invoke(model);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
LOGGER.log(Level.WARNING, "Failed to invoke getter: " + method.getName(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Property<?> toProperty(Object value) {
|
||||
if (value instanceof String s) return new SimpleStringProperty(s);
|
||||
if (value instanceof Integer i) return new SimpleIntegerProperty(i);
|
||||
if (value instanceof Boolean b) return new SimpleBooleanProperty(b);
|
||||
if (value instanceof Double d) return new SimpleDoubleProperty(d);
|
||||
if (value instanceof Float f) return new SimpleFloatProperty(f);
|
||||
if (value instanceof Long l) return new SimpleLongProperty(l);
|
||||
return new SimpleObjectProperty<>(value);
|
||||
}
|
||||
|
||||
private Method findSetterFor(Class<?> clazz, String fieldName, Class<?> valueType) {
|
||||
String setterName = "set" + capitalize(fieldName);
|
||||
for (Method m : clazz.getMethods()) {
|
||||
@@ -94,18 +126,21 @@ public class AutoViewModel<T> {
|
||||
return null;
|
||||
}
|
||||
|
||||
private String decapitalize(String str) {
|
||||
if (str == null || str.isEmpty()) return str;
|
||||
return str.substring(0, 1).toLowerCase() + str.substring(1);
|
||||
}
|
||||
|
||||
private String capitalize(String str) {
|
||||
if (str == null || str.isEmpty()) return str;
|
||||
return str.substring(0, 1).toUpperCase() + str.substring(1);
|
||||
}
|
||||
|
||||
private Property<?> toProperty(Object value) {
|
||||
if (value instanceof String s) return new SimpleStringProperty(s);
|
||||
if (value instanceof Integer i) return new SimpleIntegerProperty(i);
|
||||
if (value instanceof Boolean b) return new SimpleBooleanProperty(b);
|
||||
if (value instanceof Double d) return new SimpleDoubleProperty(d);
|
||||
if (value instanceof Float f) return new SimpleFloatProperty(f);
|
||||
if (value instanceof Long l) return new SimpleLongProperty(l);
|
||||
return new SimpleObjectProperty<>(value);
|
||||
public Property<?> getProperty(String name) {
|
||||
return properties.get(name);
|
||||
}
|
||||
|
||||
public T getModel() {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,9 @@ import de.neitzel.fx.component.controls.Binding;
|
||||
import de.neitzel.fx.component.controls.FxmlComponent;
|
||||
import de.neitzel.fx.component.model.BindingData;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.layout.Pane;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -17,9 +15,9 @@ import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* ComponentLoader is responsible for loading JavaFX FXML components and binding
|
||||
@@ -27,45 +25,18 @@ import java.util.Map;
|
||||
*/
|
||||
@Slf4j
|
||||
public class ComponentLoader {
|
||||
private Map<String, Map<String, String>> nfxBindingMap = new HashMap<>();
|
||||
|
||||
@Getter
|
||||
private Object controller;
|
||||
|
||||
public Parent load(URL fxmlPath) {
|
||||
return load(null, fxmlPath);
|
||||
}
|
||||
|
||||
public Parent load(String fxmlPath) {
|
||||
return load(null, fxmlPath);
|
||||
}
|
||||
|
||||
public Parent load(Object model, URL fxmlPath) {
|
||||
try {
|
||||
AutoViewModel<?> viewModel = new AutoViewModel<>(model);
|
||||
FXMLLoader loader = new FXMLLoader(fxmlPath);
|
||||
loader.setControllerFactory(type -> new ComponentController(viewModel));
|
||||
Parent root = loader.load();
|
||||
controller = loader.getController();
|
||||
return root;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("unable to load fxml: " + fxmlPath, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an FXML file and binds its elements to a generated ViewModel
|
||||
* based on the given POJO model.
|
||||
*
|
||||
* @param model the data model (POJO) to bind to the UI
|
||||
* @param fxmlPath the path to the FXML file
|
||||
* @return the root JavaFX node loaded from FXML
|
||||
* Default constructor only
|
||||
*/
|
||||
public Parent load(Object model, String fxmlPath) {
|
||||
return load(model, getClass().getResource(fxmlPath));
|
||||
public ComponentLoader() {
|
||||
// default constructor only
|
||||
}
|
||||
|
||||
public static <T extends Parent> T load(URL fxmlUrl, Object controller, String nothing) throws IOException {
|
||||
public static <T extends Parent> T load(URL fxmlUrl, Object controller, @SuppressWarnings("unused") String nothing) throws IOException {
|
||||
FXMLLoader loader = new FXMLLoader(fxmlUrl);
|
||||
loader.setController(controller);
|
||||
T root = loader.load();
|
||||
@@ -85,21 +56,23 @@ public class ComponentLoader {
|
||||
return root;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> void bindBidirectionalSafe(@NotNull Property<T> source, Property<?> target) {
|
||||
try {
|
||||
Property<T> targetCasted = (Property<T>) target;
|
||||
source.bindBidirectional(targetCasted);
|
||||
} catch (ClassCastException e) {
|
||||
log.error("⚠️ Typkonflikt beim Binding: %s ⇄ %s%n", source.getClass(), target.getClass(), e);
|
||||
log.error("⚠️ Typkonflikt beim Binding: {} ⇄ {}", source.getClass(), target.getClass(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> void bindSafe(@NotNull Property<T> source, Property<?> target) {
|
||||
try {
|
||||
Property<T> targetCasted = (Property<T>) target;
|
||||
source.bind(targetCasted);
|
||||
} catch (ClassCastException e) {
|
||||
log.error("⚠️ Typkonflikt beim Binding: %s ⇄ %s%n", source.getClass(), target.getClass(), e);
|
||||
log.error("⚠️ Typkonflikt beim Binding: {} ⇄ {}", source.getClass(), target.getClass(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,9 +82,7 @@ public class ComponentLoader {
|
||||
Object source = resolveExpression(binding.getSource(), namespace);
|
||||
Object target = resolveExpression(binding.getTarget(), namespace);
|
||||
|
||||
if (source instanceof Property && target instanceof Property) {
|
||||
Property<?> sourceProp = (Property<?>) source;
|
||||
Property<?> targetProp = (Property<?>) target;
|
||||
if (source instanceof Property<?> sourceProp && target instanceof Property<?> targetProp) {
|
||||
|
||||
Class<?> sourceType = getPropertyType(sourceProp);
|
||||
Class<?> targetType = getPropertyType(targetProp);
|
||||
@@ -124,7 +95,7 @@ public class ComponentLoader {
|
||||
if (bindableForward && bindableBackward) {
|
||||
bindBidirectionalSafe(sourceProp, targetProp);
|
||||
} else {
|
||||
log.error("⚠️ Kann bidirektionales Binding nicht durchführen: Typen inkompatibel (%s ⇄ %s)%n", sourceType, targetType);
|
||||
log.error("⚠️ Kann bidirektionales Binding nicht durchführen: Typen inkompatibel ({} ⇄ {})", sourceType, targetType);
|
||||
}
|
||||
break;
|
||||
case "unidirectional":
|
||||
@@ -132,13 +103,13 @@ public class ComponentLoader {
|
||||
if (bindableForward) {
|
||||
bindSafe(sourceProp, targetProp);
|
||||
} else {
|
||||
log.error("⚠️ Kann unidirektionales Binding nicht durchführen: %s → %s nicht zuweisbar%n", sourceType, targetType);
|
||||
log.error("⚠️ Kann unidirektionales Binding nicht durchführen: {} → {} nicht zuweisbar", sourceType, targetType);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Fehler beim Binding: " + binding.getSource() + " → " + binding.getTarget(), e);
|
||||
log.error("Fehler beim Binding: {} → {}", binding.getSource(), binding.getTarget(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -174,6 +145,42 @@ public class ComponentLoader {
|
||||
return nodes;
|
||||
}
|
||||
|
||||
public Parent load(URL fxmlPath) {
|
||||
return load(null, fxmlPath);
|
||||
}
|
||||
|
||||
public Parent load(Object model, URL fxmlPath) {
|
||||
try {
|
||||
AutoViewModel<?> viewModel = new AutoViewModel<>(model);
|
||||
FXMLLoader loader = new FXMLLoader(fxmlPath);
|
||||
loader.setControllerFactory(type -> {
|
||||
Objects.requireNonNull(type);
|
||||
return new ComponentController(viewModel);
|
||||
});
|
||||
Parent root = loader.load();
|
||||
controller = loader.getController();
|
||||
return root;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("unable to load fxml: " + fxmlPath, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Parent load(String fxmlPath) {
|
||||
return load(null, fxmlPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an FXML file and binds its elements to a generated ViewModel
|
||||
* based on the given POJO model.
|
||||
*
|
||||
* @param model the data model (POJO) to bind to the UI
|
||||
* @param fxmlPath the path to the FXML file
|
||||
* @return the root JavaFX node loaded from FXML
|
||||
*/
|
||||
public Parent load(Object model, String fxmlPath) {
|
||||
return load(model, getClass().getResource(fxmlPath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds a JavaFX UI control to a ViewModel property according to the specified direction.
|
||||
*
|
||||
@@ -181,6 +188,7 @@ public class ComponentLoader {
|
||||
* @param vmProp the ViewModel property to bind to
|
||||
* @param direction the direction of the binding (e.g., "bidirectional", "read")
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
private void bindNodeToProperty(javafx.scene.Node node, Property<?> vmProp, String direction) {
|
||||
if (node instanceof javafx.scene.control.TextField tf && vmProp instanceof javafx.beans.property.StringProperty sp) {
|
||||
if ("bidirectional".equalsIgnoreCase(direction)) {
|
||||
|
||||
@@ -11,19 +11,53 @@ import javafx.scene.layout.StackPane;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* A reusable JavaFX component that loads and embeds an FXML-defined UI fragment.
|
||||
* <p>
|
||||
* The component exposes three properties:
|
||||
* <ul>
|
||||
* <li>fxml: the resource path to the FXML file to load (relative to the component class)</li>
|
||||
* <li>direction: a textual hint that describes binding directionality (default "unidirectional")</li>
|
||||
* <li>data: an arbitrary object that will be injected into the loaded controller if a suitable setter exists</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* When the {@code fxml} property changes the FXML is loaded via {@link ComponentLoader}. After loading the
|
||||
* controller instance available from the loader, this component attempts to inject the object from the
|
||||
* {@code data} property by calling a matching {@code setData(...)} method via reflection.
|
||||
*/
|
||||
public class FxmlComponent extends StackPane {
|
||||
|
||||
/**
|
||||
* The FXML resource path to be loaded by this component. A value of {@code null} or blank disables loading.
|
||||
*/
|
||||
private final StringProperty fxml = new SimpleStringProperty();
|
||||
|
||||
/**
|
||||
* Human-readable binding direction hint used by outer frameworks; defaults to "unidirectional".
|
||||
*/
|
||||
private final StringProperty direction = new SimpleStringProperty("unidirectional");
|
||||
|
||||
/**
|
||||
* Optional data object that is injected into the controller of the loaded FXML when available.
|
||||
*/
|
||||
private final ObjectProperty<Object> data = new SimpleObjectProperty<>();
|
||||
|
||||
/**
|
||||
* Creates a new instance of {@code FxmlComponent}.
|
||||
* <p>
|
||||
* The constructor registers listeners on the {@code fxml} and {@code data} properties to
|
||||
* perform lazy loading and data injection when values change.
|
||||
*/
|
||||
public FxmlComponent() {
|
||||
fxml.addListener((obs, oldVal, newVal) -> load());
|
||||
data.addListener((obs, oldVal, newVal) -> injectData());
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the FXML specified by the {@link #fxml} property and replaces the current children
|
||||
* with the loaded root node. If no FXML is configured or the path is blank, the method returns
|
||||
* without performing any action.
|
||||
*/
|
||||
private void load() {
|
||||
if (getFxml() == null || getFxml().isBlank()) return;
|
||||
ComponentLoader loader = new ComponentLoader();
|
||||
@@ -37,6 +71,10 @@ public class FxmlComponent extends StackPane {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to inject the {@link #data} object into the controller of the currently loaded child node,
|
||||
* if a controller is present and defines a single-argument method named {@code setData}.
|
||||
*/
|
||||
private void injectData() {
|
||||
if (!getChildren().isEmpty() && getData() != null) {
|
||||
Object controller = getControllerFromChild(getChildren().get(0));
|
||||
@@ -46,22 +84,50 @@ public class FxmlComponent extends StackPane {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configured FXML resource path.
|
||||
*
|
||||
* @return the FXML path or {@code null} if not set
|
||||
*/
|
||||
public String getFxml() {
|
||||
return fxml.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the FXML resource path to load. Changing this value triggers a reload of the component content.
|
||||
*
|
||||
* @param fxml resource path relative to the component class
|
||||
*/
|
||||
public void setFxml(String fxml) {
|
||||
this.fxml.set(fxml);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data object that will be injected into the loaded controller when available.
|
||||
*
|
||||
* @return the data object or {@code null}
|
||||
*/
|
||||
public Object getData() {
|
||||
return data.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the data object to be injected into the controller of the loaded FXML.
|
||||
* Changing the value will attempt injection into the currently loaded controller.
|
||||
*
|
||||
* @param data the data object to inject
|
||||
*/
|
||||
public void setData(Object data) {
|
||||
this.data.set(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses reflection to find a single-argument method named {@code setData} on the controller and
|
||||
* invokes it with the provided {@code dataObject}.
|
||||
*
|
||||
* @param controller the controller instance to inject into
|
||||
* @param dataObject the object to inject
|
||||
*/
|
||||
private void injectDataToController(Object controller, Object dataObject) {
|
||||
// Daten-Objekt per Reflection zuweisen
|
||||
// Beispiel: Controller hat `setData(User data)`
|
||||
@@ -79,6 +145,12 @@ public class FxmlComponent extends StackPane {
|
||||
// Optional: automatisch Binding ausführen (s. u.)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the controller instance from a child node's properties map if present.
|
||||
*
|
||||
* @param node the child node to inspect
|
||||
* @return the controller object stored under the property key {@code "fx:controller"} or {@code null}
|
||||
*/
|
||||
private Object getControllerFromChild(Node node) {
|
||||
if (node.getProperties().containsKey("fx:controller")) {
|
||||
return node.getProperties().get("fx:controller");
|
||||
@@ -86,24 +158,48 @@ public class FxmlComponent extends StackPane {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JavaFX property representing the configured FXML path.
|
||||
*
|
||||
* @return the FXML property
|
||||
*/
|
||||
public StringProperty fxmlProperty() {
|
||||
return fxml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JavaFX property representing the binding direction hint.
|
||||
*
|
||||
* @return the direction property
|
||||
*/
|
||||
public StringProperty directionProperty() {
|
||||
return direction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configured direction string.
|
||||
*
|
||||
* @return the direction hint (e.g., "unidirectional")
|
||||
*/
|
||||
public String getDirection() {
|
||||
return direction.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the direction hint used by higher-level binding logic.
|
||||
*
|
||||
* @param direction textual direction hint
|
||||
*/
|
||||
public void setDirection(String direction) {
|
||||
this.direction.set(direction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JavaFX property representing the data object used for injection.
|
||||
*
|
||||
* @return the data property
|
||||
*/
|
||||
public ObjectProperty<Object> dataProperty() {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,21 +7,22 @@ import javafx.beans.property.StringProperty;
|
||||
* The Binding class represents a connection between a source and a target,
|
||||
* with an associated direction. It is typically used to define a binding
|
||||
* relationship that determines how data flows between these two entities.
|
||||
*
|
||||
* <p>
|
||||
* The class provides three properties:
|
||||
* - direction: Represents the type of the binding. Defaults to "unidirectional".
|
||||
* - source: Represents the source of the binding.
|
||||
* - target: Represents the target of the binding.
|
||||
*
|
||||
* <p>
|
||||
* It uses JavaFX property types, allowing these properties to be observed
|
||||
* for changes.
|
||||
*/
|
||||
public class BindingData {
|
||||
|
||||
/**
|
||||
* Represents the direction of the binding in the {@code Binding} class.
|
||||
* It determines whether the binding is unidirectional or bidirectional.
|
||||
* The default value is "unidirectional".
|
||||
*
|
||||
* <p>
|
||||
* This property is observed for changes, enabling dynamic updates
|
||||
* within the JavaFX property system.
|
||||
*/
|
||||
@@ -34,12 +35,20 @@ public class BindingData {
|
||||
* when the source value is modified.
|
||||
*/
|
||||
private StringProperty source = new SimpleStringProperty();
|
||||
|
||||
/**
|
||||
* Represents the target of the binding in the Binding class.
|
||||
* This property holds the target value, which can be observed for changes.
|
||||
*/
|
||||
private StringProperty target = new SimpleStringProperty();
|
||||
|
||||
/**
|
||||
* Default constructor only
|
||||
*/
|
||||
public BindingData() {
|
||||
// default constructor only
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current direction of the binding.
|
||||
* The direction determines how data flows between
|
||||
@@ -47,37 +56,52 @@ public class BindingData {
|
||||
*
|
||||
* @return the current direction of the binding
|
||||
*/
|
||||
public String getDirection() { return direction.get(); }
|
||||
public String getDirection() {
|
||||
return direction.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the direction of the binding.
|
||||
*
|
||||
* @param dir the new direction to set for the binding
|
||||
*/
|
||||
public void setDirection(String dir) { direction.set(dir); }
|
||||
public void setDirection(String dir) {
|
||||
direction.set(dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current source value of the binding.
|
||||
*
|
||||
* @return the source value as a String.
|
||||
*/
|
||||
public String getSource() { return source.get(); }
|
||||
public String getSource() {
|
||||
return source.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of the source property for this binding.
|
||||
*
|
||||
* @param source the new source value to be set
|
||||
*/
|
||||
public void setSource(String source) { this.source.set(source); }
|
||||
public void setSource(String source) {
|
||||
this.source.set(source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current value of the target property.
|
||||
*
|
||||
* @return the value of the target property as a String.
|
||||
*/
|
||||
public String getTarget() { return target.get(); }
|
||||
public String getTarget() {
|
||||
return target.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the target property of the binding.
|
||||
*
|
||||
* @param target the new value to set for the target property
|
||||
*/
|
||||
public void setTarget(String target) { this.target.set(target); }
|
||||
public void setTarget(String target) {
|
||||
this.target.set(target);
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,9 @@ public class FXMLComponentInstances {
|
||||
/** Map holding instances of all @FXMLComponent classes, indexed by class and its unique superclasses/interfaces. */
|
||||
private final Map<Class<?>, Object> instanceMap = new HashMap<>();
|
||||
|
||||
/** The InjectableComponents instance that provides information about instantiable components. */
|
||||
/**
|
||||
* The InjectableComponents instance that provides information about instantiable components.
|
||||
*/
|
||||
private final ComponentScanner injectableScanner;
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,27 +13,29 @@ import java.util.stream.Stream;
|
||||
* support dependency injection. It uses a parameter map to resolve and supply dependencies to
|
||||
* controller constructors dynamically during instantiation.
|
||||
*
|
||||
* This class simplifies the process of injecting dependencies into JavaFX controllers by analyzing
|
||||
* the constructors of the given controller classes at runtime. It selects the constructor that best
|
||||
* matches the available dependencies in the parameter map and creates an instance of the controller.
|
||||
* <p>This class analyzes available constructors of a requested controller class at runtime and
|
||||
* selects the first constructor whose parameter types are all present in the internal parameter map.
|
||||
* The corresponding instances are retrieved and passed to the constructor to create the controller.
|
||||
*
|
||||
* It implements the Callback interface to provide compatibility with the JavaFX FXMLLoader, allowing
|
||||
* controllers with dependencies to be injected seamlessly during the FXML loading process.
|
||||
* <p>It implements the {@link javafx.util.Callback} interface to integrate with the JavaFX
|
||||
* {@link javafx.fxml.FXMLLoader} controller factory mechanism.
|
||||
*/
|
||||
public class InjectingControllerFactory implements Callback<Class<?>, Object> {
|
||||
|
||||
/**
|
||||
* A map that stores class-to-object mappings used for dependency injection
|
||||
* in controller instantiation. This map is utilized to resolve and supply
|
||||
* the required dependencies for constructors during the creation of controller
|
||||
* instances.
|
||||
*
|
||||
* Each key in the map represents a class type, and the corresponding value
|
||||
* is the instance of that type. This allows the {@link InjectingControllerFactory}
|
||||
* to use the stored instances to dynamically match and inject dependencies
|
||||
* into controllers at runtime.
|
||||
* in controller instantiation. Each key represents a parameter type and the
|
||||
* associated value is the instance to be injected for that type.
|
||||
*/
|
||||
private final Map<Class<?>, Object> parameterMap = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Default constructor only
|
||||
*/
|
||||
public InjectingControllerFactory() {
|
||||
// default constructor only
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a mapping between a class and its corresponding object instance
|
||||
* to the parameter map used for dependency injection.
|
||||
@@ -81,11 +83,11 @@ public class InjectingControllerFactory implements Callback<Class<?>, Object> {
|
||||
* This method checks if the parameter map contains entries for all parameter types required
|
||||
* by the specified constructor.
|
||||
*
|
||||
* @param constructor The constructor to be evaluated for instantiability.
|
||||
* @param constructor The constructor to be evaluated for instantiability.
|
||||
* @param parameterMap A map where keys are parameter types and values are the corresponding
|
||||
* instances available for injection.
|
||||
* @return {@code true} if the constructor can be instantiated with the given parameter map;
|
||||
* {@code false} otherwise.
|
||||
* {@code false} otherwise.
|
||||
*/
|
||||
private boolean canBeInstantiated(Constructor<?> constructor, Map<Class<?>, Object> parameterMap) {
|
||||
return Stream.of(constructor.getParameterTypes()).allMatch(parameterMap::containsKey);
|
||||
|
||||
@@ -11,12 +11,12 @@ import java.net.URL;
|
||||
* The InjectingFXMLLoader class provides a custom implementation of JavaFX's FXMLLoader
|
||||
* that supports dependency injection. It facilitates the loading of FXML files while
|
||||
* dynamically injecting dependencies into controllers during the loading process.
|
||||
*
|
||||
* <p>
|
||||
* This class utilizes the InjectingControllerFactory to enable seamless integration
|
||||
* of dependency injection with JavaFX controllers. Dependencies can be added to the
|
||||
* controller factory, and the loader will use this factory to instantiate controllers
|
||||
* with the appropriate dependencies.
|
||||
*
|
||||
* <p>
|
||||
* Features of this loader include:
|
||||
* - Support for dependency injection into JavaFX controllers by using a custom factory.
|
||||
* - Adding custom dependency mappings at runtime.
|
||||
@@ -28,20 +28,20 @@ public class InjectingFXMLLoader {
|
||||
/**
|
||||
* Represents an instance of the {@link InjectingControllerFactory}, which is used for creating
|
||||
* and managing controller instances with dependency injection capabilities in a JavaFX application.
|
||||
*
|
||||
* <p>
|
||||
* This factory facilitates the injection of dependencies by dynamically resolving and supplying
|
||||
* required objects during controller instantiation. It plays a critical role in enabling seamless
|
||||
* integration of dependency injection with JavaFX's {@link FXMLLoader}.
|
||||
*
|
||||
* <p>
|
||||
* The `controllerFactory` is used internally by the {@link InjectingFXMLLoader} to provide a consistent
|
||||
* and extensible mechanism for controller creation while maintaining loose coupling and enhancing testability.
|
||||
*
|
||||
* <p>
|
||||
* Key responsibilities:
|
||||
* - Manages a mapping of classes to their injectable instances required for controller instantiation.
|
||||
* - Dynamically analyzes and invokes appropriate constructors for controllers based on the availability
|
||||
* of dependencies.
|
||||
* of dependencies.
|
||||
* - Ensures that controllers are created with required dependencies, preventing manual resolution of injections.
|
||||
*
|
||||
* <p>
|
||||
* This variable is initialized in the {@link InjectingFXMLLoader} and can be extended with additional
|
||||
* mappings at runtime using relevant methods.
|
||||
*/
|
||||
@@ -51,7 +51,7 @@ public class InjectingFXMLLoader {
|
||||
* Default constructor for the InjectingFXMLLoader class.
|
||||
* This initializes the loader with a new instance of the {@link InjectingControllerFactory},
|
||||
* which is used to provide dependency injection capabilities for JavaFX controllers.
|
||||
*
|
||||
* <p>
|
||||
* The {@link InjectingControllerFactory} allows for the registration and dynamic injection
|
||||
* of dependencies into controllers when they are instantiated during the FXML loading process.
|
||||
*/
|
||||
@@ -100,7 +100,7 @@ public class InjectingFXMLLoader {
|
||||
* available for dependency injection.
|
||||
*/
|
||||
private void addInjectingData(FXMLComponentInstances instances) {
|
||||
for (var clazz: instances.getInstanceMap().keySet()) {
|
||||
for (var clazz : instances.getInstanceMap().keySet()) {
|
||||
addInjectingData(clazz, instances.getInstance(clazz));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ public class BindingAwareFXMLLoader<T> {
|
||||
* Recursively traverses the scene graph and binds controls to properties
|
||||
* in the ViewModel based on custom metadata in the node properties map.
|
||||
*
|
||||
* @param node the current node to inspect
|
||||
* @param node the current node to inspect
|
||||
* @param viewModel the ViewModel holding the properties
|
||||
*/
|
||||
private void bindNodesRecursively(Node node, GenericViewModel<T> viewModel) {
|
||||
@@ -92,9 +92,9 @@ public class BindingAwareFXMLLoader<T> {
|
||||
* Binds two JavaFX properties according to the specified binding direction.
|
||||
*
|
||||
* @param controlProperty the property of the control (e.g. TextField.textProperty)
|
||||
* @param modelProperty the property from the ViewModel
|
||||
* @param direction the direction of the binding
|
||||
* @param <V> the value type of the property
|
||||
* @param modelProperty the property from the ViewModel
|
||||
* @param direction the direction of the binding
|
||||
* @param <V> the value type of the property
|
||||
*/
|
||||
private <V> void bind(Property<V> controlProperty, Property<V> modelProperty, BindDirection direction) {
|
||||
switch (direction) {
|
||||
|
||||
@@ -12,18 +12,21 @@ import java.util.ResourceBundle;
|
||||
* @param <T> the type of the model
|
||||
*/
|
||||
public class GenericViewController<T> implements Initializable {
|
||||
|
||||
/**
|
||||
* The GenericViewModel instance wrapping the underlying model.
|
||||
*/
|
||||
private GenericViewModel<T> viewModel;
|
||||
|
||||
/**
|
||||
* Sets the model for this controller by wrapping it in a GenericViewModel.
|
||||
* Creates a new controller instance in an uninitialized state.
|
||||
*
|
||||
* @param model the model to be used by the ViewModel
|
||||
* <p>The constructor does not set a model. Call {@link #setModel(Object)} to
|
||||
* attach a model instance before performing view bindings or accessing the view model via
|
||||
* {@link #getViewModel()}.</p>
|
||||
*/
|
||||
public void setModel(T model) {
|
||||
this.viewModel = new GenericViewModel<>(model);
|
||||
public GenericViewController() {
|
||||
// default constructor only ...
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,10 +38,19 @@ public class GenericViewController<T> implements Initializable {
|
||||
return viewModel.getModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the model for this controller by wrapping it in a GenericViewModel.
|
||||
*
|
||||
* @param model the model to be used by the ViewModel
|
||||
*/
|
||||
public void setModel(T model) {
|
||||
this.viewModel = new GenericViewModel<>(model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard JavaFX initialize method. No-op, as actual binding is done externally via FXMLLoader extension.
|
||||
*
|
||||
* @param location the location used to resolve relative paths for the root object, or null if unknown
|
||||
* @param location the location used to resolve relative paths for the root object, or null if unknown
|
||||
* @param resources the resources used to localize the root object, or null if not localized
|
||||
*/
|
||||
@Override
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
package de.neitzel.fx.mvvm;
|
||||
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleLongProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
|
||||
import java.beans.BeanInfo;
|
||||
import java.beans.IntrospectionException;
|
||||
@@ -80,7 +86,7 @@ public class GenericViewModel<T> {
|
||||
/**
|
||||
* Creates an appropriate JavaFX Property instance for the given type and initial value.
|
||||
*
|
||||
* @param type the type of the property (e.g., String.class, int.class)
|
||||
* @param type the type of the property (e.g., String.class, int.class)
|
||||
* @param initialValue the initial value of the property
|
||||
* @return a new JavaFX Property instance
|
||||
*/
|
||||
@@ -98,8 +104,8 @@ public class GenericViewModel<T> {
|
||||
* Retrieves the JavaFX property associated with the given name and type.
|
||||
*
|
||||
* @param propertyClass the expected property type (e.g., StringProperty.class)
|
||||
* @param name the name of the model field
|
||||
* @param <P> the type of the Property
|
||||
* @param name the name of the model field
|
||||
* @param <P> the type of the Property
|
||||
* @return the corresponding JavaFX property
|
||||
* @throws IllegalArgumentException if the property doesn't exist or the type mismatches
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user