> properties = new HashMap<>();
+
+ public AutoViewModel(T model) {
+ this.model = model;
+ initProperties();
+ }
+
+ 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) -> {
+ Method setter = findSetterFor(model.getClass(), fieldName, newVal != null ? newVal.getClass() : null);
+ if (setter != null) {
+ try {
+ setter.invoke(model, newVal);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ });
+ }
+ }
+ }
+
+ 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
+ && !method.getReturnType().equals(void.class)
+ && (method.getName().startsWith("get") || method.getName().startsWith("is"));
+ }
+
+ 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;
+ }
+
+ private String decapitalize(String str) {
+ if (str == null || str.isEmpty()) return str;
+ return str.substring(0, 1).toLowerCase() + str.substring(1);
+ }
+
+ private Object invokeGetter(Method method) {
+ try {
+ return method.invoke(model);
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ 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;
+ }
+
+ 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);
+ }
+}
diff --git a/injectfx-lib/src/main/java/de/neitzel/neitzelfx/component/ComponentController.java b/injectfx-lib/src/main/java/de/neitzel/neitzelfx/component/ComponentController.java
new file mode 100644
index 0000000..45809af
--- /dev/null
+++ b/injectfx-lib/src/main/java/de/neitzel/neitzelfx/component/ComponentController.java
@@ -0,0 +1,23 @@
+package de.neitzel.neitzelfx.component;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * 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.
+ */
+@RequiredArgsConstructor
+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;
+}
diff --git a/injectfx-lib/src/main/java/de/neitzel/neitzelfx/component/ComponentLoader.java b/injectfx-lib/src/main/java/de/neitzel/neitzelfx/component/ComponentLoader.java
new file mode 100644
index 0000000..2feeda2
--- /dev/null
+++ b/injectfx-lib/src/main/java/de/neitzel/neitzelfx/component/ComponentLoader.java
@@ -0,0 +1,188 @@
+package de.neitzel.neitzelfx.component;
+
+import javafx.beans.property.Property;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Parent;
+import javafx.scene.layout.Pane;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * ComponentLoader is responsible for loading JavaFX FXML components and binding
+ * them to automatically generated ViewModels based on simple POJO models.
+ *
+ * It parses custom NFX attributes in FXML to bind UI elements to properties in the ViewModel,
+ * and supports recursive loading of subcomponents.
+ */
+public class ComponentLoader {
+ private Map> nfxBindingMap = new HashMap<>();
+
+ /**
+ * 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) {
+ try {
+ AutoViewModel> viewModel = new AutoViewModel<>(model);
+ String cleanedUri = preprocessNfxAttributes(fxmlPath);
+ FXMLLoader loader = new FXMLLoader(new URL(cleanedUri));
+ loader.setControllerFactory(type -> new ComponentController(viewModel));
+ Parent root = loader.load();
+ processNfxBindings(root, viewModel, loader);
+ return root;
+ } catch (IOException e) {
+ throw new RuntimeException("unable to load fxml: " + fxmlPath, e);
+ }
+ }
+
+ /**
+ * Processes all UI elements for NFX binding attributes and applies
+ * the appropriate bindings to the ViewModel.
+ *
+ * @param root the root node of the loaded FXML hierarchy
+ * @param viewModel the generated ViewModel to bind against
+ */
+ private void processNfxBindings(Parent root, AutoViewModel> viewModel, FXMLLoader loader) {
+ walkNodes(root, node -> {
+ var nfx = lookupNfxAttributes(node, loader);
+ String target = nfx.get("nfx:target");
+ String direction = nfx.get("nfx:direction");
+ String source = nfx.get("nfx:source");
+
+ if (target != null && direction != null) {
+ Property> vmProp = viewModel.getProperty(target);
+ bindNodeToProperty(node, vmProp, direction);
+ }
+
+ if (source != null) {
+ Object subModel = ((Property>) viewModel.getProperty(target)).getValue();
+ Parent subComponent = load(subModel, source);
+ if (node instanceof Pane pane) {
+ pane.getChildren().setAll(subComponent);
+ }
+ }
+ });
+ }
+
+ /**
+ * Recursively walks all nodes in the scene graph starting from the root,
+ * applying the given consumer to each node.
+ *
+ * @param root the starting node
+ * @param consumer the consumer to apply to each node
+ */
+ private void walkNodes(Parent root, java.util.function.Consumer consumer) {
+ consumer.accept(root);
+ if (root instanceof Pane pane) {
+ for (javafx.scene.Node child : pane.getChildren()) {
+ if (child instanceof Parent p) {
+ walkNodes(p, consumer);
+ } else {
+ consumer.accept(child);
+ }
+ }
+ }
+ }
+
+ /**
+ * Extracts custom NFX attributes from a node's properties map.
+ * These attributes are expected to be in the format "nfx:..." and hold string values.
+ *
+ * @param node the node to inspect
+ * @return a map of NFX attribute names to values
+ */
+ private Map extractNfxAttributes(javafx.scene.Node node) {
+ Map result = new HashMap<>();
+ node.getProperties().forEach((k, v) -> {
+ if (k instanceof String key && key.startsWith("nfx:") && v instanceof String value) {
+ result.put(key, value);
+ }
+ });
+ return result;
+ }
+
+ /**
+ * 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")
+ */
+ 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
+ }
+
+ private String preprocessNfxAttributes(String fxmlPath) {
+ try {
+ nfxBindingMap.clear();
+ var factory = javax.xml.parsers.DocumentBuilderFactory.newInstance();
+ var builder = factory.newDocumentBuilder();
+ var doc = builder.parse(getClass().getResourceAsStream(fxmlPath));
+ var all = doc.getElementsByTagName("*");
+
+ int autoId = 0;
+ for (int i = 0; i < all.getLength(); i++) {
+ var el = (org.w3c.dom.Element) all.item(i);
+ Map nfxAttrs = new HashMap<>();
+ var attrs = el.getAttributes();
+
+ List toRemove = new ArrayList<>();
+ for (int j = 0; j < attrs.getLength(); j++) {
+ var attr = (org.w3c.dom.Attr) attrs.item(j);
+ if (attr.getName().startsWith("nfx:")) {
+ nfxAttrs.put(attr.getName(), attr.getValue());
+ toRemove.add(attr.getName());
+ }
+ }
+
+ if (!nfxAttrs.isEmpty()) {
+ String fxid = el.getAttribute("fx:id");
+ if (fxid == null || fxid.isBlank()) {
+ fxid = "auto_id_" + (++autoId);
+ el.setAttribute("fx:id", fxid);
+ }
+ nfxBindingMap.put(fxid, nfxAttrs);
+ toRemove.forEach(el::removeAttribute);
+ }
+ }
+
+ // Speichere das bereinigte Dokument als temporäre Datei
+ File tempFile = File.createTempFile("cleaned_fxml", ".fxml");
+ tempFile.deleteOnExit();
+ var transformer = javax.xml.transform.TransformerFactory.newInstance().newTransformer();
+ transformer.setOutputProperty(javax.xml.transform.OutputKeys.INDENT, "yes");
+ transformer.transform(new javax.xml.transform.dom.DOMSource(doc),
+ new javax.xml.transform.stream.StreamResult(tempFile));
+
+ return tempFile.toURI().toString();
+ } catch (Exception e) {
+ throw new RuntimeException("Preprocessing failed for: " + fxmlPath, e);
+ }
+ }
+
+ private Map lookupNfxAttributes(javafx.scene.Node node, FXMLLoader loader) {
+ String fxid = loader.getNamespace().entrySet().stream()
+ .filter(e -> e.getValue() == node)
+ .map(Map.Entry::getKey)
+ .findFirst().orElse(null);
+ if (fxid == null) return Map.of();
+ return nfxBindingMap.getOrDefault(fxid, Map.of());
+ }
+}
diff --git a/injectfx-lib/src/main/java/de/neitzel/injectfx/FXMLComponentInstances.java b/injectfx-lib/src/main/java/de/neitzel/neitzelfx/injectfx/FXMLComponentInstances.java
similarity index 99%
rename from injectfx-lib/src/main/java/de/neitzel/injectfx/FXMLComponentInstances.java
rename to injectfx-lib/src/main/java/de/neitzel/neitzelfx/injectfx/FXMLComponentInstances.java
index 789ed7f..5da5942 100644
--- a/injectfx-lib/src/main/java/de/neitzel/injectfx/FXMLComponentInstances.java
+++ b/injectfx-lib/src/main/java/de/neitzel/neitzelfx/injectfx/FXMLComponentInstances.java
@@ -1,4 +1,4 @@
-package de.neitzel.injectfx;
+package de.neitzel.neitzelfx.injectfx;
import java.lang.reflect.Constructor;
import java.util.*;
diff --git a/injectfx-lib/src/main/java/de/neitzel/injectfx/InjectableComponentScanner.java b/injectfx-lib/src/main/java/de/neitzel/neitzelfx/injectfx/InjectableComponentScanner.java
similarity index 98%
rename from injectfx-lib/src/main/java/de/neitzel/injectfx/InjectableComponentScanner.java
rename to injectfx-lib/src/main/java/de/neitzel/neitzelfx/injectfx/InjectableComponentScanner.java
index e09e0f6..5791754 100644
--- a/injectfx-lib/src/main/java/de/neitzel/injectfx/InjectableComponentScanner.java
+++ b/injectfx-lib/src/main/java/de/neitzel/neitzelfx/injectfx/InjectableComponentScanner.java
@@ -1,6 +1,6 @@
-package de.neitzel.injectfx;
+package de.neitzel.neitzelfx.injectfx;
-import de.neitzel.injectfx.annotation.FXMLComponent;
+import de.neitzel.neitzelfx.injectfx.annotation.FXMLComponent;
import org.reflections.Reflections;
import java.lang.reflect.Constructor;
diff --git a/injectfx-lib/src/main/java/de/neitzel/injectfx/InjectingControllerFactory.java b/injectfx-lib/src/main/java/de/neitzel/neitzelfx/injectfx/InjectingControllerFactory.java
similarity index 97%
rename from injectfx-lib/src/main/java/de/neitzel/injectfx/InjectingControllerFactory.java
rename to injectfx-lib/src/main/java/de/neitzel/neitzelfx/injectfx/InjectingControllerFactory.java
index 2467345..33856d1 100644
--- a/injectfx-lib/src/main/java/de/neitzel/injectfx/InjectingControllerFactory.java
+++ b/injectfx-lib/src/main/java/de/neitzel/neitzelfx/injectfx/InjectingControllerFactory.java
@@ -1,4 +1,4 @@
-package de.neitzel.injectfx;
+package de.neitzel.neitzelfx.injectfx;
import javafx.util.Callback;
diff --git a/injectfx-lib/src/main/java/de/neitzel/injectfx/InjectingFXMLLoader.java b/injectfx-lib/src/main/java/de/neitzel/neitzelfx/injectfx/InjectingFXMLLoader.java
similarity index 97%
rename from injectfx-lib/src/main/java/de/neitzel/injectfx/InjectingFXMLLoader.java
rename to injectfx-lib/src/main/java/de/neitzel/neitzelfx/injectfx/InjectingFXMLLoader.java
index 90e4c79..d3309be 100644
--- a/injectfx-lib/src/main/java/de/neitzel/injectfx/InjectingFXMLLoader.java
+++ b/injectfx-lib/src/main/java/de/neitzel/neitzelfx/injectfx/InjectingFXMLLoader.java
@@ -1,4 +1,4 @@
-package de.neitzel.injectfx;
+package de.neitzel.neitzelfx.injectfx;
import javafx.fxml.FXMLLoader;
import lombok.extern.slf4j.Slf4j;
diff --git a/injectfx-lib/src/main/java/de/neitzel/injectfx/annotation/FXMLComponent.java b/injectfx-lib/src/main/java/de/neitzel/neitzelfx/injectfx/annotation/FXMLComponent.java
similarity index 83%
rename from injectfx-lib/src/main/java/de/neitzel/injectfx/annotation/FXMLComponent.java
rename to injectfx-lib/src/main/java/de/neitzel/neitzelfx/injectfx/annotation/FXMLComponent.java
index 0d585e6..c18f7a9 100644
--- a/injectfx-lib/src/main/java/de/neitzel/injectfx/annotation/FXMLComponent.java
+++ b/injectfx-lib/src/main/java/de/neitzel/neitzelfx/injectfx/annotation/FXMLComponent.java
@@ -1,4 +1,4 @@
-package de.neitzel.injectfx.annotation;
+package de.neitzel.neitzelfx.injectfx.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
diff --git a/injectfx-lib/src/main/java/de/neitzel/injectfx/annotation/FXMLConfig.java b/injectfx-lib/src/main/java/de/neitzel/neitzelfx/injectfx/annotation/FXMLConfig.java
similarity index 85%
rename from injectfx-lib/src/main/java/de/neitzel/injectfx/annotation/FXMLConfig.java
rename to injectfx-lib/src/main/java/de/neitzel/neitzelfx/injectfx/annotation/FXMLConfig.java
index 1294bd7..d92c59c 100644
--- a/injectfx-lib/src/main/java/de/neitzel/injectfx/annotation/FXMLConfig.java
+++ b/injectfx-lib/src/main/java/de/neitzel/neitzelfx/injectfx/annotation/FXMLConfig.java
@@ -1,4 +1,4 @@
-package de.neitzel.injectfx.annotation;
+package de.neitzel.neitzelfx.injectfx.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
diff --git a/injectfx-lib/src/main/java/de/neitzel/neitzelfx/mvvm/BindDirection.java b/injectfx-lib/src/main/java/de/neitzel/neitzelfx/mvvm/BindDirection.java
new file mode 100644
index 0000000..b48b226
--- /dev/null
+++ b/injectfx-lib/src/main/java/de/neitzel/neitzelfx/mvvm/BindDirection.java
@@ -0,0 +1,50 @@
+package de.neitzel.neitzelfx.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/injectfx-lib/src/main/java/de/neitzel/neitzelfx/mvvm/BindingAwareFXMLLoader.java b/injectfx-lib/src/main/java/de/neitzel/neitzelfx/mvvm/BindingAwareFXMLLoader.java
new file mode 100644
index 0000000..b5695ce
--- /dev/null
+++ b/injectfx-lib/src/main/java/de/neitzel/neitzelfx/mvvm/BindingAwareFXMLLoader.java
@@ -0,0 +1,116 @@
+package de.neitzel.neitzelfx.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 lombok.RequiredArgsConstructor;
+
+import java.io.InputStream;
+
+@RequiredArgsConstructor
+/**
+ * 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;
+
+ /**
+ * 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);
+
+ if (node instanceof TextInputControl control) {
+ Property prop = viewModel.property(StringProperty.class, propertyName);
+ bind(control.textProperty(), prop, direction);
+ } else if (node instanceof javafx.scene.control.Label label) {
+ Property prop = viewModel.property(StringProperty.class, propertyName);
+ bind(label.textProperty(), prop, direction);
+ } else if (node instanceof javafx.scene.control.CheckBox checkBox) {
+ Property prop = viewModel.property(javafx.beans.property.BooleanProperty.class, propertyName);
+ bind(checkBox.selectedProperty(), prop, direction);
+ } else if (node instanceof javafx.scene.control.Slider slider) {
+ Property prop = viewModel.property(javafx.beans.property.DoubleProperty.class, propertyName);
+ bind(slider.valueProperty(), prop, direction);
+ } else if (node instanceof javafx.scene.control.DatePicker datePicker) {
+ @SuppressWarnings("unchecked")
+ Property prop = (Property) viewModel.property(javafx.beans.property.ObjectProperty.class, propertyName);
+ bind(datePicker.valueProperty(), prop, direction);
+ }
+ }
+ }
+
+ /**
+ * 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/injectfx-lib/src/main/java/de/neitzel/neitzelfx/mvvm/GenericViewController.java b/injectfx-lib/src/main/java/de/neitzel/neitzelfx/mvvm/GenericViewController.java
new file mode 100644
index 0000000..66ec99e
--- /dev/null
+++ b/injectfx-lib/src/main/java/de/neitzel/neitzelfx/mvvm/GenericViewController.java
@@ -0,0 +1,56 @@
+package de.neitzel.neitzelfx.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;
+
+ /**
+ * 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);
+ }
+
+ /**
+ * Retrieves the original model instance from the ViewModel.
+ *
+ * @return the model instance
+ */
+ public T getModel() {
+ return viewModel.getModel();
+ }
+
+ /**
+ * 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/injectfx-lib/src/main/java/de/neitzel/neitzelfx/mvvm/GenericViewModel.java b/injectfx-lib/src/main/java/de/neitzel/neitzelfx/mvvm/GenericViewModel.java
new file mode 100644
index 0000000..ac18c25
--- /dev/null
+++ b/injectfx-lib/src/main/java/de/neitzel/neitzelfx/mvvm/GenericViewModel.java
@@ -0,0 +1,141 @@
+package de.neitzel.neitzelfx.mvvm;
+
+import javafx.beans.property.*;
+
+import java.beans.BeanInfo;
+import java.beans.IntrospectionException;
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.lang.reflect.*;
+import java.util.*;
+
+/**
+ * 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/injectfx-lib/src/test/java/de/neitzel/injectfx/testcomponents/test1ok/SuperClass.java b/injectfx-lib/src/test/java/de/neitzel/injectfx/testcomponents/test1ok/SuperClass.java
deleted file mode 100644
index e38029a..0000000
--- a/injectfx-lib/src/test/java/de/neitzel/injectfx/testcomponents/test1ok/SuperClass.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package de.neitzel.injectfx.testcomponents.test1ok;
-
-public class SuperClass {
-}
diff --git a/injectfx-lib/src/test/java/de/neitzel/injectfx/testcomponents/test1ok/TestInterface1_1.java b/injectfx-lib/src/test/java/de/neitzel/injectfx/testcomponents/test1ok/TestInterface1_1.java
deleted file mode 100644
index 3d86e74..0000000
--- a/injectfx-lib/src/test/java/de/neitzel/injectfx/testcomponents/test1ok/TestInterface1_1.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package de.neitzel.injectfx.testcomponents.test1ok;
-
-public interface TestInterface1_1 {
-}
diff --git a/injectfx-lib/src/test/java/de/neitzel/injectfx/testcomponents/test1ok/TestInterface1_2.java b/injectfx-lib/src/test/java/de/neitzel/injectfx/testcomponents/test1ok/TestInterface1_2.java
deleted file mode 100644
index b923839..0000000
--- a/injectfx-lib/src/test/java/de/neitzel/injectfx/testcomponents/test1ok/TestInterface1_2.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package de.neitzel.injectfx.testcomponents.test1ok;
-
-public interface TestInterface1_2 {
-}
diff --git a/injectfx-lib/src/test/java/de/neitzel/injectfx/testcomponents/test1ok/sub/TestComponent1_2.java b/injectfx-lib/src/test/java/de/neitzel/injectfx/testcomponents/test1ok/sub/TestComponent1_2.java
deleted file mode 100644
index cd6a859..0000000
--- a/injectfx-lib/src/test/java/de/neitzel/injectfx/testcomponents/test1ok/sub/TestComponent1_2.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package de.neitzel.injectfx.testcomponents.test1ok.sub;
-
-import de.neitzel.injectfx.annotation.FXMLComponent;
-import de.neitzel.injectfx.testcomponents.test1ok.SuperClass;
-import de.neitzel.injectfx.testcomponents.test1ok.TestInterface1_1;
-import de.neitzel.injectfx.testcomponents.test1ok.TestInterface1_2;
-
-@FXMLComponent
-public class TestComponent1_2 extends SuperClass implements TestInterface1_1, TestInterface1_2 {
- public TestComponent1_2() {
- }
-}
diff --git a/injectfx-lib/src/test/java/de/neitzel/injectfx/InjectableComponentScannerTest.java b/injectfx-lib/src/test/java/de/neitzel/neitzelfx/injectfx/InjectableComponentScannerTest.java
similarity index 81%
rename from injectfx-lib/src/test/java/de/neitzel/injectfx/InjectableComponentScannerTest.java
rename to injectfx-lib/src/test/java/de/neitzel/neitzelfx/injectfx/InjectableComponentScannerTest.java
index 4a39e69..e9f43c7 100644
--- a/injectfx-lib/src/test/java/de/neitzel/injectfx/InjectableComponentScannerTest.java
+++ b/injectfx-lib/src/test/java/de/neitzel/neitzelfx/injectfx/InjectableComponentScannerTest.java
@@ -1,10 +1,10 @@
-package de.neitzel.injectfx;
+package de.neitzel.neitzelfx.injectfx;
-import de.neitzel.injectfx.testcomponents.test1ok.SuperClass;
-import de.neitzel.injectfx.testcomponents.test1ok.TestComponent1_1;
-import de.neitzel.injectfx.testcomponents.test1ok.TestInterface1_1;
-import de.neitzel.injectfx.testcomponents.test1ok.TestInterface1_2;
-import de.neitzel.injectfx.testcomponents.test1ok.sub.TestComponent1_2;
+import de.neitzel.neitzelfx.injectfx.testcomponents.test1ok.SuperClass;
+import de.neitzel.neitzelfx.injectfx.testcomponents.test1ok.TestComponent1_1;
+import de.neitzel.neitzelfx.injectfx.testcomponents.test1ok.TestInterface1_1;
+import de.neitzel.neitzelfx.injectfx.testcomponents.test1ok.TestInterface1_2;
+import de.neitzel.neitzelfx.injectfx.testcomponents.test1ok.sub.TestComponent1_2;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
diff --git a/injectfx-lib/src/test/java/de/neitzel/neitzelfx/injectfx/testcomponents/test1ok/SuperClass.java b/injectfx-lib/src/test/java/de/neitzel/neitzelfx/injectfx/testcomponents/test1ok/SuperClass.java
new file mode 100644
index 0000000..4338fa5
--- /dev/null
+++ b/injectfx-lib/src/test/java/de/neitzel/neitzelfx/injectfx/testcomponents/test1ok/SuperClass.java
@@ -0,0 +1,4 @@
+package de.neitzel.neitzelfx.injectfx.testcomponents.test1ok;
+
+public class SuperClass {
+}
diff --git a/injectfx-lib/src/test/java/de/neitzel/injectfx/testcomponents/test1ok/TestComponent1_1.java b/injectfx-lib/src/test/java/de/neitzel/neitzelfx/injectfx/testcomponents/test1ok/TestComponent1_1.java
similarity index 52%
rename from injectfx-lib/src/test/java/de/neitzel/injectfx/testcomponents/test1ok/TestComponent1_1.java
rename to injectfx-lib/src/test/java/de/neitzel/neitzelfx/injectfx/testcomponents/test1ok/TestComponent1_1.java
index 44dc8bf..7ad88ba 100644
--- a/injectfx-lib/src/test/java/de/neitzel/injectfx/testcomponents/test1ok/TestComponent1_1.java
+++ b/injectfx-lib/src/test/java/de/neitzel/neitzelfx/injectfx/testcomponents/test1ok/TestComponent1_1.java
@@ -1,6 +1,6 @@
-package de.neitzel.injectfx.testcomponents.test1ok;
+package de.neitzel.neitzelfx.injectfx.testcomponents.test1ok;
-import de.neitzel.injectfx.annotation.FXMLComponent;
+import de.neitzel.neitzelfx.injectfx.annotation.FXMLComponent;
@FXMLComponent
public class TestComponent1_1 extends SuperClass implements TestInterface1_2 {
diff --git a/injectfx-lib/src/test/java/de/neitzel/neitzelfx/injectfx/testcomponents/test1ok/TestInterface1_1.java b/injectfx-lib/src/test/java/de/neitzel/neitzelfx/injectfx/testcomponents/test1ok/TestInterface1_1.java
new file mode 100644
index 0000000..6683035
--- /dev/null
+++ b/injectfx-lib/src/test/java/de/neitzel/neitzelfx/injectfx/testcomponents/test1ok/TestInterface1_1.java
@@ -0,0 +1,4 @@
+package de.neitzel.neitzelfx.injectfx.testcomponents.test1ok;
+
+public interface TestInterface1_1 {
+}
diff --git a/injectfx-lib/src/test/java/de/neitzel/neitzelfx/injectfx/testcomponents/test1ok/TestInterface1_2.java b/injectfx-lib/src/test/java/de/neitzel/neitzelfx/injectfx/testcomponents/test1ok/TestInterface1_2.java
new file mode 100644
index 0000000..fd899b0
--- /dev/null
+++ b/injectfx-lib/src/test/java/de/neitzel/neitzelfx/injectfx/testcomponents/test1ok/TestInterface1_2.java
@@ -0,0 +1,4 @@
+package de.neitzel.neitzelfx.injectfx.testcomponents.test1ok;
+
+public interface TestInterface1_2 {
+}
diff --git a/injectfx-lib/src/test/java/de/neitzel/neitzelfx/injectfx/testcomponents/test1ok/sub/TestComponent1_2.java b/injectfx-lib/src/test/java/de/neitzel/neitzelfx/injectfx/testcomponents/test1ok/sub/TestComponent1_2.java
new file mode 100644
index 0000000..ba2c0fd
--- /dev/null
+++ b/injectfx-lib/src/test/java/de/neitzel/neitzelfx/injectfx/testcomponents/test1ok/sub/TestComponent1_2.java
@@ -0,0 +1,12 @@
+package de.neitzel.neitzelfx.injectfx.testcomponents.test1ok.sub;
+
+import de.neitzel.neitzelfx.injectfx.annotation.FXMLComponent;
+import de.neitzel.neitzelfx.injectfx.testcomponents.test1ok.SuperClass;
+import de.neitzel.neitzelfx.injectfx.testcomponents.test1ok.TestInterface1_1;
+import de.neitzel.neitzelfx.injectfx.testcomponents.test1ok.TestInterface1_2;
+
+@FXMLComponent
+public class TestComponent1_2 extends SuperClass implements TestInterface1_1, TestInterface1_2 {
+ public TestComponent1_2() {
+ }
+}
diff --git a/injectfx-lib/src/test/java/de/neitzel/injectfx/testcomponents/test2fail/TestComponent2_1.java b/injectfx-lib/src/test/java/de/neitzel/neitzelfx/injectfx/testcomponents/test2fail/TestComponent2_1.java
similarity index 53%
rename from injectfx-lib/src/test/java/de/neitzel/injectfx/testcomponents/test2fail/TestComponent2_1.java
rename to injectfx-lib/src/test/java/de/neitzel/neitzelfx/injectfx/testcomponents/test2fail/TestComponent2_1.java
index 78f0e3e..3dea0d9 100644
--- a/injectfx-lib/src/test/java/de/neitzel/injectfx/testcomponents/test2fail/TestComponent2_1.java
+++ b/injectfx-lib/src/test/java/de/neitzel/neitzelfx/injectfx/testcomponents/test2fail/TestComponent2_1.java
@@ -1,6 +1,6 @@
-package de.neitzel.injectfx.testcomponents.test2fail;
+package de.neitzel.neitzelfx.injectfx.testcomponents.test2fail;
-import de.neitzel.injectfx.annotation.FXMLComponent;
+import de.neitzel.neitzelfx.injectfx.annotation.FXMLComponent;
/**
* TestComponent1 that should fail.
diff --git a/pom.xml b/pom.xml
index 07ed0e0..1d7799e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -61,6 +61,11 @@
+
+ org.openjfx
+ javafx-base
+ ${javafx.version}
+
org.openjfx
javafx-controls