From eaebe66b0dba52e2dfcafd3bdc869b618659b3fa Mon Sep 17 00:00:00 2001 From: Konrad Neitzel Date: Fri, 30 Oct 2026 18:35:18 +0100 Subject: [PATCH] Remove the replaced JavaFX units so the view library is the only fx API. fx-example still calls those units, so it leaves the reactor until it is migrated. Co-authored-by: Cursor --- .../neitzel/fx/component/AutoViewModel.java | 226 -------------- .../fx/component/ComponentController.java | 31 -- .../neitzel/fx/component/ComponentLoader.java | 295 ------------------ .../fx/component/controls/Binding.java | 67 ---- .../fx/component/controls/FxmlComponent.java | 196 ------------ .../fx/component/model/BindingData.java | 107 ------- .../fx/injectfx/FXMLComponentInstances.java | 138 -------- .../injectfx/InjectingControllerFactory.java | 95 ------ .../fx/injectfx/InjectingFXMLLoader.java | 134 -------- .../de/neitzel/fx/mvvm/BindDirection.java | 50 --- .../fx/mvvm/BindingAwareFXMLLoader.java | 138 -------- .../fx/mvvm/GenericViewController.java | 69 ---- .../de/neitzel/fx/mvvm/GenericViewModel.java | 150 --------- pom.xml | 2 +- 14 files changed, 1 insertion(+), 1697 deletions(-) delete mode 100644 fx/src/main/java/de/neitzel/fx/component/AutoViewModel.java delete mode 100644 fx/src/main/java/de/neitzel/fx/component/ComponentController.java delete mode 100644 fx/src/main/java/de/neitzel/fx/component/ComponentLoader.java delete mode 100644 fx/src/main/java/de/neitzel/fx/component/controls/Binding.java delete mode 100644 fx/src/main/java/de/neitzel/fx/component/controls/FxmlComponent.java delete mode 100644 fx/src/main/java/de/neitzel/fx/component/model/BindingData.java delete mode 100644 fx/src/main/java/de/neitzel/fx/injectfx/FXMLComponentInstances.java delete mode 100644 fx/src/main/java/de/neitzel/fx/injectfx/InjectingControllerFactory.java delete mode 100644 fx/src/main/java/de/neitzel/fx/injectfx/InjectingFXMLLoader.java delete mode 100644 fx/src/main/java/de/neitzel/fx/mvvm/BindDirection.java delete mode 100644 fx/src/main/java/de/neitzel/fx/mvvm/BindingAwareFXMLLoader.java delete mode 100644 fx/src/main/java/de/neitzel/fx/mvvm/GenericViewController.java delete mode 100644 fx/src/main/java/de/neitzel/fx/mvvm/GenericViewModel.java diff --git a/fx/src/main/java/de/neitzel/fx/component/AutoViewModel.java b/fx/src/main/java/de/neitzel/fx/component/AutoViewModel.java deleted file mode 100644 index 3f34725..0000000 --- a/fx/src/main/java/de/neitzel/fx/component/AutoViewModel.java +++ /dev/null @@ -1,226 +0,0 @@ -package de.neitzel.fx.component; - -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 lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.HashMap; -import java.util.Map; - -/** - * 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 the type of the underlying model - */ -@Slf4j -public class AutoViewModel { - - /** - * The wrapped model instance. - * -- GETTER -- - * Retrieves the model associated with this AutoViewModel. - */ - @Getter - private final T model; - - /** - * Map of field name to JavaFX Property instance exposing that field's value. - */ - private final Map> 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(); - } - - /** - * Initializes a mapping of field names to JavaFX properties for the associated model object. - *

- * This method utilizes reflection to iterate through the public no-argument getters - * of the model's class to create corresponding JavaFX properties. It performs the following: - *

- * 1. Identifies methods that adhere to the getter naming conventions (e.g., `getFieldName` or `isFieldName`). - * 2. Maps the field name derived from the getter method to a JavaFX property, creating an appropriate - * property type based on the getter's return type. - * 3. Adds a listener to each property to bind updates from the property back to the model object - * using the corresponding setter method, if available. This ensures bi-directional synchronization - * between the ViewModel and the underlying model. - *

- * Exceptions that may occur during reflective operations (e.g., invoking methods) are caught - * and logged to avoid runtime failures. - */ - private void initProperties() { - for (Method getter : model.getClass().getMethods()) { - if (isGetter(getter)) { - String fieldName = getFieldName(getter); - Object value = invokeGetter(getter); - - Property prop = toProperty(value); - properties.put(fieldName, prop); - - // Bind ViewModel → Model - 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) { - log.warn("Failed to invoke setter for field {}", fieldName, e); - } - } - }); - } - } - } - - /** - * Determines if a given method follows the JavaBean getter convention. - * The method must be public, take no parameters, have a non-void return type, - * and its name should start with either "get" or "is". - * - * @param method the method to be assessed - * @return true if the method adheres to the JavaBean getter convention, false otherwise - */ - private boolean isGetter(Method method) { - return Modifier.isPublic(method.getModifiers()) - && method.getParameterCount() == 0 - && !method.getReturnType().equals(void.class) - && (method.getName().startsWith("get") || method.getName().startsWith("is")); - } - - /** - * Derives the field name from a given Java method by following JavaBean naming conventions. - * The method's name is analyzed to strip the "get" or "is" prefix (if present), - * and the result is converted into a decapitalized field name. - * - * @param method the method whose name is to be processed to derive the field name - * @return the derived field name, or the original method name if no "get" or "is" prefix is found - */ - private String getFieldName(Method method) { - String name = method.getName(); - if (name.startsWith("get")) { - return decapitalize(name.substring(3)); - } else if (name.startsWith("is")) { - return decapitalize(name.substring(2)); - } - return name; - } - - // ========== Hilfsmethoden ========== - - /** - * Invokes the specified method on the model object and returns the result. - * If the method invocation fails, logs a warning and returns null. - * - * @param method the method to be invoked, typically a JavaBean getter - * @return the result of invoking the method, or null if an error occurs - */ - private Object invokeGetter(Method method) { - try { - return method.invoke(model); - } catch (Exception e) { - log.warn("Failed to invoke getter: {}", method.getName(), e); - return null; - } - } - - /** - * Converts an object into an appropriate JavaFX {@link Property} based on its type. - * - * @param value the object to be converted into a JavaFX property; can be of type String, Integer, - * Boolean, Double, Float, Long, or any other object - * @return a JavaFX {@link Property} corresponding to the type of the input object, such as - * {@link SimpleStringProperty} for String, {@link SimpleIntegerProperty} for Integer, - * {@link SimpleBooleanProperty} for Boolean, and so on. If the type is not explicitly - * handled, a {@link SimpleObjectProperty} is returned wrapping the object - */ - 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); - } - - /** - * Finds a setter method in a specified class that corresponds to the given field name and value type. - *

- * The method searches for a public method in the class whose name matches the JavaBean-style setter - * naming convention (e.g., "setFieldName"). The method must accept exactly one parameter, and if a - * value type is provided, the parameter type must be assignable from it. - * - * @param clazz the class to search for the method - * @param fieldName the name of the field for which the setter method is being sought - * @param valueType the expected type of the parameter for the setter method, or null if no specific - * type constraint is required - * @return the setter method if found, or null if no matching method is identified - */ - private Method findSetterFor(Class clazz, String fieldName, Class valueType) { - String setterName = "set" + capitalize(fieldName); - for (Method m : clazz.getMethods()) { - if (m.getName().equals(setterName) && m.getParameterCount() == 1) { - if (valueType == null || m.getParameterTypes()[0].isAssignableFrom(valueType)) { - return m; - } - } - } - return null; - } - - /** - * Converts the first character of the given string to lowercase, leaving the rest of the string unchanged. - * If the input string is null or empty, it is returned as-is. - * - * @param str the string to be decapitalized - * @return the decapitalized string, or the original string if it is null or empty - */ - private String decapitalize(String str) { - if (str == null || str.isEmpty()) return str; - return str.substring(0, 1).toLowerCase() + str.substring(1); - } - - /** - * Capitalizes the first letter of the given string and leaves the rest of the string unchanged. - * If the input string is null or empty, it is returned as is. - * - * @param str the string to be capitalized - * @return the input string with its first character converted to uppercase, or the original string if it is null or empty - */ - private String capitalize(String str) { - if (str == null || str.isEmpty()) return str; - return str.substring(0, 1).toUpperCase() + str.substring(1); - } - - /** - * Retrieves the JavaFX property associated with the given field name. - * - * @param name the name of the field whose associated JavaFX property is to be returned - * @return the JavaFX {@link Property} associated with the specified field name, - * or null if no property exists for the given name - */ - public Property getProperty(String name) { - return properties.get(name); - } -} diff --git a/fx/src/main/java/de/neitzel/fx/component/ComponentController.java b/fx/src/main/java/de/neitzel/fx/component/ComponentController.java deleted file mode 100644 index 37d636f..0000000 --- a/fx/src/main/java/de/neitzel/fx/component/ComponentController.java +++ /dev/null @@ -1,31 +0,0 @@ -package de.neitzel.fx.component; - -import lombok.Getter; - -/** - * Generic controller used by the ComponentLoader to bind FXML views - * to automatically generated ViewModels based on POJO models. - *

- * This controller provides access to the {@link AutoViewModel} - * which contains JavaFX properties derived from the model. - */ -public class ComponentController { - - /** - * The automatically generated ViewModel that holds JavaFX properties - * for all model fields. It is used to create bindings between - * the view and the model. - */ - @Getter - private final AutoViewModel viewModel; - - /** - * Constructs a new ComponentController instance with the specified AutoViewModel. - * - * @param viewModel the AutoViewModel containing JavaFX properties derived from the model, - * used for binding the view to the model. - */ - public ComponentController(AutoViewModel viewModel) { - this.viewModel = viewModel; - } -} diff --git a/fx/src/main/java/de/neitzel/fx/component/ComponentLoader.java b/fx/src/main/java/de/neitzel/fx/component/ComponentLoader.java deleted file mode 100644 index 466fca2..0000000 --- a/fx/src/main/java/de/neitzel/fx/component/ComponentLoader.java +++ /dev/null @@ -1,295 +0,0 @@ -package de.neitzel.fx.component; - -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.fxml.FXMLLoader; -import javafx.scene.Node; -import javafx.scene.Parent; -import lombok.extern.slf4j.Slf4j; -import org.jetbrains.annotations.NotNull; - -import java.io.IOException; -import java.lang.reflect.Method; -import java.net.URL; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -/** - * ComponentLoader is responsible for loading JavaFX FXML components and binding - * them to automatically generated ViewModels based on simple POJO models. - */ -@Slf4j -public class ComponentLoader { - - /** - * The controller object associated with the ComponentLoader. - * It is typically used to manage the behavior and interactions of the UI components - * defined in an FXML file. - */ - private Object controller; - - /** - * Default constructor only - */ - public ComponentLoader() { - // default constructor only - } - - /** - * Loads an FXML file from the specified URL, sets the provided controller, and processes bindings. - * This method parses the FXML file, initializes the specified controller, and evaluates binding controls - * defined within the FXML namespace. - * - * @param the type of the root node, which must extend {@code Parent} - * @param fxmlUrl the URL of the FXML file to be loaded - * @param controller the controller to associate with the FXML file - * @param nothing an unused parameter, included for compatibility - * @return the root node loaded from the FXML file - * @throws IOException if there is an error reading the FXML file - */ - public static T load(URL fxmlUrl, Object controller, @SuppressWarnings("unused") String nothing) throws IOException { - FXMLLoader loader = new FXMLLoader(fxmlUrl); - loader.setController(controller); - T root = loader.load(); - - Map namespace = loader.getNamespace(); - - // Nach allen BindingControls suchen: - List bindingControls = collectAllNodes(root).stream() - .filter(n -> n instanceof Binding) - .map(n -> (Binding) n) - .toList(); - - for (Binding bc : bindingControls) { - evaluateBindings(bc.getBindings(), namespace); - } - - return root; - } - - /** - * Safely binds two properties bidirectionally while ensuring type compatibility. - * If the target property's type is not compatible with the source property's type, the binding will - * not be performed, and an error will be logged. - * - * @param source the source property to bind, must not be null - * @param target the target property to bind to; the type will be checked for compatibility at runtime - * @param the type of the source property's value - */ - @SuppressWarnings("unchecked") - private static void bindBidirectionalSafe(@NotNull Property source, Property target) { - try { - Property targetCasted = (Property) target; - source.bindBidirectional(targetCasted); - } catch (ClassCastException e) { - log.error("⚠️ Typkonflikt beim Binding: {} ⇄ {}", source.getClass(), target.getClass(), e); - } - } - - /** - * Attempts to bind the source property to the target property in a type-safe manner. - * If the target property's type is incompatible with the source property's type, the binding operation - * will be skipped, and an error message will be logged. - * - * @param source the source property to bind, must not be null - * @param target the target property to which the source will be bound - * @param the type of the source property's value - */ - @SuppressWarnings("unchecked") - private static void bindSafe(@NotNull Property source, Property target) { - try { - Property targetCasted = (Property) target; - source.bind(targetCasted); - } catch (ClassCastException e) { - log.error("⚠️ Typkonflikt beim Binding: {} ⇄ {}", source.getClass(), target.getClass(), e); - } - } - - /** - * Evaluates and establishes bindings between source and target properties based on the provided - * bindings list and namespace. Supports unidirectional and bidirectional bindings, ensuring type - * compatibility before binding. Logs errors for unsuccessful binding attempts or exceptions. - * - * @param bindings a list of BindingData objects that define the source, target, and direction for the bindings - * @param namespace a map representing the namespace from which expressions in the bindings are resolved - */ - private static void evaluateBindings(List bindings, Map namespace) { - for (var binding : bindings) { - try { - Object source = resolveExpression(binding.getSource(), namespace); - Object target = resolveExpression(binding.getTarget(), namespace); - - if (source instanceof Property sourceProp && target instanceof Property targetProp) { - - Class sourceType = getPropertyType(sourceProp); - Class targetType = getPropertyType(targetProp); - - boolean bindableForward = targetType.isAssignableFrom(sourceType); - boolean bindableBackward = sourceType.isAssignableFrom(targetType); - - switch (binding.getDirection().toLowerCase()) { - case "bidirectional": - if (bindableForward && bindableBackward) { - bindBidirectionalSafe(sourceProp, targetProp); - } else { - log.error("⚠️ Kann bidirektionales Binding nicht durchführen: Typen inkompatibel ({} ⇄ {})", sourceType, targetType); - } - break; - case "unidirectional": - default: - if (bindableForward) { - bindSafe(sourceProp, targetProp); - } else { - 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); - } - } - } - - /** - * Determines the type of the value held by the specified property. - * - * @param prop the property whose value type is to be determined, must not be null - * @return the class of the property's value type; returns Object.class if the type cannot be determined - */ - private static Class getPropertyType(Property prop) { - try { - Method getter = prop.getClass().getMethod("get"); - return getter.getReturnType(); - } catch (Exception e) { - return Object.class; // Fallback - } - } - - /** - * Resolves a dot-separated expression (e.g., "viewModel.username") by navigating through the provided namespace - * map and invoking the corresponding getter methods to access nested properties. - * - * @param expr the dot-separated expression to resolve, must not be null - * @param namespace a map containing the initial objects to resolve the expression from, must not be null - * @return the value resolved from the specified expression - * @throws Exception if an error occurs while invoking getter methods or resolving the expression - */ - private static Object resolveExpression(@NotNull String expr, @NotNull Map namespace) throws Exception { - // z.B. "viewModel.username" - String[] parts = expr.split("\\."); - Object current = namespace.get(parts[0]); - for (int i = 1; i < parts.length; i++) { - String getter = "get" + Character.toUpperCase(parts[i].charAt(0)) + parts[i].substring(1); - current = current.getClass().getMethod(getter).invoke(current); - } - return current; - } - - /** - * Recursively collects all nodes in a scene graph starting from the given root node. - * - * @param root the starting node from which all descendant nodes will be collected - * @return a list containing all nodes, including the root node and its descendants - */ - private static @NotNull List collectAllNodes(Node root) { - List nodes = new ArrayList<>(); - nodes.add(root); - if (root instanceof Parent parent && !(root instanceof FxmlComponent)) { - for (Node child : parent.getChildrenUnmodifiable()) { - nodes.addAll(collectAllNodes(child)); - } - } - return nodes; - } - - /** - * Returns the controller associated with the ComponentLoader. - * - * @return the controller object - */ - public Object getController() { - return controller; - } - - /** - * Loads an FXML file and returns the root JavaFX node without binding to a data model. - * This method uses the given {@code fxmlPath} to locate and load the FXML file. - * - * @param fxmlPath the URL of the FXML file to be loaded - * @return the root JavaFX node loaded from the specified FXML file - */ - public Parent load(URL fxmlPath) { - return load(null, fxmlPath); - } - - /** - * Loads an FXML file and binds its elements to a generated ViewModel - * based on the given POJO model and specified FXML path. - * - * @param model the data model (POJO) to bind to the UI - * @param fxmlPath the URL path to the FXML file - * @return the root JavaFX node loaded from FXML - * @throws RuntimeException if the FXML could not be loaded - */ - 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); - } - } - - /** - * Loads an FXML file and returns its root node. - * - * @param fxmlPath the relative path to the FXML file - * @return the root JavaFX node loaded from the FXML file - */ - 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. - * - * @param node the UI node (e.g., TextField) - * @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)) { - tf.textProperty().bindBidirectional(sp); - } else if ("read".equalsIgnoreCase(direction)) { - tf.textProperty().bind(sp); - } - } - // Additional control types (e.g., CheckBox, ComboBox) can be added here - } -} diff --git a/fx/src/main/java/de/neitzel/fx/component/controls/Binding.java b/fx/src/main/java/de/neitzel/fx/component/controls/Binding.java deleted file mode 100644 index ffafadd..0000000 --- a/fx/src/main/java/de/neitzel/fx/component/controls/Binding.java +++ /dev/null @@ -1,67 +0,0 @@ -package de.neitzel.fx.component.controls; - -import de.neitzel.fx.component.model.BindingData; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import javafx.scene.layout.Region; - -/** - * The BindingControl class represents a UI control that manages a list - * of {@link BindingData} objects. It extends the {@link Region} class and - * provides functionality to bind and monitor connections between source - * and target properties. - *

- * The primary purpose of this control is to maintain an observable list - * of bindings, allowing developers to track or adjust the linked properties - * dynamically. - *

- * The internal list of bindings is implemented as an {@link ObservableList}, - * allowing property change notifications to be easily monitored for UI - * updates or other reactive behaviors. - *

- * This class serves as an organizational component and does not provide - * any user interaction by default. - */ -public class Binding extends Region { - - /** - * Represents an observable list of {@link BindingData} objects contained within the - * {@link Binding} instance. This list is utilized to manage and monitor - * the bindings between source and target properties dynamically. - *

- * The list is implemented as an {@link ObservableList}, allowing changes in the - * collection to be observed and reacted to, such as triggering UI updates or - * responding to binding modifications. - *

- * This field is initialized as an empty list using {@link FXCollections#observableArrayList()}. - * It is declared as final to ensure its reference cannot be changed, while the - * contents of the list remain mutable. - */ - private final ObservableList bindings = FXCollections.observableArrayList(); - - /** - * Constructs a new instance of the BindingControl class. - *

- * This default constructor initializes the BindingControl without - * any pre-configured bindings. The instance will contain an empty - * {@link ObservableList} of {@link BindingData} objects, which can later - * be populated as needed. - *

- * The constructor does not perform additional setup or initialization, - * allowing the class to be extended or customized as necessary. - */ - public Binding() { - // Empty, the ComponentLoader is used to work on the bindings. - } - - /** - * Retrieves the observable list of {@code Binding} objects associated with this control. - * The returned list allows monitoring and management of the bindings maintained - * by the {@code BindingControl}. - * - * @return an {@code ObservableList} containing the bindings managed by this control - */ - public ObservableList getBindings() { - return bindings; - } -} \ No newline at end of file diff --git a/fx/src/main/java/de/neitzel/fx/component/controls/FxmlComponent.java b/fx/src/main/java/de/neitzel/fx/component/controls/FxmlComponent.java deleted file mode 100644 index 70c9617..0000000 --- a/fx/src/main/java/de/neitzel/fx/component/controls/FxmlComponent.java +++ /dev/null @@ -1,196 +0,0 @@ -package de.neitzel.fx.component.controls; - -import de.neitzel.fx.component.ComponentLoader; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; -import javafx.scene.Node; -import javafx.scene.Parent; -import javafx.scene.layout.StackPane; - -import java.util.Arrays; - -/** - * A reusable JavaFX component that loads and embeds an FXML-defined UI fragment. - *

- * The component exposes three properties: - *

    - *
  • fxml: the resource path to the FXML file to load (relative to the component class)
  • - *
  • direction: a textual hint that describes binding directionality (default "unidirectional")
  • - *
  • data: an arbitrary object that will be injected into the loaded controller if a suitable setter exists
  • - *
- *

- * 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 { - - private final StringProperty fxml = new SimpleStringProperty(); - - private final StringProperty direction = new SimpleStringProperty("unidirectional"); - - private final ObjectProperty data = new SimpleObjectProperty<>(); - - /** - * Creates a new instance of {@code FxmlComponent}. - *

- * 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(); - Parent content = loader.load(getClass().getResource(getFxml())); - - getChildren().setAll(content); - - Object controller = loader.getController(); - if (controller != null && getData() != null) { - injectDataToController(controller, getData()); - } - } - - /** - * 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)); - if (controller != null) { - injectDataToController(controller, getData()); - } - } - } - - /** - * 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)` - Arrays.stream(controller.getClass().getMethods()) - .filter(m -> m.getName().equals("setData") && m.getParameterCount() == 1) - .findFirst() - .ifPresent(method -> { - try { - method.invoke(controller, dataObject); - } catch (Exception e) { - e.printStackTrace(); - } - }); - - // 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"); - } - 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 dataProperty() { - return data; - } -} diff --git a/fx/src/main/java/de/neitzel/fx/component/model/BindingData.java b/fx/src/main/java/de/neitzel/fx/component/model/BindingData.java deleted file mode 100644 index de3bdef..0000000 --- a/fx/src/main/java/de/neitzel/fx/component/model/BindingData.java +++ /dev/null @@ -1,107 +0,0 @@ -package de.neitzel.fx.component.model; - -import javafx.beans.property.SimpleStringProperty; -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. - *

- * 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. - *

- * 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". - *

- * This property is observed for changes, enabling dynamic updates - * within the JavaFX property system. - */ - private StringProperty direction = new SimpleStringProperty("unidirectional"); - - /** - * Represents the source of the binding. This property holds a string value - * that specifies the originating object or identifier in the binding connection. - * It can be observed for changes, allowing updates to the binding relationship - * 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 - * the source and the target, and typically defaults to "unidirectional". - * - * @return the current direction of the binding - */ - 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); - } - - /** - * Gets the current source value of the binding. - * - * @return the source value as a String. - */ - 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); - } - - /** - * Retrieves the current value of the target property. - * - * @return the value of the target property as a String. - */ - 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); - } -} \ No newline at end of file diff --git a/fx/src/main/java/de/neitzel/fx/injectfx/FXMLComponentInstances.java b/fx/src/main/java/de/neitzel/fx/injectfx/FXMLComponentInstances.java deleted file mode 100644 index cee8917..0000000 --- a/fx/src/main/java/de/neitzel/fx/injectfx/FXMLComponentInstances.java +++ /dev/null @@ -1,138 +0,0 @@ -package de.neitzel.fx.injectfx; - -import de.neitzel.injection.ComponentData; -import de.neitzel.injection.ComponentScanner; -import lombok.Getter; - -import java.lang.reflect.Constructor; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; - -/** - * Manages the creation and storage of singleton instances for all @FXMLComponent classes. - * It ensures that each component is instantiated only once and resolves dependencies recursively. - */ -public class FXMLComponentInstances { - - @Getter - /** Map holding instances of all @FXMLComponent classes, indexed by class and its unique superclasses/interfaces. */ - private final Map, Object> instanceMap = new HashMap<>(); - - /** - * The InjectableComponents instance that provides information about instantiable components. - */ - private final ComponentScanner injectableScanner; - - /** - * Constructs an FXMLComponentInstances manager and initializes all component instances. - * - * @param injectableScanner The InjectableComponents instance containing resolved component types. - */ - public FXMLComponentInstances(ComponentScanner injectableScanner) { - this.injectableScanner = injectableScanner; - createAllInstances(); - } - - /** - * Creates instances for all registered @FXMLComponent classes. - */ - private void createAllInstances() { - for (Class componentClass : injectableScanner.getInstantiableComponents().keySet()) { - getInstance(componentClass); // Ensures each component is instantiated once - } - } - - /** - * Retrieves an instance of a given component class, creating it if necessary. - * - * @param componentClass The class for which an instance is needed. - * @return An instance of the requested class. - */ - public Object getInstance(Class componentClass) { - if (instanceMap.containsKey(componentClass)) { - return instanceMap.get(componentClass); - } - - Class concreteClass = injectableScanner.getInstantiableComponents().get(componentClass).getType(); - if (concreteClass == null) { - throw new IllegalStateException("No concrete implementation found for: " + componentClass.getName()); - } - - Object instance = createInstance(concreteClass); - registerInstance(concreteClass, instance); - - return instance; - } - - /** - * Creates an instance of the given class by selecting the best constructor. - * - * @param concreteClass The class to instantiate. - * @return A new instance of the class. - */ - private Object createInstance(Class concreteClass) { - try { - Constructor bestConstructor = findBestConstructor(concreteClass); - if (bestConstructor == null) { - throw new IllegalStateException("No suitable constructor found for: " + concreteClass.getName()); - } - - Class[] paramTypes = bestConstructor.getParameterTypes(); - Object[] params = Arrays.stream(paramTypes) - .map(this::getInstance) // Recursively resolve dependencies - .toArray(); - - return bestConstructor.newInstance(params); - - } catch (Exception e) { - throw new RuntimeException("Failed to instantiate " + concreteClass.getName(), e); - } - } - - /** - * Finds the best constructor for a given class by checking which one has resolvable parameters. - * - * @param concreteClass The class to analyze. - * @return The best constructor or {@code null} if none can be used. - */ - private Constructor findBestConstructor(Class concreteClass) { - Constructor[] constructors = concreteClass.getConstructors(); - - // Prefer constructors with all parameters resolvable - for (Constructor constructor : constructors) { - if (canUseConstructor(constructor)) { - return constructor; - } - } - - return null; - } - - /** - * Checks whether a given constructor can be used by ensuring all parameters are known. - * - * @param constructor The constructor to check. - * @return {@code true} if all parameters can be resolved, otherwise {@code false}. - */ - private boolean canUseConstructor(Constructor constructor) { - return Arrays.stream(constructor.getParameterTypes()) - .allMatch(injectableScanner.getInstantiableComponents()::containsKey); - } - - /** - * Registers an instance in the map for all unique interfaces and superclasses it implements. - * - * @param concreteClass The concrete component class. - * @param instance The instance to store. - */ - private void registerInstance(Class concreteClass, Object instance) { - instanceMap.put(concreteClass, instance); - - for (Map.Entry, ComponentData> entry : injectableScanner.getInstantiableComponents().entrySet()) { - if (entry.getValue().getType().equals(concreteClass)) { - instanceMap.put(entry.getKey(), instance); - } - } - } -} \ No newline at end of file diff --git a/fx/src/main/java/de/neitzel/fx/injectfx/InjectingControllerFactory.java b/fx/src/main/java/de/neitzel/fx/injectfx/InjectingControllerFactory.java deleted file mode 100644 index 2e6f12b..0000000 --- a/fx/src/main/java/de/neitzel/fx/injectfx/InjectingControllerFactory.java +++ /dev/null @@ -1,95 +0,0 @@ -package de.neitzel.fx.injectfx; - -import javafx.util.Callback; - -import java.lang.reflect.Constructor; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Stream; - -/** - * The InjectingControllerFactory is responsible for creating controller instances for JavaFX that - * support dependency injection. It uses a parameter map to resolve and supply dependencies to - * controller constructors dynamically during instantiation. - * - *

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 {@link javafx.util.Callback} interface to integrate with the JavaFX - * {@link javafx.fxml.FXMLLoader} controller factory mechanism. - */ -public class InjectingControllerFactory implements Callback, Object> { - - /** - * A map that stores class-to-object mappings used for dependency injection - * 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, 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. - * - * @param clazz The class type to be associated with the provided object instance. - * @param object The object instance to be injected for the specified class type. - */ - public void addInjectingData(Class clazz, Object object) { - parameterMap.put(clazz, object); - } - - /** - * Creates an instance of a controller class using a constructor that matches the dependencies - * defined in the parameter map. The method dynamically analyzes the constructors of the given - * class and attempts to instantiate the class by injecting required dependencies. - * - * @param controllerClass the class of the controller to be instantiated - * @return an instance of the specified controller class - * @throws RuntimeException if an error occurs while creating the controller instance or if no suitable constructor is found - */ - @Override - public Object call(Class controllerClass) { - try { - Optional> bestConstructor = Stream.of(controllerClass.getConstructors()) - .filter(constructor -> canBeInstantiated(constructor, parameterMap)) - .findFirst(); - - if (bestConstructor.isPresent()) { - Constructor constructor = bestConstructor.get(); - Object[] parameters = Stream.of(constructor.getParameterTypes()) - .map(parameterMap::get) - .toArray(); - - return constructor.newInstance(parameters); - } else { - throw new IllegalStateException("Kein passender Konstruktor gefunden für " + controllerClass.getName()); - } - } catch (Exception e) { - throw new RuntimeException("Fehler beim Erstellen der Controller-Instanz für " + controllerClass.getName(), e); - } - } - - /** - * Determines if a given constructor can be instantiated using the provided parameter map. - * 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 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. - */ - private boolean canBeInstantiated(Constructor constructor, Map, Object> parameterMap) { - return Stream.of(constructor.getParameterTypes()).allMatch(parameterMap::containsKey); - } -} diff --git a/fx/src/main/java/de/neitzel/fx/injectfx/InjectingFXMLLoader.java b/fx/src/main/java/de/neitzel/fx/injectfx/InjectingFXMLLoader.java deleted file mode 100644 index 6c96fc2..0000000 --- a/fx/src/main/java/de/neitzel/fx/injectfx/InjectingFXMLLoader.java +++ /dev/null @@ -1,134 +0,0 @@ -package de.neitzel.fx.injectfx; - -import de.neitzel.injection.ComponentScanner; -import javafx.fxml.FXMLLoader; -import lombok.extern.slf4j.Slf4j; - -import java.io.IOException; -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. - *

- * 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. - *

- * Features of this loader include: - * - Support for dependency injection into JavaFX controllers by using a custom factory. - * - Adding custom dependency mappings at runtime. - * - Scanning and initializing injectable components from a specified package. - */ -@Slf4j -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. - *

- * 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}. - *

- * 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. - *

- * 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. - * - Ensures that controllers are created with required dependencies, preventing manual resolution of injections. - *

- * This variable is initialized in the {@link InjectingFXMLLoader} and can be extended with additional - * mappings at runtime using relevant methods. - */ - private final InjectingControllerFactory controllerFactory; - - /** - * 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. - *

- * The {@link InjectingControllerFactory} allows for the registration and dynamic injection - * of dependencies into controllers when they are instantiated during the FXML loading process. - */ - public InjectingFXMLLoader() { - controllerFactory = new InjectingControllerFactory(); - } - - /** - * Constructs a new instance of the InjectingFXMLLoader class with a specified - * InjectingControllerFactory for dependency injection. This enables the FXMLLoader - * to use the provided controller factory to instantiate controllers with their - * required dependencies during the FXML loading process. - * - * @param controllerFactory The controller factory responsible for creating controller - * instances with dependency injection support. - */ - public InjectingFXMLLoader(InjectingControllerFactory controllerFactory) { - this.controllerFactory = controllerFactory; - } - - /** - * Constructs an instance of `InjectingFXMLLoader` and initializes the component scanner - * for dependency injection based on components found within a specified package. - * The scanned components are analyzed and their instances are prepared for injection - * using the internal `InjectingControllerFactory`. - * - * @param packageName The package name to be scanned for injectable components. - * Classes within the specified package will be identified - * as potential dependencies and made available for injection - * into JavaFX controllers during FXML loading. - */ - public InjectingFXMLLoader(String packageName) { - controllerFactory = new InjectingControllerFactory(); - ComponentScanner scanner = new ComponentScanner(packageName); - FXMLComponentInstances instances = new FXMLComponentInstances(scanner); - addInjectingData(instances); - } - - /** - * Adds all injectable data from the given {@code FXMLComponentInstances} to the controller factory. - * Iterates through the classes in the instance map and delegates adding each class-instance pair - * to the {@link #addInjectingData(Class, Object)} method. - * - * @param instances An {@code FXMLComponentInstances} object containing the mapping of classes - * to their respective singleton instances. This data represents the components - * available for dependency injection. - */ - private void addInjectingData(FXMLComponentInstances instances) { - for (var clazz : instances.getInstanceMap().keySet()) { - addInjectingData(clazz, instances.getInstance(clazz)); - } - } - - /** - * Adds a specific class-object mapping to the controller factory for dependency injection. - * This method allows the association of a given class type with its corresponding instance, - * enabling dependency injection into controllers during the FXML loading process. - * - * @param clazz The class type to be associated with the provided object instance. - * @param object The object instance to be injected for the specified class type. - */ - public void addInjectingData(Class clazz, Object object) { - controllerFactory.addInjectingData(clazz, object); - } - - /** - * Loads an FXML resource from the provided URL and injects dependencies into its controller - * using the configured {@code InjectingControllerFactory}. - * - * @param url the URL of the FXML file to be loaded - * @param the type of the root element defined in the FXML - * @return the root element of the FXML file - * @throws IOException if an error occurs during the loading of the FXML file - */ - public T load(URL url) throws IOException { - FXMLLoader loader = new FXMLLoader(url); - loader.setControllerFactory(controllerFactory); - return loader.load(); - } -} diff --git a/fx/src/main/java/de/neitzel/fx/mvvm/BindDirection.java b/fx/src/main/java/de/neitzel/fx/mvvm/BindDirection.java deleted file mode 100644 index 5bd9a41..0000000 --- a/fx/src/main/java/de/neitzel/fx/mvvm/BindDirection.java +++ /dev/null @@ -1,50 +0,0 @@ -package de.neitzel.fx.mvvm; - -/** - * Enum representing the direction of data binding between a JavaFX control and a ViewModel property. - * - READ: one-way binding from ViewModel to Control. - * - WRITE: one-way binding from Control to ViewModel. - * - BIDIRECTIONAL: two-way binding between Control and ViewModel. - */ -public enum BindDirection { - /** - * One-way binding from the ViewModel to the JavaFX control. - * The control reflects changes in the ViewModel but does not update it. - */ - READ, - - /** - * One-way binding from the JavaFX control to the ViewModel. - * The ViewModel is updated when the control changes, but not vice versa. - */ - WRITE, - - /** - * Two-way binding between the JavaFX control and the ViewModel. - * Changes in either are reflected in the other. - */ - BIDIRECTIONAL; - - /** - * Parses a string to determine the corresponding BindDirection. - * Defaults to BIDIRECTIONAL if the input is null or empty. - * - * @param value the string representation of the direction - * @return the corresponding BindDirection enum constant - * @throws IllegalArgumentException if the string does not match any valid direction - */ - public static BindDirection fromString(String value) { - if (value == null || value.isEmpty()) { - return BIDIRECTIONAL; - } - - for (BindDirection direction : BindDirection.values()) { - if (direction.name().equalsIgnoreCase(value)) { - return direction; - } - } - - throw new IllegalArgumentException("Not a valid direction: " + value); - } - -} diff --git a/fx/src/main/java/de/neitzel/fx/mvvm/BindingAwareFXMLLoader.java b/fx/src/main/java/de/neitzel/fx/mvvm/BindingAwareFXMLLoader.java deleted file mode 100644 index 04c4a56..0000000 --- a/fx/src/main/java/de/neitzel/fx/mvvm/BindingAwareFXMLLoader.java +++ /dev/null @@ -1,138 +0,0 @@ -package de.neitzel.fx.mvvm; - -import javafx.beans.property.Property; -import javafx.beans.property.StringProperty; -import javafx.fxml.FXMLLoader; -import javafx.scene.Node; -import javafx.scene.Parent; -import javafx.scene.control.TextInputControl; - -import java.io.InputStream; - -/** - * Custom FXMLLoader that binds JavaFX controls to a GenericViewModel using metadata from FXML. - * It supports automatic binding setup based on properties defined in the FXML's node properties. - * - * @param the type of the model used in the ViewModel - */ -public class BindingAwareFXMLLoader { - - /** - * The model instance used to construct the GenericViewModel. - */ - private final T model; - - /** - * Constructs a new instance of the BindingAwareFXMLLoader class. - * This loader is designed to load FXML files and automatically bind UI elements - * to the properties of the provided model using a binding-aware approach. - * - * @param model the model object to be used for binding to the UI components - */ - public BindingAwareFXMLLoader(T model) { - this.model = model; - } - - /** - * Loads an FXML file and performs automatic binding setup using the GenericViewModel. - * - * @param fxml the input stream of the FXML file - * @return the root node of the loaded scene graph - * @throws Exception if loading the FXML or binding fails - */ - public Parent load(InputStream fxml) throws Exception { - FXMLLoader loader = new FXMLLoader(); - loader.setControllerFactory(param -> { - GenericViewController controller = new GenericViewController<>(); - controller.setModel(model); - return controller; - }); - - Parent root = loader.load(fxml); - - GenericViewController controller = loader.getController(); - GenericViewModel viewModel = controller.getViewModel(); - - // Traverse Nodes and evaluate binding info - bindNodesRecursively(root, viewModel); - - return root; - } - - /** - * 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 viewModel the ViewModel holding the properties - */ - private void bindNodesRecursively(Node node, GenericViewModel viewModel) { - if (node instanceof Parent parent) { - for (Node child : parent.getChildrenUnmodifiable()) { - bindNodesRecursively(child, viewModel); - } - } - - Object userData = node.getProperties().get("bind:property"); - if (userData instanceof String propertyName) { - BindDirection direction = getDirection(node); - - switch (node) { - case TextInputControl control -> { - Property prop = viewModel.property(StringProperty.class, propertyName); - bind(control.textProperty(), prop, direction); - } - case javafx.scene.control.Label label -> { - Property prop = viewModel.property(StringProperty.class, propertyName); - bind(label.textProperty(), prop, direction); - } - case javafx.scene.control.CheckBox checkBox -> { - Property prop = viewModel.property(javafx.beans.property.BooleanProperty.class, propertyName); - bind(checkBox.selectedProperty(), prop, direction); - } - case javafx.scene.control.Slider slider -> { - Property prop = viewModel.property(javafx.beans.property.DoubleProperty.class, propertyName); - bind(slider.valueProperty(), prop, direction); - } - case javafx.scene.control.DatePicker datePicker -> { - @SuppressWarnings("unchecked") - Property prop = - (Property) viewModel.property( - javafx.beans.property.ObjectProperty.class, - propertyName - ); - bind(datePicker.valueProperty(), prop, direction); - } - default -> { - // ignore unsupported nodes - } - } - } - } - - /** - * 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 the value type of the property - */ - private void bind(Property controlProperty, Property modelProperty, BindDirection direction) { - switch (direction) { - case BIDIRECTIONAL -> controlProperty.bindBidirectional(modelProperty); - case READ -> controlProperty.bind(modelProperty); - case WRITE -> modelProperty.bind(controlProperty); - } - } - - /** - * Retrieves the binding direction for a node based on its "bind:direction" property. - * - * @param node the node whose direction property is read - * @return the BindDirection specified or a default - */ - private BindDirection getDirection(final Node node) { - return BindDirection.fromString((String) node.getProperties().get("bind:direction")); - } -} \ No newline at end of file diff --git a/fx/src/main/java/de/neitzel/fx/mvvm/GenericViewController.java b/fx/src/main/java/de/neitzel/fx/mvvm/GenericViewController.java deleted file mode 100644 index 537ecda..0000000 --- a/fx/src/main/java/de/neitzel/fx/mvvm/GenericViewController.java +++ /dev/null @@ -1,69 +0,0 @@ -package de.neitzel.fx.mvvm; - -import javafx.fxml.Initializable; - -import java.net.URL; -import java.util.ResourceBundle; - -/** - * A generic JavaFX controller that holds a GenericViewModel for the given model type. - * Intended for use with a custom FXMLLoader that performs automatic property bindings. - * - * @param the type of the model - */ -public class GenericViewController implements Initializable { - - /** - * The GenericViewModel instance wrapping the underlying model. - */ - private GenericViewModel viewModel; - - /** - * Creates a new controller instance in an uninitialized state. - * - *

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()}.

- */ - public GenericViewController() { - // default constructor only ... - } - - /** - * Retrieves the original model instance from the ViewModel. - * - * @return the model instance - */ - public T getModel() { - 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 resources the resources used to localize the root object, or null if not localized - */ - @Override - public void initialize(URL location, ResourceBundle resources) { - // Real binding happens in FXMLLoader extension - } - - /** - * Returns the GenericViewModel used by this controller. - * - * @return the GenericViewModel instance - */ - public GenericViewModel getViewModel() { - return viewModel; - } -} diff --git a/fx/src/main/java/de/neitzel/fx/mvvm/GenericViewModel.java b/fx/src/main/java/de/neitzel/fx/mvvm/GenericViewModel.java deleted file mode 100644 index beedc94..0000000 --- a/fx/src/main/java/de/neitzel/fx/mvvm/GenericViewModel.java +++ /dev/null @@ -1,150 +0,0 @@ -package de.neitzel.fx.mvvm; - -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; -import java.beans.Introspector; -import java.beans.PropertyDescriptor; -import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * A generic ViewModel for JavaFX using reflection to automatically create properties for model attributes. - * Supports binding between JavaFX controls and the model via JavaFX properties. - * - * @param the type of the underlying model - */ -public class GenericViewModel { - /** - * The original model instance. - */ - private final T model; - - /** - * A map holding JavaFX properties corresponding to the model's fields. - */ - private final Map> properties = new HashMap<>(); - - /** - * Constructs a GenericViewModel for the given model instance. - * - * @param model the model to wrap - */ - public GenericViewModel(T model) { - this.model = model; - initProperties(); - } - - /** - * Initializes JavaFX properties for all readable and writable model fields using reflection. - */ - private void initProperties() { - Class clazz = model.getClass(); - for (PropertyDescriptor pd : getPropertyDescriptors(clazz)) { - Method getter = pd.getReadMethod(); - Method setter = pd.getWriteMethod(); - String name = pd.getName(); - - if (getter != null && setter != null) { - Class propertyType = pd.getPropertyType(); - try { - Object value = getter.invoke(model); - Property property = createJavaFXProperty(propertyType, value); - properties.put(name, property); - } catch (Exception e) { - throw new RuntimeException("Failed to initialize property: " + name, e); - } - } - } - } - - /** - * Retrieves all readable/writable property descriptors of the given class. - * - * @param clazz the class to introspect - * @return a list of property descriptors - */ - private List getPropertyDescriptors(Class clazz) { - try { - BeanInfo info = Introspector.getBeanInfo(clazz, Object.class); - return Arrays.asList(info.getPropertyDescriptors()); - } catch (IntrospectionException e) { - throw new RuntimeException("Cannot introspect model class: " + clazz, e); - } - } - - /** - * 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 initialValue the initial value of the property - * @return a new JavaFX Property instance - */ - private Property createJavaFXProperty(Class type, Object initialValue) { - if (type == String.class) return new SimpleStringProperty((String) initialValue); - if (type == int.class || type == Integer.class) return new SimpleIntegerProperty((Integer) initialValue); - if (type == double.class || type == Double.class) return new SimpleDoubleProperty((Double) initialValue); - if (type == boolean.class || type == Boolean.class) return new SimpleBooleanProperty((Boolean) initialValue); - if (type == long.class || type == Long.class) return new SimpleLongProperty((Long) initialValue); - // Fallback for unsupported types: - return new SimpleObjectProperty<>(initialValue); - } - - /** - * 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

the type of the Property - * @return the corresponding JavaFX property - * @throws IllegalArgumentException if the property doesn't exist or the type mismatches - */ - @SuppressWarnings("unchecked") - public

> P property(Class

propertyClass, String name) { - Property prop = properties.get(name); - if (prop == null) { - throw new IllegalArgumentException("No property found for name: " + name); - } - if (!propertyClass.isInstance(prop)) { - throw new IllegalArgumentException("Property type mismatch: expected " + propertyClass.getSimpleName()); - } - return (P) prop; - } - - /** - * Writes the current values of all properties back into the model using their respective setters. - */ - public void save() { - for (PropertyDescriptor pd : getPropertyDescriptors(model.getClass())) { - String name = pd.getName(); - Method setter = pd.getWriteMethod(); - if (setter != null && properties.containsKey(name)) { - try { - Object value = properties.get(name).getValue(); - setter.invoke(model, value); - } catch (Exception e) { - throw new RuntimeException("Failed to save property: " + name, e); - } - } - } - } - - /** - * Returns the wrapped model instance. - * - * @return the model - */ - public T getModel() { - return model; - } -} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 3339dba..71a597f 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ gson net log4j - fx-example + quarkus