First try to build some component based solution.
This commit is contained in:
parent
eb14cb7994
commit
371656572c
29
README.md
Normal file
29
README.md
Normal 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.**
|
||||
@ -16,7 +16,7 @@
|
||||
<link.name>${project.artifactId}</link.name>
|
||||
<launcher>${project.artifactId}</launcher>
|
||||
<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>
|
||||
</properties>
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package de.neitzel.injectfx.example;
|
||||
package de.neitzel.neitzelfx.injectfx.example;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
@ -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
|
||||
@ -1,4 +1,4 @@
|
||||
package de.neitzel.injectfx.example;
|
||||
package de.neitzel.neitzelfx.injectfx.example;
|
||||
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
17
injectfx-example/src/main/resources/address.fxml
Normal file
17
injectfx-example/src/main/resources/address.fxml
Normal 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>
|
||||
@ -3,7 +3,7 @@
|
||||
<?import javafx.scene.control.*?>
|
||||
<?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>
|
||||
<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" />
|
||||
18
injectfx-example/src/main/resources/person.fxml
Normal file
18
injectfx-example/src/main/resources/person.fxml
Normal 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>
|
||||
@ -17,7 +17,7 @@
|
||||
<link.name>${project.artifactId}</link.name>
|
||||
<launcher>${project.artifactId}</launcher>
|
||||
<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>
|
||||
</properties>
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package de.neitzel.injectfx;
|
||||
package de.neitzel.neitzelfx.injectfx;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.*;
|
||||
@ -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;
|
||||
@ -1,4 +1,4 @@
|
||||
package de.neitzel.injectfx;
|
||||
package de.neitzel.neitzelfx.injectfx;
|
||||
|
||||
import javafx.util.Callback;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package de.neitzel.injectfx;
|
||||
package de.neitzel.neitzelfx.injectfx;
|
||||
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
package de.neitzel.injectfx.testcomponents.test1ok;
|
||||
|
||||
public class SuperClass {
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
package de.neitzel.injectfx.testcomponents.test1ok;
|
||||
|
||||
public interface TestInterface1_1 {
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
package de.neitzel.injectfx.testcomponents.test1ok;
|
||||
|
||||
public interface TestInterface1_2 {
|
||||
}
|
||||
@ -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() {
|
||||
}
|
||||
}
|
||||
@ -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.*;
|
||||
@ -0,0 +1,4 @@
|
||||
package de.neitzel.neitzelfx.injectfx.testcomponents.test1ok;
|
||||
|
||||
public class SuperClass {
|
||||
}
|
||||
@ -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 {
|
||||
@ -0,0 +1,4 @@
|
||||
package de.neitzel.neitzelfx.injectfx.testcomponents.test1ok;
|
||||
|
||||
public interface TestInterface1_1 {
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
package de.neitzel.neitzelfx.injectfx.testcomponents.test1ok;
|
||||
|
||||
public interface TestInterface1_2 {
|
||||
}
|
||||
@ -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() {
|
||||
}
|
||||
}
|
||||
@ -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.
|
||||
Loading…
x
Reference in New Issue
Block a user