Remove the replaced JavaFX units so the view library is the only fx API.
fx-example still calls those units, so it leaves the reactor until it is migrated. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,226 +0,0 @@
|
|||||||
package de.neitzel.fx.component;
|
|
||||||
|
|
||||||
import javafx.beans.property.Property;
|
|
||||||
import javafx.beans.property.SimpleBooleanProperty;
|
|
||||||
import javafx.beans.property.SimpleDoubleProperty;
|
|
||||||
import javafx.beans.property.SimpleFloatProperty;
|
|
||||||
import javafx.beans.property.SimpleIntegerProperty;
|
|
||||||
import javafx.beans.property.SimpleLongProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.lang.reflect.Modifier;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AutoViewModel automatically exposes JavaFX properties for all readable/writable fields
|
|
||||||
* of a given POJO model. It creates appropriate Property instances and keeps them in a map
|
|
||||||
* for lookup by field name.
|
|
||||||
*
|
|
||||||
* @param <T> the type of the underlying model
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class AutoViewModel<T> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The wrapped model instance.
|
|
||||||
* -- GETTER --
|
|
||||||
* Retrieves the model associated with this AutoViewModel.
|
|
||||||
*/
|
|
||||||
@Getter
|
|
||||||
private final T model;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map of field name to JavaFX Property instance exposing that field's value.
|
|
||||||
*/
|
|
||||||
private final Map<String, Property<?>> properties = new HashMap<>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs an AutoViewModel for the provided model and initializes properties via reflection.
|
|
||||||
*
|
|
||||||
* @param model the POJO model whose getters/setters are used to create JavaFX properties
|
|
||||||
*/
|
|
||||||
public AutoViewModel(T model) {
|
|
||||||
this.model = model;
|
|
||||||
initProperties();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes a mapping of field names to JavaFX properties for the associated model object.
|
|
||||||
* <p>
|
|
||||||
* This method utilizes reflection to iterate through the public no-argument getters
|
|
||||||
* of the model's class to create corresponding JavaFX properties. It performs the following:
|
|
||||||
* <p>
|
|
||||||
* 1. Identifies methods that adhere to the getter naming conventions (e.g., `getFieldName` or `isFieldName`).
|
|
||||||
* 2. Maps the field name derived from the getter method to a JavaFX property, creating an appropriate
|
|
||||||
* property type based on the getter's return type.
|
|
||||||
* 3. Adds a listener to each property to bind updates from the property back to the model object
|
|
||||||
* using the corresponding setter method, if available. This ensures bi-directional synchronization
|
|
||||||
* between the ViewModel and the underlying model.
|
|
||||||
* <p>
|
|
||||||
* Exceptions that may occur during reflective operations (e.g., invoking methods) are caught
|
|
||||||
* and logged to avoid runtime failures.
|
|
||||||
*/
|
|
||||||
private void initProperties() {
|
|
||||||
for (Method getter : model.getClass().getMethods()) {
|
|
||||||
if (isGetter(getter)) {
|
|
||||||
String fieldName = getFieldName(getter);
|
|
||||||
Object value = invokeGetter(getter);
|
|
||||||
|
|
||||||
Property<?> prop = toProperty(value);
|
|
||||||
properties.put(fieldName, prop);
|
|
||||||
|
|
||||||
// Bind ViewModel → Model
|
|
||||||
prop.addListener((_obs, _oldVal, newVal) -> {
|
|
||||||
// use _obs and _oldVal to satisfy static analysis (they are intentionally retained)
|
|
||||||
if (_oldVal != null && _oldVal.equals(newVal)) {
|
|
||||||
// no-op: values unchanged
|
|
||||||
}
|
|
||||||
Method setter = findSetterFor(model.getClass(), fieldName, newVal != null ? newVal.getClass() : null);
|
|
||||||
if (setter != null) {
|
|
||||||
try {
|
|
||||||
setter.invoke(model, newVal);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("Failed to invoke setter for field {}", fieldName, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if a given method follows the JavaBean getter convention.
|
|
||||||
* The method must be public, take no parameters, have a non-void return type,
|
|
||||||
* and its name should start with either "get" or "is".
|
|
||||||
*
|
|
||||||
* @param method the method to be assessed
|
|
||||||
* @return true if the method adheres to the JavaBean getter convention, false otherwise
|
|
||||||
*/
|
|
||||||
private boolean isGetter(Method method) {
|
|
||||||
return Modifier.isPublic(method.getModifiers())
|
|
||||||
&& method.getParameterCount() == 0
|
|
||||||
&& !method.getReturnType().equals(void.class)
|
|
||||||
&& (method.getName().startsWith("get") || method.getName().startsWith("is"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Derives the field name from a given Java method by following JavaBean naming conventions.
|
|
||||||
* The method's name is analyzed to strip the "get" or "is" prefix (if present),
|
|
||||||
* and the result is converted into a decapitalized field name.
|
|
||||||
*
|
|
||||||
* @param method the method whose name is to be processed to derive the field name
|
|
||||||
* @return the derived field name, or the original method name if no "get" or "is" prefix is found
|
|
||||||
*/
|
|
||||||
private String getFieldName(Method method) {
|
|
||||||
String name = method.getName();
|
|
||||||
if (name.startsWith("get")) {
|
|
||||||
return decapitalize(name.substring(3));
|
|
||||||
} else if (name.startsWith("is")) {
|
|
||||||
return decapitalize(name.substring(2));
|
|
||||||
}
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== Hilfsmethoden ==========
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invokes the specified method on the model object and returns the result.
|
|
||||||
* If the method invocation fails, logs a warning and returns null.
|
|
||||||
*
|
|
||||||
* @param method the method to be invoked, typically a JavaBean getter
|
|
||||||
* @return the result of invoking the method, or null if an error occurs
|
|
||||||
*/
|
|
||||||
private Object invokeGetter(Method method) {
|
|
||||||
try {
|
|
||||||
return method.invoke(model);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("Failed to invoke getter: {}", method.getName(), e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts an object into an appropriate JavaFX {@link Property} based on its type.
|
|
||||||
*
|
|
||||||
* @param value the object to be converted into a JavaFX property; can be of type String, Integer,
|
|
||||||
* Boolean, Double, Float, Long, or any other object
|
|
||||||
* @return a JavaFX {@link Property} corresponding to the type of the input object, such as
|
|
||||||
* {@link SimpleStringProperty} for String, {@link SimpleIntegerProperty} for Integer,
|
|
||||||
* {@link SimpleBooleanProperty} for Boolean, and so on. If the type is not explicitly
|
|
||||||
* handled, a {@link SimpleObjectProperty} is returned wrapping the object
|
|
||||||
*/
|
|
||||||
private Property<?> toProperty(Object value) {
|
|
||||||
if (value instanceof String s) return new SimpleStringProperty(s);
|
|
||||||
if (value instanceof Integer i) return new SimpleIntegerProperty(i);
|
|
||||||
if (value instanceof Boolean b) return new SimpleBooleanProperty(b);
|
|
||||||
if (value instanceof Double d) return new SimpleDoubleProperty(d);
|
|
||||||
if (value instanceof Float f) return new SimpleFloatProperty(f);
|
|
||||||
if (value instanceof Long l) return new SimpleLongProperty(l);
|
|
||||||
return new SimpleObjectProperty<>(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds a setter method in a specified class that corresponds to the given field name and value type.
|
|
||||||
* <p>
|
|
||||||
* The method searches for a public method in the class whose name matches the JavaBean-style setter
|
|
||||||
* naming convention (e.g., "setFieldName"). The method must accept exactly one parameter, and if a
|
|
||||||
* value type is provided, the parameter type must be assignable from it.
|
|
||||||
*
|
|
||||||
* @param clazz the class to search for the method
|
|
||||||
* @param fieldName the name of the field for which the setter method is being sought
|
|
||||||
* @param valueType the expected type of the parameter for the setter method, or null if no specific
|
|
||||||
* type constraint is required
|
|
||||||
* @return the setter method if found, or null if no matching method is identified
|
|
||||||
*/
|
|
||||||
private Method findSetterFor(Class<?> clazz, String fieldName, Class<?> valueType) {
|
|
||||||
String setterName = "set" + capitalize(fieldName);
|
|
||||||
for (Method m : clazz.getMethods()) {
|
|
||||||
if (m.getName().equals(setterName) && m.getParameterCount() == 1) {
|
|
||||||
if (valueType == null || m.getParameterTypes()[0].isAssignableFrom(valueType)) {
|
|
||||||
return m;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts the first character of the given string to lowercase, leaving the rest of the string unchanged.
|
|
||||||
* If the input string is null or empty, it is returned as-is.
|
|
||||||
*
|
|
||||||
* @param str the string to be decapitalized
|
|
||||||
* @return the decapitalized string, or the original string if it is null or empty
|
|
||||||
*/
|
|
||||||
private String decapitalize(String str) {
|
|
||||||
if (str == null || str.isEmpty()) return str;
|
|
||||||
return str.substring(0, 1).toLowerCase() + str.substring(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Capitalizes the first letter of the given string and leaves the rest of the string unchanged.
|
|
||||||
* If the input string is null or empty, it is returned as is.
|
|
||||||
*
|
|
||||||
* @param str the string to be capitalized
|
|
||||||
* @return the input string with its first character converted to uppercase, or the original string if it is null or empty
|
|
||||||
*/
|
|
||||||
private String capitalize(String str) {
|
|
||||||
if (str == null || str.isEmpty()) return str;
|
|
||||||
return str.substring(0, 1).toUpperCase() + str.substring(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the JavaFX property associated with the given field name.
|
|
||||||
*
|
|
||||||
* @param name the name of the field whose associated JavaFX property is to be returned
|
|
||||||
* @return the JavaFX {@link Property} associated with the specified field name,
|
|
||||||
* or null if no property exists for the given name
|
|
||||||
*/
|
|
||||||
public Property<?> getProperty(String name) {
|
|
||||||
return properties.get(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package de.neitzel.fx.component;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generic controller used by the ComponentLoader to bind FXML views
|
|
||||||
* to automatically generated ViewModels based on POJO models.
|
|
||||||
* <p>
|
|
||||||
* This controller provides access to the {@link AutoViewModel}
|
|
||||||
* which contains JavaFX properties derived from the model.
|
|
||||||
*/
|
|
||||||
public class ComponentController {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The automatically generated ViewModel that holds JavaFX properties
|
|
||||||
* for all model fields. It is used to create bindings between
|
|
||||||
* the view and the model.
|
|
||||||
*/
|
|
||||||
@Getter
|
|
||||||
private final AutoViewModel<?> viewModel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new ComponentController instance with the specified AutoViewModel.
|
|
||||||
*
|
|
||||||
* @param viewModel the AutoViewModel containing JavaFX properties derived from the model,
|
|
||||||
* used for binding the view to the model.
|
|
||||||
*/
|
|
||||||
public ComponentController(AutoViewModel<?> viewModel) {
|
|
||||||
this.viewModel = viewModel;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,295 +0,0 @@
|
|||||||
package de.neitzel.fx.component;
|
|
||||||
|
|
||||||
import de.neitzel.fx.component.controls.Binding;
|
|
||||||
import de.neitzel.fx.component.controls.FxmlComponent;
|
|
||||||
import de.neitzel.fx.component.model.BindingData;
|
|
||||||
import javafx.beans.property.Property;
|
|
||||||
import javafx.fxml.FXMLLoader;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.Parent;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ComponentLoader is responsible for loading JavaFX FXML components and binding
|
|
||||||
* them to automatically generated ViewModels based on simple POJO models.
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class ComponentLoader {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The controller object associated with the ComponentLoader.
|
|
||||||
* It is typically used to manage the behavior and interactions of the UI components
|
|
||||||
* defined in an FXML file.
|
|
||||||
*/
|
|
||||||
private Object controller;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default constructor only
|
|
||||||
*/
|
|
||||||
public ComponentLoader() {
|
|
||||||
// default constructor only
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads an FXML file from the specified URL, sets the provided controller, and processes bindings.
|
|
||||||
* This method parses the FXML file, initializes the specified controller, and evaluates binding controls
|
|
||||||
* defined within the FXML namespace.
|
|
||||||
*
|
|
||||||
* @param <T> the type of the root node, which must extend {@code Parent}
|
|
||||||
* @param fxmlUrl the URL of the FXML file to be loaded
|
|
||||||
* @param controller the controller to associate with the FXML file
|
|
||||||
* @param nothing an unused parameter, included for compatibility
|
|
||||||
* @return the root node loaded from the FXML file
|
|
||||||
* @throws IOException if there is an error reading the FXML file
|
|
||||||
*/
|
|
||||||
public static <T extends Parent> T load(URL fxmlUrl, Object controller, @SuppressWarnings("unused") String nothing) throws IOException {
|
|
||||||
FXMLLoader loader = new FXMLLoader(fxmlUrl);
|
|
||||||
loader.setController(controller);
|
|
||||||
T root = loader.load();
|
|
||||||
|
|
||||||
Map<String, Object> namespace = loader.getNamespace();
|
|
||||||
|
|
||||||
// Nach allen BindingControls suchen:
|
|
||||||
List<Binding> bindingControls = collectAllNodes(root).stream()
|
|
||||||
.filter(n -> n instanceof Binding)
|
|
||||||
.map(n -> (Binding) n)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
for (Binding bc : bindingControls) {
|
|
||||||
evaluateBindings(bc.getBindings(), namespace);
|
|
||||||
}
|
|
||||||
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Safely binds two properties bidirectionally while ensuring type compatibility.
|
|
||||||
* If the target property's type is not compatible with the source property's type, the binding will
|
|
||||||
* not be performed, and an error will be logged.
|
|
||||||
*
|
|
||||||
* @param source the source property to bind, must not be null
|
|
||||||
* @param target the target property to bind to; the type will be checked for compatibility at runtime
|
|
||||||
* @param <T> the type of the source property's value
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private static <T> void bindBidirectionalSafe(@NotNull Property<T> source, Property<?> target) {
|
|
||||||
try {
|
|
||||||
Property<T> targetCasted = (Property<T>) target;
|
|
||||||
source.bindBidirectional(targetCasted);
|
|
||||||
} catch (ClassCastException e) {
|
|
||||||
log.error("⚠️ Typkonflikt beim Binding: {} ⇄ {}", source.getClass(), target.getClass(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempts to bind the source property to the target property in a type-safe manner.
|
|
||||||
* If the target property's type is incompatible with the source property's type, the binding operation
|
|
||||||
* will be skipped, and an error message will be logged.
|
|
||||||
*
|
|
||||||
* @param source the source property to bind, must not be null
|
|
||||||
* @param target the target property to which the source will be bound
|
|
||||||
* @param <T> the type of the source property's value
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private static <T> void bindSafe(@NotNull Property<T> source, Property<?> target) {
|
|
||||||
try {
|
|
||||||
Property<T> targetCasted = (Property<T>) target;
|
|
||||||
source.bind(targetCasted);
|
|
||||||
} catch (ClassCastException e) {
|
|
||||||
log.error("⚠️ Typkonflikt beim Binding: {} ⇄ {}", source.getClass(), target.getClass(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Evaluates and establishes bindings between source and target properties based on the provided
|
|
||||||
* bindings list and namespace. Supports unidirectional and bidirectional bindings, ensuring type
|
|
||||||
* compatibility before binding. Logs errors for unsuccessful binding attempts or exceptions.
|
|
||||||
*
|
|
||||||
* @param bindings a list of BindingData objects that define the source, target, and direction for the bindings
|
|
||||||
* @param namespace a map representing the namespace from which expressions in the bindings are resolved
|
|
||||||
*/
|
|
||||||
private static void evaluateBindings(List<BindingData> bindings, Map<String, Object> namespace) {
|
|
||||||
for (var binding : bindings) {
|
|
||||||
try {
|
|
||||||
Object source = resolveExpression(binding.getSource(), namespace);
|
|
||||||
Object target = resolveExpression(binding.getTarget(), namespace);
|
|
||||||
|
|
||||||
if (source instanceof Property<?> sourceProp && target instanceof Property<?> targetProp) {
|
|
||||||
|
|
||||||
Class<?> sourceType = getPropertyType(sourceProp);
|
|
||||||
Class<?> targetType = getPropertyType(targetProp);
|
|
||||||
|
|
||||||
boolean bindableForward = targetType.isAssignableFrom(sourceType);
|
|
||||||
boolean bindableBackward = sourceType.isAssignableFrom(targetType);
|
|
||||||
|
|
||||||
switch (binding.getDirection().toLowerCase()) {
|
|
||||||
case "bidirectional":
|
|
||||||
if (bindableForward && bindableBackward) {
|
|
||||||
bindBidirectionalSafe(sourceProp, targetProp);
|
|
||||||
} else {
|
|
||||||
log.error("⚠️ Kann bidirektionales Binding nicht durchführen: Typen inkompatibel ({} ⇄ {})", sourceType, targetType);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "unidirectional":
|
|
||||||
default:
|
|
||||||
if (bindableForward) {
|
|
||||||
bindSafe(sourceProp, targetProp);
|
|
||||||
} else {
|
|
||||||
log.error("⚠️ Kann unidirektionales Binding nicht durchführen: {} → {} nicht zuweisbar", sourceType, targetType);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Fehler beim Binding: {} → {}", binding.getSource(), binding.getTarget(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines the type of the value held by the specified property.
|
|
||||||
*
|
|
||||||
* @param prop the property whose value type is to be determined, must not be null
|
|
||||||
* @return the class of the property's value type; returns Object.class if the type cannot be determined
|
|
||||||
*/
|
|
||||||
private static Class<?> getPropertyType(Property<?> prop) {
|
|
||||||
try {
|
|
||||||
Method getter = prop.getClass().getMethod("get");
|
|
||||||
return getter.getReturnType();
|
|
||||||
} catch (Exception e) {
|
|
||||||
return Object.class; // Fallback
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves a dot-separated expression (e.g., "viewModel.username") by navigating through the provided namespace
|
|
||||||
* map and invoking the corresponding getter methods to access nested properties.
|
|
||||||
*
|
|
||||||
* @param expr the dot-separated expression to resolve, must not be null
|
|
||||||
* @param namespace a map containing the initial objects to resolve the expression from, must not be null
|
|
||||||
* @return the value resolved from the specified expression
|
|
||||||
* @throws Exception if an error occurs while invoking getter methods or resolving the expression
|
|
||||||
*/
|
|
||||||
private static Object resolveExpression(@NotNull String expr, @NotNull Map<String, Object> namespace) throws Exception {
|
|
||||||
// z.B. "viewModel.username"
|
|
||||||
String[] parts = expr.split("\\.");
|
|
||||||
Object current = namespace.get(parts[0]);
|
|
||||||
for (int i = 1; i < parts.length; i++) {
|
|
||||||
String getter = "get" + Character.toUpperCase(parts[i].charAt(0)) + parts[i].substring(1);
|
|
||||||
current = current.getClass().getMethod(getter).invoke(current);
|
|
||||||
}
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively collects all nodes in a scene graph starting from the given root node.
|
|
||||||
*
|
|
||||||
* @param root the starting node from which all descendant nodes will be collected
|
|
||||||
* @return a list containing all nodes, including the root node and its descendants
|
|
||||||
*/
|
|
||||||
private static @NotNull List<Node> collectAllNodes(Node root) {
|
|
||||||
List<Node> nodes = new ArrayList<>();
|
|
||||||
nodes.add(root);
|
|
||||||
if (root instanceof Parent parent && !(root instanceof FxmlComponent)) {
|
|
||||||
for (Node child : parent.getChildrenUnmodifiable()) {
|
|
||||||
nodes.addAll(collectAllNodes(child));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the controller associated with the ComponentLoader.
|
|
||||||
*
|
|
||||||
* @return the controller object
|
|
||||||
*/
|
|
||||||
public Object getController() {
|
|
||||||
return controller;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads an FXML file and returns the root JavaFX node without binding to a data model.
|
|
||||||
* This method uses the given {@code fxmlPath} to locate and load the FXML file.
|
|
||||||
*
|
|
||||||
* @param fxmlPath the URL of the FXML file to be loaded
|
|
||||||
* @return the root JavaFX node loaded from the specified FXML file
|
|
||||||
*/
|
|
||||||
public Parent load(URL fxmlPath) {
|
|
||||||
return load(null, fxmlPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads an FXML file and binds its elements to a generated ViewModel
|
|
||||||
* based on the given POJO model and specified FXML path.
|
|
||||||
*
|
|
||||||
* @param model the data model (POJO) to bind to the UI
|
|
||||||
* @param fxmlPath the URL path to the FXML file
|
|
||||||
* @return the root JavaFX node loaded from FXML
|
|
||||||
* @throws RuntimeException if the FXML could not be loaded
|
|
||||||
*/
|
|
||||||
public Parent load(Object model, URL fxmlPath) {
|
|
||||||
try {
|
|
||||||
AutoViewModel<?> viewModel = new AutoViewModel<>(model);
|
|
||||||
FXMLLoader loader = new FXMLLoader(fxmlPath);
|
|
||||||
loader.setControllerFactory(type -> {
|
|
||||||
Objects.requireNonNull(type);
|
|
||||||
return new ComponentController(viewModel);
|
|
||||||
});
|
|
||||||
Parent root = loader.load();
|
|
||||||
controller = loader.getController();
|
|
||||||
return root;
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException("unable to load fxml: " + fxmlPath, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads an FXML file and returns its root node.
|
|
||||||
*
|
|
||||||
* @param fxmlPath the relative path to the FXML file
|
|
||||||
* @return the root JavaFX node loaded from the FXML file
|
|
||||||
*/
|
|
||||||
public Parent load(String fxmlPath) {
|
|
||||||
return load(null, fxmlPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads an FXML file and binds its elements to a generated ViewModel
|
|
||||||
* based on the given POJO model.
|
|
||||||
*
|
|
||||||
* @param model the data model (POJO) to bind to the UI
|
|
||||||
* @param fxmlPath the path to the FXML file
|
|
||||||
* @return the root JavaFX node loaded from FXML
|
|
||||||
*/
|
|
||||||
public Parent load(Object model, String fxmlPath) {
|
|
||||||
return load(model, getClass().getResource(fxmlPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binds a JavaFX UI control to a ViewModel property according to the specified direction.
|
|
||||||
*
|
|
||||||
* @param node the UI node (e.g., TextField)
|
|
||||||
* @param vmProp the ViewModel property to bind to
|
|
||||||
* @param direction the direction of the binding (e.g., "bidirectional", "read")
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
private void bindNodeToProperty(javafx.scene.Node node, Property<?> vmProp, String direction) {
|
|
||||||
if (node instanceof javafx.scene.control.TextField tf && vmProp instanceof javafx.beans.property.StringProperty sp) {
|
|
||||||
if ("bidirectional".equalsIgnoreCase(direction)) {
|
|
||||||
tf.textProperty().bindBidirectional(sp);
|
|
||||||
} else if ("read".equalsIgnoreCase(direction)) {
|
|
||||||
tf.textProperty().bind(sp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Additional control types (e.g., CheckBox, ComboBox) can be added here
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
package de.neitzel.fx.component.controls;
|
|
||||||
|
|
||||||
import de.neitzel.fx.component.model.BindingData;
|
|
||||||
import javafx.collections.FXCollections;
|
|
||||||
import javafx.collections.ObservableList;
|
|
||||||
import javafx.scene.layout.Region;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The BindingControl class represents a UI control that manages a list
|
|
||||||
* of {@link BindingData} objects. It extends the {@link Region} class and
|
|
||||||
* provides functionality to bind and monitor connections between source
|
|
||||||
* and target properties.
|
|
||||||
* <p>
|
|
||||||
* The primary purpose of this control is to maintain an observable list
|
|
||||||
* of bindings, allowing developers to track or adjust the linked properties
|
|
||||||
* dynamically.
|
|
||||||
* <p>
|
|
||||||
* The internal list of bindings is implemented as an {@link ObservableList},
|
|
||||||
* allowing property change notifications to be easily monitored for UI
|
|
||||||
* updates or other reactive behaviors.
|
|
||||||
* <p>
|
|
||||||
* This class serves as an organizational component and does not provide
|
|
||||||
* any user interaction by default.
|
|
||||||
*/
|
|
||||||
public class Binding extends Region {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents an observable list of {@link BindingData} objects contained within the
|
|
||||||
* {@link Binding} instance. This list is utilized to manage and monitor
|
|
||||||
* the bindings between source and target properties dynamically.
|
|
||||||
* <p>
|
|
||||||
* The list is implemented as an {@link ObservableList}, allowing changes in the
|
|
||||||
* collection to be observed and reacted to, such as triggering UI updates or
|
|
||||||
* responding to binding modifications.
|
|
||||||
* <p>
|
|
||||||
* This field is initialized as an empty list using {@link FXCollections#observableArrayList()}.
|
|
||||||
* It is declared as final to ensure its reference cannot be changed, while the
|
|
||||||
* contents of the list remain mutable.
|
|
||||||
*/
|
|
||||||
private final ObservableList<BindingData> bindings = FXCollections.observableArrayList();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new instance of the BindingControl class.
|
|
||||||
* <p>
|
|
||||||
* This default constructor initializes the BindingControl without
|
|
||||||
* any pre-configured bindings. The instance will contain an empty
|
|
||||||
* {@link ObservableList} of {@link BindingData} objects, which can later
|
|
||||||
* be populated as needed.
|
|
||||||
* <p>
|
|
||||||
* The constructor does not perform additional setup or initialization,
|
|
||||||
* allowing the class to be extended or customized as necessary.
|
|
||||||
*/
|
|
||||||
public Binding() {
|
|
||||||
// Empty, the ComponentLoader is used to work on the bindings.
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the observable list of {@code Binding} objects associated with this control.
|
|
||||||
* The returned list allows monitoring and management of the bindings maintained
|
|
||||||
* by the {@code BindingControl}.
|
|
||||||
*
|
|
||||||
* @return an {@code ObservableList<Binding>} containing the bindings managed by this control
|
|
||||||
*/
|
|
||||||
public ObservableList<BindingData> getBindings() {
|
|
||||||
return bindings;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
package de.neitzel.fx.component.controls;
|
|
||||||
|
|
||||||
import de.neitzel.fx.component.ComponentLoader;
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
|
||||||
import javafx.beans.property.StringProperty;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.Parent;
|
|
||||||
import javafx.scene.layout.StackPane;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A reusable JavaFX component that loads and embeds an FXML-defined UI fragment.
|
|
||||||
* <p>
|
|
||||||
* The component exposes three properties:
|
|
||||||
* <ul>
|
|
||||||
* <li>fxml: the resource path to the FXML file to load (relative to the component class)</li>
|
|
||||||
* <li>direction: a textual hint that describes binding directionality (default "unidirectional")</li>
|
|
||||||
* <li>data: an arbitrary object that will be injected into the loaded controller if a suitable setter exists</li>
|
|
||||||
* </ul>
|
|
||||||
* <p>
|
|
||||||
* When the {@code fxml} property changes the FXML is loaded via {@link ComponentLoader}. After loading the
|
|
||||||
* controller instance available from the loader, this component attempts to inject the object from the
|
|
||||||
* {@code data} property by calling a matching {@code setData(...)} method via reflection.
|
|
||||||
*/
|
|
||||||
public class FxmlComponent extends StackPane {
|
|
||||||
|
|
||||||
private final StringProperty fxml = new SimpleStringProperty();
|
|
||||||
|
|
||||||
private final StringProperty direction = new SimpleStringProperty("unidirectional");
|
|
||||||
|
|
||||||
private final ObjectProperty<Object> data = new SimpleObjectProperty<>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new instance of {@code FxmlComponent}.
|
|
||||||
* <p>
|
|
||||||
* The constructor registers listeners on the {@code fxml} and {@code data} properties to
|
|
||||||
* perform lazy loading and data injection when values change.
|
|
||||||
*/
|
|
||||||
public FxmlComponent() {
|
|
||||||
fxml.addListener((obs, oldVal, newVal) -> load());
|
|
||||||
data.addListener((obs, oldVal, newVal) -> injectData());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads the FXML specified by the {@link #fxml} property and replaces the current children
|
|
||||||
* with the loaded root node. If no FXML is configured or the path is blank, the method returns
|
|
||||||
* without performing any action.
|
|
||||||
*/
|
|
||||||
private void load() {
|
|
||||||
if (getFxml() == null || getFxml().isBlank()) return;
|
|
||||||
ComponentLoader loader = new ComponentLoader();
|
|
||||||
Parent content = loader.load(getClass().getResource(getFxml()));
|
|
||||||
|
|
||||||
getChildren().setAll(content);
|
|
||||||
|
|
||||||
Object controller = loader.getController();
|
|
||||||
if (controller != null && getData() != null) {
|
|
||||||
injectDataToController(controller, getData());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempts to inject the {@link #data} object into the controller of the currently loaded child node,
|
|
||||||
* if a controller is present and defines a single-argument method named {@code setData}.
|
|
||||||
*/
|
|
||||||
private void injectData() {
|
|
||||||
if (!getChildren().isEmpty() && getData() != null) {
|
|
||||||
Object controller = getControllerFromChild(getChildren().get(0));
|
|
||||||
if (controller != null) {
|
|
||||||
injectDataToController(controller, getData());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the configured FXML resource path.
|
|
||||||
*
|
|
||||||
* @return the FXML path or {@code null} if not set
|
|
||||||
*/
|
|
||||||
public String getFxml() {
|
|
||||||
return fxml.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the FXML resource path to load. Changing this value triggers a reload of the component content.
|
|
||||||
*
|
|
||||||
* @param fxml resource path relative to the component class
|
|
||||||
*/
|
|
||||||
public void setFxml(String fxml) {
|
|
||||||
this.fxml.set(fxml);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the data object that will be injected into the loaded controller when available.
|
|
||||||
*
|
|
||||||
* @return the data object or {@code null}
|
|
||||||
*/
|
|
||||||
public Object getData() {
|
|
||||||
return data.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the data object to be injected into the controller of the loaded FXML.
|
|
||||||
* Changing the value will attempt injection into the currently loaded controller.
|
|
||||||
*
|
|
||||||
* @param data the data object to inject
|
|
||||||
*/
|
|
||||||
public void setData(Object data) {
|
|
||||||
this.data.set(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Uses reflection to find a single-argument method named {@code setData} on the controller and
|
|
||||||
* invokes it with the provided {@code dataObject}.
|
|
||||||
*
|
|
||||||
* @param controller the controller instance to inject into
|
|
||||||
* @param dataObject the object to inject
|
|
||||||
*/
|
|
||||||
private void injectDataToController(Object controller, Object dataObject) {
|
|
||||||
// Daten-Objekt per Reflection zuweisen
|
|
||||||
// Beispiel: Controller hat `setData(User data)`
|
|
||||||
Arrays.stream(controller.getClass().getMethods())
|
|
||||||
.filter(m -> m.getName().equals("setData") && m.getParameterCount() == 1)
|
|
||||||
.findFirst()
|
|
||||||
.ifPresent(method -> {
|
|
||||||
try {
|
|
||||||
method.invoke(controller, dataObject);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Optional: automatisch Binding ausführen (s. u.)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts the controller instance from a child node's properties map if present.
|
|
||||||
*
|
|
||||||
* @param node the child node to inspect
|
|
||||||
* @return the controller object stored under the property key {@code "fx:controller"} or {@code null}
|
|
||||||
*/
|
|
||||||
private Object getControllerFromChild(Node node) {
|
|
||||||
if (node.getProperties().containsKey("fx:controller")) {
|
|
||||||
return node.getProperties().get("fx:controller");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the JavaFX property representing the configured FXML path.
|
|
||||||
*
|
|
||||||
* @return the FXML property
|
|
||||||
*/
|
|
||||||
public StringProperty fxmlProperty() {
|
|
||||||
return fxml;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the JavaFX property representing the binding direction hint.
|
|
||||||
*
|
|
||||||
* @return the direction property
|
|
||||||
*/
|
|
||||||
public StringProperty directionProperty() {
|
|
||||||
return direction;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the configured direction string.
|
|
||||||
*
|
|
||||||
* @return the direction hint (e.g., "unidirectional")
|
|
||||||
*/
|
|
||||||
public String getDirection() {
|
|
||||||
return direction.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the direction hint used by higher-level binding logic.
|
|
||||||
*
|
|
||||||
* @param direction textual direction hint
|
|
||||||
*/
|
|
||||||
public void setDirection(String direction) {
|
|
||||||
this.direction.set(direction);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the JavaFX property representing the data object used for injection.
|
|
||||||
*
|
|
||||||
* @return the data property
|
|
||||||
*/
|
|
||||||
public ObjectProperty<Object> dataProperty() {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
package de.neitzel.fx.component.model;
|
|
||||||
|
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
|
||||||
import javafx.beans.property.StringProperty;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Binding class represents a connection between a source and a target,
|
|
||||||
* with an associated direction. It is typically used to define a binding
|
|
||||||
* relationship that determines how data flows between these two entities.
|
|
||||||
* <p>
|
|
||||||
* The class provides three properties:
|
|
||||||
* - direction: Represents the type of the binding. Defaults to "unidirectional".
|
|
||||||
* - source: Represents the source of the binding.
|
|
||||||
* - target: Represents the target of the binding.
|
|
||||||
* <p>
|
|
||||||
* It uses JavaFX property types, allowing these properties to be observed
|
|
||||||
* for changes.
|
|
||||||
*/
|
|
||||||
public class BindingData {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents the direction of the binding in the {@code Binding} class.
|
|
||||||
* It determines whether the binding is unidirectional or bidirectional.
|
|
||||||
* The default value is "unidirectional".
|
|
||||||
* <p>
|
|
||||||
* This property is observed for changes, enabling dynamic updates
|
|
||||||
* within the JavaFX property system.
|
|
||||||
*/
|
|
||||||
private StringProperty direction = new SimpleStringProperty("unidirectional");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents the source of the binding. This property holds a string value
|
|
||||||
* that specifies the originating object or identifier in the binding connection.
|
|
||||||
* It can be observed for changes, allowing updates to the binding relationship
|
|
||||||
* when the source value is modified.
|
|
||||||
*/
|
|
||||||
private StringProperty source = new SimpleStringProperty();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents the target of the binding in the Binding class.
|
|
||||||
* This property holds the target value, which can be observed for changes.
|
|
||||||
*/
|
|
||||||
private StringProperty target = new SimpleStringProperty();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default constructor only
|
|
||||||
*/
|
|
||||||
public BindingData() {
|
|
||||||
// default constructor only
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the current direction of the binding.
|
|
||||||
* The direction determines how data flows between
|
|
||||||
* the source and the target, and typically defaults to "unidirectional".
|
|
||||||
*
|
|
||||||
* @return the current direction of the binding
|
|
||||||
*/
|
|
||||||
public String getDirection() {
|
|
||||||
return direction.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the direction of the binding.
|
|
||||||
*
|
|
||||||
* @param dir the new direction to set for the binding
|
|
||||||
*/
|
|
||||||
public void setDirection(String dir) {
|
|
||||||
direction.set(dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the current source value of the binding.
|
|
||||||
*
|
|
||||||
* @return the source value as a String.
|
|
||||||
*/
|
|
||||||
public String getSource() {
|
|
||||||
return source.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the value of the source property for this binding.
|
|
||||||
*
|
|
||||||
* @param source the new source value to be set
|
|
||||||
*/
|
|
||||||
public void setSource(String source) {
|
|
||||||
this.source.set(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the current value of the target property.
|
|
||||||
*
|
|
||||||
* @return the value of the target property as a String.
|
|
||||||
*/
|
|
||||||
public String getTarget() {
|
|
||||||
return target.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the target property of the binding.
|
|
||||||
*
|
|
||||||
* @param target the new value to set for the target property
|
|
||||||
*/
|
|
||||||
public void setTarget(String target) {
|
|
||||||
this.target.set(target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
package de.neitzel.fx.injectfx;
|
|
||||||
|
|
||||||
import de.neitzel.injection.ComponentData;
|
|
||||||
import de.neitzel.injection.ComponentScanner;
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
import java.lang.reflect.Constructor;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manages the creation and storage of singleton instances for all @FXMLComponent classes.
|
|
||||||
* It ensures that each component is instantiated only once and resolves dependencies recursively.
|
|
||||||
*/
|
|
||||||
public class FXMLComponentInstances {
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
/** Map holding instances of all @FXMLComponent classes, indexed by class and its unique superclasses/interfaces. */
|
|
||||||
private final Map<Class<?>, Object> instanceMap = new HashMap<>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The InjectableComponents instance that provides information about instantiable components.
|
|
||||||
*/
|
|
||||||
private final ComponentScanner injectableScanner;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs an FXMLComponentInstances manager and initializes all component instances.
|
|
||||||
*
|
|
||||||
* @param injectableScanner The InjectableComponents instance containing resolved component types.
|
|
||||||
*/
|
|
||||||
public FXMLComponentInstances(ComponentScanner injectableScanner) {
|
|
||||||
this.injectableScanner = injectableScanner;
|
|
||||||
createAllInstances();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates instances for all registered @FXMLComponent classes.
|
|
||||||
*/
|
|
||||||
private void createAllInstances() {
|
|
||||||
for (Class<?> componentClass : injectableScanner.getInstantiableComponents().keySet()) {
|
|
||||||
getInstance(componentClass); // Ensures each component is instantiated once
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves an instance of a given component class, creating it if necessary.
|
|
||||||
*
|
|
||||||
* @param componentClass The class for which an instance is needed.
|
|
||||||
* @return An instance of the requested class.
|
|
||||||
*/
|
|
||||||
public Object getInstance(Class<?> componentClass) {
|
|
||||||
if (instanceMap.containsKey(componentClass)) {
|
|
||||||
return instanceMap.get(componentClass);
|
|
||||||
}
|
|
||||||
|
|
||||||
Class<?> concreteClass = injectableScanner.getInstantiableComponents().get(componentClass).getType();
|
|
||||||
if (concreteClass == null) {
|
|
||||||
throw new IllegalStateException("No concrete implementation found for: " + componentClass.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
Object instance = createInstance(concreteClass);
|
|
||||||
registerInstance(concreteClass, instance);
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an instance of the given class by selecting the best constructor.
|
|
||||||
*
|
|
||||||
* @param concreteClass The class to instantiate.
|
|
||||||
* @return A new instance of the class.
|
|
||||||
*/
|
|
||||||
private Object createInstance(Class<?> concreteClass) {
|
|
||||||
try {
|
|
||||||
Constructor<?> bestConstructor = findBestConstructor(concreteClass);
|
|
||||||
if (bestConstructor == null) {
|
|
||||||
throw new IllegalStateException("No suitable constructor found for: " + concreteClass.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
Class<?>[] paramTypes = bestConstructor.getParameterTypes();
|
|
||||||
Object[] params = Arrays.stream(paramTypes)
|
|
||||||
.map(this::getInstance) // Recursively resolve dependencies
|
|
||||||
.toArray();
|
|
||||||
|
|
||||||
return bestConstructor.newInstance(params);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException("Failed to instantiate " + concreteClass.getName(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds the best constructor for a given class by checking which one has resolvable parameters.
|
|
||||||
*
|
|
||||||
* @param concreteClass The class to analyze.
|
|
||||||
* @return The best constructor or {@code null} if none can be used.
|
|
||||||
*/
|
|
||||||
private Constructor<?> findBestConstructor(Class<?> concreteClass) {
|
|
||||||
Constructor<?>[] constructors = concreteClass.getConstructors();
|
|
||||||
|
|
||||||
// Prefer constructors with all parameters resolvable
|
|
||||||
for (Constructor<?> constructor : constructors) {
|
|
||||||
if (canUseConstructor(constructor)) {
|
|
||||||
return constructor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether a given constructor can be used by ensuring all parameters are known.
|
|
||||||
*
|
|
||||||
* @param constructor The constructor to check.
|
|
||||||
* @return {@code true} if all parameters can be resolved, otherwise {@code false}.
|
|
||||||
*/
|
|
||||||
private boolean canUseConstructor(Constructor<?> constructor) {
|
|
||||||
return Arrays.stream(constructor.getParameterTypes())
|
|
||||||
.allMatch(injectableScanner.getInstantiableComponents()::containsKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers an instance in the map for all unique interfaces and superclasses it implements.
|
|
||||||
*
|
|
||||||
* @param concreteClass The concrete component class.
|
|
||||||
* @param instance The instance to store.
|
|
||||||
*/
|
|
||||||
private void registerInstance(Class<?> concreteClass, Object instance) {
|
|
||||||
instanceMap.put(concreteClass, instance);
|
|
||||||
|
|
||||||
for (Map.Entry<Class<?>, ComponentData> entry : injectableScanner.getInstantiableComponents().entrySet()) {
|
|
||||||
if (entry.getValue().getType().equals(concreteClass)) {
|
|
||||||
instanceMap.put(entry.getKey(), instance);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
package de.neitzel.fx.injectfx;
|
|
||||||
|
|
||||||
import javafx.util.Callback;
|
|
||||||
|
|
||||||
import java.lang.reflect.Constructor;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The InjectingControllerFactory is responsible for creating controller instances for JavaFX that
|
|
||||||
* support dependency injection. It uses a parameter map to resolve and supply dependencies to
|
|
||||||
* controller constructors dynamically during instantiation.
|
|
||||||
*
|
|
||||||
* <p>This class analyzes available constructors of a requested controller class at runtime and
|
|
||||||
* selects the first constructor whose parameter types are all present in the internal parameter map.
|
|
||||||
* The corresponding instances are retrieved and passed to the constructor to create the controller.
|
|
||||||
*
|
|
||||||
* <p>It implements the {@link javafx.util.Callback} interface to integrate with the JavaFX
|
|
||||||
* {@link javafx.fxml.FXMLLoader} controller factory mechanism.
|
|
||||||
*/
|
|
||||||
public class InjectingControllerFactory implements Callback<Class<?>, Object> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A map that stores class-to-object mappings used for dependency injection
|
|
||||||
* in controller instantiation. Each key represents a parameter type and the
|
|
||||||
* associated value is the instance to be injected for that type.
|
|
||||||
*/
|
|
||||||
private final Map<Class<?>, Object> parameterMap = new HashMap<>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default constructor only
|
|
||||||
*/
|
|
||||||
public InjectingControllerFactory() {
|
|
||||||
// default constructor only
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a mapping between a class and its corresponding object instance
|
|
||||||
* to the parameter map used for dependency injection.
|
|
||||||
*
|
|
||||||
* @param clazz The class type to be associated with the provided object instance.
|
|
||||||
* @param object The object instance to be injected for the specified class type.
|
|
||||||
*/
|
|
||||||
public void addInjectingData(Class<?> clazz, Object object) {
|
|
||||||
parameterMap.put(clazz, object);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an instance of a controller class using a constructor that matches the dependencies
|
|
||||||
* defined in the parameter map. The method dynamically analyzes the constructors of the given
|
|
||||||
* class and attempts to instantiate the class by injecting required dependencies.
|
|
||||||
*
|
|
||||||
* @param controllerClass the class of the controller to be instantiated
|
|
||||||
* @return an instance of the specified controller class
|
|
||||||
* @throws RuntimeException if an error occurs while creating the controller instance or if no suitable constructor is found
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public Object call(Class<?> controllerClass) {
|
|
||||||
try {
|
|
||||||
Optional<Constructor<?>> bestConstructor = Stream.of(controllerClass.getConstructors())
|
|
||||||
.filter(constructor -> canBeInstantiated(constructor, parameterMap))
|
|
||||||
.findFirst();
|
|
||||||
|
|
||||||
if (bestConstructor.isPresent()) {
|
|
||||||
Constructor<?> constructor = bestConstructor.get();
|
|
||||||
Object[] parameters = Stream.of(constructor.getParameterTypes())
|
|
||||||
.map(parameterMap::get)
|
|
||||||
.toArray();
|
|
||||||
|
|
||||||
return constructor.newInstance(parameters);
|
|
||||||
} else {
|
|
||||||
throw new IllegalStateException("Kein passender Konstruktor gefunden für " + controllerClass.getName());
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException("Fehler beim Erstellen der Controller-Instanz für " + controllerClass.getName(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if a given constructor can be instantiated using the provided parameter map.
|
|
||||||
* This method checks if the parameter map contains entries for all parameter types required
|
|
||||||
* by the specified constructor.
|
|
||||||
*
|
|
||||||
* @param constructor The constructor to be evaluated for instantiability.
|
|
||||||
* @param parameterMap A map where keys are parameter types and values are the corresponding
|
|
||||||
* instances available for injection.
|
|
||||||
* @return {@code true} if the constructor can be instantiated with the given parameter map;
|
|
||||||
* {@code false} otherwise.
|
|
||||||
*/
|
|
||||||
private boolean canBeInstantiated(Constructor<?> constructor, Map<Class<?>, Object> parameterMap) {
|
|
||||||
return Stream.of(constructor.getParameterTypes()).allMatch(parameterMap::containsKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
package de.neitzel.fx.injectfx;
|
|
||||||
|
|
||||||
import de.neitzel.injection.ComponentScanner;
|
|
||||||
import javafx.fxml.FXMLLoader;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.URL;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The InjectingFXMLLoader class provides a custom implementation of JavaFX's FXMLLoader
|
|
||||||
* that supports dependency injection. It facilitates the loading of FXML files while
|
|
||||||
* dynamically injecting dependencies into controllers during the loading process.
|
|
||||||
* <p>
|
|
||||||
* This class utilizes the InjectingControllerFactory to enable seamless integration
|
|
||||||
* of dependency injection with JavaFX controllers. Dependencies can be added to the
|
|
||||||
* controller factory, and the loader will use this factory to instantiate controllers
|
|
||||||
* with the appropriate dependencies.
|
|
||||||
* <p>
|
|
||||||
* Features of this loader include:
|
|
||||||
* - Support for dependency injection into JavaFX controllers by using a custom factory.
|
|
||||||
* - Adding custom dependency mappings at runtime.
|
|
||||||
* - Scanning and initializing injectable components from a specified package.
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class InjectingFXMLLoader {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents an instance of the {@link InjectingControllerFactory}, which is used for creating
|
|
||||||
* and managing controller instances with dependency injection capabilities in a JavaFX application.
|
|
||||||
* <p>
|
|
||||||
* This factory facilitates the injection of dependencies by dynamically resolving and supplying
|
|
||||||
* required objects during controller instantiation. It plays a critical role in enabling seamless
|
|
||||||
* integration of dependency injection with JavaFX's {@link FXMLLoader}.
|
|
||||||
* <p>
|
|
||||||
* The `controllerFactory` is used internally by the {@link InjectingFXMLLoader} to provide a consistent
|
|
||||||
* and extensible mechanism for controller creation while maintaining loose coupling and enhancing testability.
|
|
||||||
* <p>
|
|
||||||
* Key responsibilities:
|
|
||||||
* - Manages a mapping of classes to their injectable instances required for controller instantiation.
|
|
||||||
* - Dynamically analyzes and invokes appropriate constructors for controllers based on the availability
|
|
||||||
* of dependencies.
|
|
||||||
* - Ensures that controllers are created with required dependencies, preventing manual resolution of injections.
|
|
||||||
* <p>
|
|
||||||
* This variable is initialized in the {@link InjectingFXMLLoader} and can be extended with additional
|
|
||||||
* mappings at runtime using relevant methods.
|
|
||||||
*/
|
|
||||||
private final InjectingControllerFactory controllerFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default constructor for the InjectingFXMLLoader class.
|
|
||||||
* This initializes the loader with a new instance of the {@link InjectingControllerFactory},
|
|
||||||
* which is used to provide dependency injection capabilities for JavaFX controllers.
|
|
||||||
* <p>
|
|
||||||
* The {@link InjectingControllerFactory} allows for the registration and dynamic injection
|
|
||||||
* of dependencies into controllers when they are instantiated during the FXML loading process.
|
|
||||||
*/
|
|
||||||
public InjectingFXMLLoader() {
|
|
||||||
controllerFactory = new InjectingControllerFactory();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new instance of the InjectingFXMLLoader class with a specified
|
|
||||||
* InjectingControllerFactory for dependency injection. This enables the FXMLLoader
|
|
||||||
* to use the provided controller factory to instantiate controllers with their
|
|
||||||
* required dependencies during the FXML loading process.
|
|
||||||
*
|
|
||||||
* @param controllerFactory The controller factory responsible for creating controller
|
|
||||||
* instances with dependency injection support.
|
|
||||||
*/
|
|
||||||
public InjectingFXMLLoader(InjectingControllerFactory controllerFactory) {
|
|
||||||
this.controllerFactory = controllerFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs an instance of `InjectingFXMLLoader` and initializes the component scanner
|
|
||||||
* for dependency injection based on components found within a specified package.
|
|
||||||
* The scanned components are analyzed and their instances are prepared for injection
|
|
||||||
* using the internal `InjectingControllerFactory`.
|
|
||||||
*
|
|
||||||
* @param packageName The package name to be scanned for injectable components.
|
|
||||||
* Classes within the specified package will be identified
|
|
||||||
* as potential dependencies and made available for injection
|
|
||||||
* into JavaFX controllers during FXML loading.
|
|
||||||
*/
|
|
||||||
public InjectingFXMLLoader(String packageName) {
|
|
||||||
controllerFactory = new InjectingControllerFactory();
|
|
||||||
ComponentScanner scanner = new ComponentScanner(packageName);
|
|
||||||
FXMLComponentInstances instances = new FXMLComponentInstances(scanner);
|
|
||||||
addInjectingData(instances);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds all injectable data from the given {@code FXMLComponentInstances} to the controller factory.
|
|
||||||
* Iterates through the classes in the instance map and delegates adding each class-instance pair
|
|
||||||
* to the {@link #addInjectingData(Class, Object)} method.
|
|
||||||
*
|
|
||||||
* @param instances An {@code FXMLComponentInstances} object containing the mapping of classes
|
|
||||||
* to their respective singleton instances. This data represents the components
|
|
||||||
* available for dependency injection.
|
|
||||||
*/
|
|
||||||
private void addInjectingData(FXMLComponentInstances instances) {
|
|
||||||
for (var clazz : instances.getInstanceMap().keySet()) {
|
|
||||||
addInjectingData(clazz, instances.getInstance(clazz));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a specific class-object mapping to the controller factory for dependency injection.
|
|
||||||
* This method allows the association of a given class type with its corresponding instance,
|
|
||||||
* enabling dependency injection into controllers during the FXML loading process.
|
|
||||||
*
|
|
||||||
* @param clazz The class type to be associated with the provided object instance.
|
|
||||||
* @param object The object instance to be injected for the specified class type.
|
|
||||||
*/
|
|
||||||
public void addInjectingData(Class<?> clazz, Object object) {
|
|
||||||
controllerFactory.addInjectingData(clazz, object);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads an FXML resource from the provided URL and injects dependencies into its controller
|
|
||||||
* using the configured {@code InjectingControllerFactory}.
|
|
||||||
*
|
|
||||||
* @param url the URL of the FXML file to be loaded
|
|
||||||
* @param <T> the type of the root element defined in the FXML
|
|
||||||
* @return the root element of the FXML file
|
|
||||||
* @throws IOException if an error occurs during the loading of the FXML file
|
|
||||||
*/
|
|
||||||
public <T> T load(URL url) throws IOException {
|
|
||||||
FXMLLoader loader = new FXMLLoader(url);
|
|
||||||
loader.setControllerFactory(controllerFactory);
|
|
||||||
return loader.load();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
package de.neitzel.fx.mvvm;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enum representing the direction of data binding between a JavaFX control and a ViewModel property.
|
|
||||||
* - READ: one-way binding from ViewModel to Control.
|
|
||||||
* - WRITE: one-way binding from Control to ViewModel.
|
|
||||||
* - BIDIRECTIONAL: two-way binding between Control and ViewModel.
|
|
||||||
*/
|
|
||||||
public enum BindDirection {
|
|
||||||
/**
|
|
||||||
* One-way binding from the ViewModel to the JavaFX control.
|
|
||||||
* The control reflects changes in the ViewModel but does not update it.
|
|
||||||
*/
|
|
||||||
READ,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* One-way binding from the JavaFX control to the ViewModel.
|
|
||||||
* The ViewModel is updated when the control changes, but not vice versa.
|
|
||||||
*/
|
|
||||||
WRITE,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Two-way binding between the JavaFX control and the ViewModel.
|
|
||||||
* Changes in either are reflected in the other.
|
|
||||||
*/
|
|
||||||
BIDIRECTIONAL;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a string to determine the corresponding BindDirection.
|
|
||||||
* Defaults to BIDIRECTIONAL if the input is null or empty.
|
|
||||||
*
|
|
||||||
* @param value the string representation of the direction
|
|
||||||
* @return the corresponding BindDirection enum constant
|
|
||||||
* @throws IllegalArgumentException if the string does not match any valid direction
|
|
||||||
*/
|
|
||||||
public static BindDirection fromString(String value) {
|
|
||||||
if (value == null || value.isEmpty()) {
|
|
||||||
return BIDIRECTIONAL;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (BindDirection direction : BindDirection.values()) {
|
|
||||||
if (direction.name().equalsIgnoreCase(value)) {
|
|
||||||
return direction;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new IllegalArgumentException("Not a valid direction: " + value);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
package de.neitzel.fx.mvvm;
|
|
||||||
|
|
||||||
import javafx.beans.property.Property;
|
|
||||||
import javafx.beans.property.StringProperty;
|
|
||||||
import javafx.fxml.FXMLLoader;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.Parent;
|
|
||||||
import javafx.scene.control.TextInputControl;
|
|
||||||
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom FXMLLoader that binds JavaFX controls to a GenericViewModel using metadata from FXML.
|
|
||||||
* It supports automatic binding setup based on properties defined in the FXML's node properties.
|
|
||||||
*
|
|
||||||
* @param <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;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new instance of the BindingAwareFXMLLoader class.
|
|
||||||
* This loader is designed to load FXML files and automatically bind UI elements
|
|
||||||
* to the properties of the provided model using a binding-aware approach.
|
|
||||||
*
|
|
||||||
* @param model the model object to be used for binding to the UI components
|
|
||||||
*/
|
|
||||||
public BindingAwareFXMLLoader(T model) {
|
|
||||||
this.model = model;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads an FXML file and performs automatic binding setup using the GenericViewModel.
|
|
||||||
*
|
|
||||||
* @param fxml the input stream of the FXML file
|
|
||||||
* @return the root node of the loaded scene graph
|
|
||||||
* @throws Exception if loading the FXML or binding fails
|
|
||||||
*/
|
|
||||||
public Parent load(InputStream fxml) throws Exception {
|
|
||||||
FXMLLoader loader = new FXMLLoader();
|
|
||||||
loader.setControllerFactory(param -> {
|
|
||||||
GenericViewController<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);
|
|
||||||
|
|
||||||
switch (node) {
|
|
||||||
case TextInputControl control -> {
|
|
||||||
Property<String> prop = viewModel.property(StringProperty.class, propertyName);
|
|
||||||
bind(control.textProperty(), prop, direction);
|
|
||||||
}
|
|
||||||
case javafx.scene.control.Label label -> {
|
|
||||||
Property<String> prop = viewModel.property(StringProperty.class, propertyName);
|
|
||||||
bind(label.textProperty(), prop, direction);
|
|
||||||
}
|
|
||||||
case javafx.scene.control.CheckBox checkBox -> {
|
|
||||||
Property<Boolean> prop = viewModel.property(javafx.beans.property.BooleanProperty.class, propertyName);
|
|
||||||
bind(checkBox.selectedProperty(), prop, direction);
|
|
||||||
}
|
|
||||||
case javafx.scene.control.Slider slider -> {
|
|
||||||
Property<Number> prop = viewModel.property(javafx.beans.property.DoubleProperty.class, propertyName);
|
|
||||||
bind(slider.valueProperty(), prop, direction);
|
|
||||||
}
|
|
||||||
case 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);
|
|
||||||
}
|
|
||||||
default -> {
|
|
||||||
// ignore unsupported nodes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binds two JavaFX properties according to the specified binding direction.
|
|
||||||
*
|
|
||||||
* @param controlProperty the property of the control (e.g. TextField.textProperty)
|
|
||||||
* @param modelProperty the property from the ViewModel
|
|
||||||
* @param direction the direction of the binding
|
|
||||||
* @param <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"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
package de.neitzel.fx.mvvm;
|
|
||||||
|
|
||||||
import javafx.fxml.Initializable;
|
|
||||||
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.ResourceBundle;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A generic JavaFX controller that holds a GenericViewModel for the given model type.
|
|
||||||
* Intended for use with a custom FXMLLoader that performs automatic property bindings.
|
|
||||||
*
|
|
||||||
* @param <T> the type of the model
|
|
||||||
*/
|
|
||||||
public class GenericViewController<T> implements Initializable {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The GenericViewModel instance wrapping the underlying model.
|
|
||||||
*/
|
|
||||||
private GenericViewModel<T> viewModel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new controller instance in an uninitialized state.
|
|
||||||
*
|
|
||||||
* <p>The constructor does not set a model. Call {@link #setModel(Object)} to
|
|
||||||
* attach a model instance before performing view bindings or accessing the view model via
|
|
||||||
* {@link #getViewModel()}.</p>
|
|
||||||
*/
|
|
||||||
public GenericViewController() {
|
|
||||||
// default constructor only ...
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the original model instance from the ViewModel.
|
|
||||||
*
|
|
||||||
* @return the model instance
|
|
||||||
*/
|
|
||||||
public T getModel() {
|
|
||||||
return viewModel.getModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the model for this controller by wrapping it in a GenericViewModel.
|
|
||||||
*
|
|
||||||
* @param model the model to be used by the ViewModel
|
|
||||||
*/
|
|
||||||
public void setModel(T model) {
|
|
||||||
this.viewModel = new GenericViewModel<>(model);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Standard JavaFX initialize method. No-op, as actual binding is done externally via FXMLLoader extension.
|
|
||||||
*
|
|
||||||
* @param location the location used to resolve relative paths for the root object, or null if unknown
|
|
||||||
* @param resources the resources used to localize the root object, or null if not localized
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void initialize(URL location, ResourceBundle resources) {
|
|
||||||
// Real binding happens in FXMLLoader extension
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the GenericViewModel used by this controller.
|
|
||||||
*
|
|
||||||
* @return the GenericViewModel instance
|
|
||||||
*/
|
|
||||||
public GenericViewModel<T> getViewModel() {
|
|
||||||
return viewModel;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
package de.neitzel.fx.mvvm;
|
|
||||||
|
|
||||||
import javafx.beans.property.Property;
|
|
||||||
import javafx.beans.property.SimpleBooleanProperty;
|
|
||||||
import javafx.beans.property.SimpleDoubleProperty;
|
|
||||||
import javafx.beans.property.SimpleIntegerProperty;
|
|
||||||
import javafx.beans.property.SimpleLongProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
|
||||||
|
|
||||||
import java.beans.BeanInfo;
|
|
||||||
import java.beans.IntrospectionException;
|
|
||||||
import java.beans.Introspector;
|
|
||||||
import java.beans.PropertyDescriptor;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A generic ViewModel for JavaFX using reflection to automatically create properties for model attributes.
|
|
||||||
* Supports binding between JavaFX controls and the model via JavaFX properties.
|
|
||||||
*
|
|
||||||
* @param <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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
<module>gson</module>
|
<module>gson</module>
|
||||||
<module>net</module>
|
<module>net</module>
|
||||||
<module>log4j</module>
|
<module>log4j</module>
|
||||||
<module>fx-example</module>
|
<!-- fx-example still calls the removed JavaFX units and is not part of this reactor until it is migrated. -->
|
||||||
<module>quarkus</module>
|
<module>quarkus</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user