First try to build some component based solution.

This commit is contained in:
Konrad Neitzel 2025-03-22 22:56:53 +01:00
parent eb14cb7994
commit 371656572c
38 changed files with 873 additions and 47 deletions

29
README.md Normal file
View File

@ -0,0 +1,29 @@
# JavaFX Maven Project
Example Maven Project for a JavaFX Application.
**Update**: Added profile fatjar
**Update**: Added reporting to create a site (html documentation of project)
**Update**: Java 21 is now fully supported
This projects includes multiple plugins:
- Build of an App-Image using JPackage (Profile: image)
- Build of an fat jar (Profile: fatjar)
- Use of Maven Wrapper
- Static code analysis with PMD and Spotbugs
- Check of dependency updates during build
- JavaFX plugin to start application
**Requirements**
To use this Template, all you need is a local Java Installation.
My current advice is to use a long term supported (LTS) version of either Java 17 or Java 21.
**[Documentation in English](documentation/en/_Index.md)**
**[Dokumentation in Deutsch](documentation/de/_Index.md)**
**Important: ChatGPT was utilized to generate the documentation based on
predefined content specifications, as it represents the fastest way to produce
comprehensive documentation.**

View File

@ -16,7 +16,7 @@
<link.name>${project.artifactId}</link.name> <link.name>${project.artifactId}</link.name>
<launcher>${project.artifactId}</launcher> <launcher>${project.artifactId}</launcher>
<appName>${project.artifactId}</appName> <appName>${project.artifactId}</appName>
<main.class>de.neitzel.injectfx.example.Main</main.class> <main.class>de.neitzel.neitzelfx.injectfx.example.Main</main.class>
<jar.filename>${project.artifactId}-${project.version}</jar.filename> <jar.filename>${project.artifactId}-${project.version}</jar.filename>
</properties> </properties>

View File

@ -0,0 +1,11 @@
package de.neitzel.neitzelfx.component.example;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class Address {
private String street;
private String city;
}

View File

@ -0,0 +1,36 @@
package de.neitzel.neitzelfx.component.example;
import de.neitzel.neitzelfx.component.ComponentLoader;
import javafx.application.Application;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class ExampleApp extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
// Beispielmodel initialisieren
Address address = new Address();
address.setStreet("Sample Street 1");
address.setCity("Sample City");
Person person = new Person();
person.setName("Max Mustermann");
person.setAddress(address);
// ComponentLoader verwenden
ComponentLoader loader = new ComponentLoader();
Parent root = loader.load(person, "/person.fxml");
// Scene erstellen und anzeigen
Scene scene = new Scene(root);
primaryStage.setTitle("ComponentLoader Example");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch();
}
}

View File

@ -0,0 +1,17 @@
package de.neitzel.neitzelfx.component.example;
import de.neitzel.neitzelfx.injectfx.example.JavaFXApp;
/**
* Another Main class as workaround when the JavaFX Application ist started without
* taking care os Classloader Requirements of JavaFX. (Important when starting from inside NetBeans!)
*/
public class Main {
/**
* Additional main methode to start Application.
* @param args Commandline Arguments.
*/
public static void main(String[] args) {
ExampleApp.main(args);
}
}

View File

@ -0,0 +1,11 @@
package de.neitzel.neitzelfx.component.example;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class Person {
private String name;
private Address address;
}

View File

@ -1,4 +1,4 @@
package de.neitzel.injectfx.example; package de.neitzel.neitzelfx.injectfx.example;
import javafx.application.Application; import javafx.application.Application;
import javafx.fxml.FXMLLoader; import javafx.fxml.FXMLLoader;

View File

