Work in progress ...

This commit is contained in:
Konrad Neitzel
2025-11-30 18:58:05 +01:00
parent 9bbddaaae9
commit 5503dd192e
64 changed files with 4855 additions and 4252 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
@@ -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
*/