@ -1,4 +1,4 @@
package de.neitzel.injectfx.example; package de.neitzel.neitzelfx.injectfx.example;
/** /**
* Another Main class as workaround when the JavaFX Application ist started without * Another Main class as workaround when the JavaFX Application ist started without

View File

@ -1,4 +1,4 @@
package de.neitzel.injectfx.example; package de.neitzel.neitzelfx.injectfx.example;
import javafx.event.ActionEvent; import javafx.event.ActionEvent;
import javafx.fxml.FXML; import javafx.fxml.FXML;

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.control.*?>
<AnchorPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
xmlns:nfx="http://example.com/nfx"
fx:controller="de.neitzel.neitzelfx.component.ComponentController"
prefWidth="300" prefHeight="100">
<children>
<TextField layoutX="10" layoutY="10" prefWidth="280"
nfx:target="street" nfx:direction="bidirectional"/>
<TextField layoutX="10" layoutY="50" prefWidth="280"
nfx:target="city" nfx:direction="bidirectional"/>
</children>
</AnchorPane>

View File

@ -3,7 +3,7 @@
<?import javafx.scene.control.*?> <?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.*?>
<AnchorPane prefHeight="127.0" prefWidth="209.0" xmlns="http://javafx.com/javafx/17.0.2-ea" xmlns:fx="http://javafx.com/fxml/1" fx:controller="de.neitzel.injectfx.example.MainWindow"> <AnchorPane prefHeight="127.0" prefWidth="209.0" xmlns="http://javafx.com/javafx/17.0.2-ea" xmlns:fx="http://javafx.com/fxml/1" fx:controller="de.neitzel.neitzelfx.injectfx.example.MainWindow">
<children> <children>
<Button fx:id="button" layoutX="44.0" layoutY="70.0" mnemonicParsing="false" onAction="#onButtonClick" text="Click Me" /> <Button fx:id="button" layoutX="44.0" layoutY="70.0" mnemonicParsing="false" onAction="#onButtonClick" text="Click Me" />
<TextField fx:id="textField" layoutX="14.0" layoutY="24.0" /> <TextField fx:id="textField" layoutX="14.0" layoutY="24.0" />

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.control.*?>
<AnchorPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
xmlns:nfx="http://example.com/nfx"
fx:controller="de.neitzel.neitzelfx.component.ComponentController"
prefWidth="300" prefHeight="180">
<children>
<TextField layoutX="10" layoutY="10" prefWidth="280"
nfx:target="name" nfx:direction="bidirectional"/>
<Pane layoutX="10" layoutY="50" prefWidth="280" prefHeight="110"
nfx:target="address" nfx:direction="bidirectional"
nfx:source="/address.fxml"/>
</children>
</AnchorPane>

View File

@ -17,7 +17,7 @@
<link.name>${project.artifactId}</link.name> <link.name>${project.artifactId}</link.name>
<launcher>${project.artifactId}</launcher> <launcher>${project.artifactId}</launcher>
<appName>${project.artifactId}</appName> <appName>${project.artifactId}</appName>
<main.class>de.neitzel.injectfx.example.Main</main.class> <main.class>example.de.neitzel.neitzelfx.injectfx.Main</main.class>
<jar.filename>${project.artifactId}-${project.version}</jar.filename> <jar.filename>${project.artifactId}-${project.version}</jar.filename>
</properties> </properties>

View File

@ -0,0 +1,108 @@
package de.neitzel.neitzelfx.component;
import javafx.beans.property.*;
import java.lang.reflect.*;
import java.util.*;
public class AutoViewModel<T> {
private final T model;
private final Map<String, Property<?>> 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);
}
}

View File

@ -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.
* <p>
* 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;
}

View File

@ -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.
* <p>
* 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<String, Map<String, String>> 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<javafx.scene.Node> 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<String, String> extractNfxAttributes(javafx.scene.Node node) {
Map<String, String> 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<String, String> nfxAttrs = new HashMap<>();
var attrs = el.getAttributes();
List<String> 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<String, String> 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());
}
}

View File

@ -1,4 +1,4 @@
package de.neitzel.injectfx; package de.neitzel.neitzelfx.injectfx;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.util.*; import java.util.*;

View File

@ -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 org.reflections.Reflections;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;

View File

@ -1,4 +1,4 @@
package de.neitzel.injectfx; package de.neitzel.neitzelfx.injectfx;
import javafx.util.Callback; import javafx.util.Callback;

View File

@ -1,4 +1,4 @@
package de.neitzel.injectfx; package de.neitzel.neitzelfx.injectfx;
import javafx.fxml.FXMLLoader; import javafx.fxml.FXMLLoader;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;

View File

@ -1,4 +1,4 @@
package de.neitzel.injectfx.annotation; package de.neitzel.neitzelfx.injectfx.annotation;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;

View File

@ -1,4 +1,4 @@
package de.neitzel.injectfx.annotation; package de.neitzel.neitzelfx.injectfx.annotation;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;

View File

@ -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);
}
}

View File

@ -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 <T> the type of the model used in the ViewModel
*/
public class BindingAwareFXMLLoader<T> {
/**
* 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<T> controller = new GenericViewController<>();
controller.setModel(model);
return controller;
});
Parent root = loader.load(fxml);
GenericViewController<T> controller = loader.getController();
GenericViewModel<T> 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<T> 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<String> prop = viewModel.property(StringProperty.class, propertyName);
bind(control.textProperty(), prop, direction);
} else if (node instanceof javafx.scene.control.Label label) {
Property<String> prop = viewModel.property(StringProperty.class, propertyName);
bind(label.textProperty(), prop, direction);
} else if (node instanceof javafx.scene.control.CheckBox checkBox) {
Property<Boolean> prop = viewModel.property(javafx.beans.property.BooleanProperty.class, propertyName);
bind(checkBox.selectedProperty(), prop, direction);
} else if (node instanceof javafx.scene.control.Slider slider) {
Property<Number> 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<java.time.LocalDate> prop = (Property<java.time.LocalDate>) 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 <V> the value type of the property
*/
private <V> void bind(Property<V> controlProperty, Property<V> 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"));
}
}

View File

@ -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 <T> the type of the model
*/
public class GenericViewController<T> implements Initializable {
/**
* The GenericViewModel instance wrapping the underlying model.
*/
private GenericViewModel<T> viewModel;
/**
* Sets the model for this controller by wrapping it in a GenericViewModel.
*
* @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<T> getViewModel() {
return viewModel;
}
}

View File

@ -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 <T> the type of the underlying model
*/
public class GenericViewModel<T> {
/**
* The original model instance.
*/
private final T model;
/**
* A map holding JavaFX properties corresponding to the model's fields.
*/
private final Map<String, Property<?>> 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<PropertyDescriptor> 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 <P> 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 extends Property<?>> P property(Class<P> 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;
}
}

View File

@ -1,4 +0,0 @@
package de.neitzel.injectfx.testcomponents.test1ok;
public class SuperClass {
}

View File

@ -1,4 +0,0 @@
package de.neitzel.injectfx.testcomponents.test1ok;
public interface TestInterface1_1 {
}

View File

@ -1,4 +0,0 @@
package de.neitzel.injectfx.testcomponents.test1ok;
public interface TestInterface1_2 {
}

View File

@ -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() {
}
}

View File

@ -1,10 +1,10 @@
package de.neitzel.injectfx; package de.neitzel.neitzelfx.injectfx;
import de.neitzel.injectfx.testcomponents.test1ok.SuperClass; import de.neitzel.neitzelfx.injectfx.testcomponents.test1ok.SuperClass;
import de.neitzel.injectfx.testcomponents.test1ok.TestComponent1_1; import de.neitzel.neitzelfx.injectfx.testcomponents.test1ok.TestComponent1_1;
import de.neitzel.injectfx.testcomponents.test1ok.TestInterface1_1; import de.neitzel.neitzelfx.injectfx.testcomponents.test1ok.TestInterface1_1;
import de.neitzel.injectfx.testcomponents.test1ok.TestInterface1_2; import de.neitzel.neitzelfx.injectfx.testcomponents.test1ok.TestInterface1_2;
import de.neitzel.injectfx.testcomponents.test1ok.sub.TestComponent1_2; import de.neitzel.neitzelfx.injectfx.testcomponents.test1ok.sub.TestComponent1_2;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;

View File

@ -0,0 +1,4 @@
package de.neitzel.neitzelfx.injectfx.testcomponents.test1ok;
public class SuperClass {
}

View File

@ -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 @FXMLComponent
public class TestComponent1_1 extends SuperClass implements TestInterface1_2 { public class TestComponent1_1 extends SuperClass implements TestInterface1_2 {

View File

@ -0,0 +1,4 @@
package de.neitzel.neitzelfx.injectfx.testcomponents.test1ok;
public interface TestInterface1_1 {
}

View File

@ -0,0 +1,4 @@
package de.neitzel.neitzelfx.injectfx.testcomponents.test1ok;
public interface TestInterface1_2 {
}

View File

@ -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() {
}
}

View File

@ -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. * TestComponent1 that should fail.

View File

@ -61,6 +61,11 @@
<dependencies> <dependencies>
<!-- JavaFX dependencies --> <!-- JavaFX dependencies -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-base</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.openjfx</groupId> <groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId> <artifactId>javafx-controls</artifactId>