Generate typed Java ViewModels and discover declarative registry defaults

This commit is contained in:
2026-11-13 20:06:26 +01:00
parent adf432b98b
commit 8be0aec287
36 changed files with 1123 additions and 17 deletions
@@ -0,0 +1,11 @@
package fixture.components;
public final class CustomGeneratedPerson extends fixture.generated.GeneratedPersonViewModel {
public CustomGeneratedPerson(fixture.model.GeneratedPerson model) {
super(model);
}
public String greeting() {
return "Hello " + getName();
}
}
@@ -0,0 +1,5 @@
package fixture.components;
@jakarta.inject.Singleton
@de.neitzel.fx.view.ViewModelClass(fixture.generated.GeneratedPersonViewModel.class)
public class GeneratedController {}
@@ -0,0 +1,22 @@
package fixture.model;
public class GeneratedAddress {
private String street = "Generated street";
private GeneratedAddress next;
public String getStreet() {
return street;
}
public void setStreet(String v) {
street = v;
}
public GeneratedAddress getNext() {
return next;
}
public void setNext(GeneratedAddress v) {
next = v;
}
}
@@ -0,0 +1,13 @@
package fixture.model;
public class GeneratedEmployee extends GeneratedPerson {
private int salary = 42;
public int getSalary() {
return salary;
}
public void setSalary(int v) {
salary = v;
}
}
@@ -0,0 +1,35 @@
package fixture.model;
public class GeneratedPerson {
static {
System.setProperty("fixture.generated.initialized", "yes");
}
private String name = "Generated sample";
private GeneratedAddress home = new GeneratedAddress();
private java.util.List<GeneratedAddress> visits = new java.util.ArrayList<>();
public String getName() {
return name;
}
public void setName(String v) {
name = v;
}
public GeneratedAddress getHome() {
return home;
}
public void setHome(GeneratedAddress v) {
home = v;
}
public java.util.List<GeneratedAddress> getVisits() {
return visits;
}
public void setVisits(java.util.List<GeneratedAddress> v) {
visits = v;
}
}
@@ -0,0 +1,3 @@
<?neitzel-model fixture.model.GeneratedPerson?>
<?import javafx.scene.layout.VBox?><?import javafx.scene.control.TextField?><?import de.neitzel.fx.view.*?>
<VBox xmlns:fx="http://javafx.com/fxml/1"><fx:define><BindingList><DirectBinding control="name" property="text" path="name" direction="both"/><DirectBinding control="street" property="text" path="home.street" direction="both"/></BindingList></fx:define><TextField fx:id="name"/><TextField fx:id="street"/></VBox>
@@ -0,0 +1,2 @@
<?neitzel-model fixture.model.GeneratedAddress?><?import javafx.scene.control.TextField?><?import javafx.scene.layout.VBox?><?import de.neitzel.fx.view.*?>
<VBox xmlns:fx="http://javafx.com/fxml/1"><fx:define><BindingList><DirectBinding control="street" property="text" path="street" direction="both"/></BindingList></fx:define><TextField fx:id="street"/></VBox>
@@ -0,0 +1,3 @@
<?neitzel-model fixture.model.GeneratedPerson?>
<?import javafx.scene.layout.VBox?><?import javafx.scene.control.TextField?><?import de.neitzel.fx.view.*?>
<VBox xmlns:fx="http://javafx.com/fxml/1" fx:controller="fixture.components.GeneratedController"><fx:define><BindingList><DirectBinding control="name" property="text" path="name" direction="both"/><DirectBinding control="street" property="text" path="home.street" direction="both"/></BindingList></fx:define><TextField fx:id="name"/><TextField fx:id="street"/></VBox>
@@ -0,0 +1,109 @@
package fixture.checks;
import de.neitzel.fx.view.*;
import de.neitzel.fxdesigner.target.BindingDiscovery;
import de.neitzel.injection.ApplicationContext;
import fixture.components.CustomGeneratedPerson;
import fixture.generated.*;
import fixture.model.*;
import java.util.concurrent.*;
import javafx.application.Platform;
import javafx.scene.control.TextField;
public final class GeneratedViewModelChecks {
public static void run() throws Exception {
var loader = GeneratedViewModelChecks.class.getClassLoader();
String xml;
try (var in = loader.getResourceAsStream("fixture/components/Generated.fxml")) {
xml = new String(in.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8);
}
var metadata = BindingDiscovery.inspect(xml, "home.street", "both", loader);
PreviewChecks.check(
metadata.viewModel().equals(CustomGeneratedPerson.class.getName()),
"Registry discovery default");
PreviewChecks.check(
metadata.problems().stream().noneMatch(p -> p.severity().equals("ERROR")),
"Generated binding validation: " + metadata.problems());
PreviewChecks.check(
System.getProperty("fixture.generated.initialized") == null,
"Registry discovery initialized Model");
var ready = new CountDownLatch(1);
Platform.startup(
() -> {
Platform.setImplicitExit(false);
ready.countDown();
});
if (!ready.await(10, TimeUnit.SECONDS)) throw new AssertionError("Toolkit startup");
try {
PreviewChecks.fx(
() -> {
var model = new GeneratedPerson();
model.getHome().setNext(model.getHome());
model.getVisits().add(model.getHome());
var context = new ApplicationContext("fixture.components");
var view =
ViewLoader.load(
loader.getResource("fixture/components/Generated.fxml"), model, context);
var vm = (CustomGeneratedPerson) view.getViewModel();
PreviewChecks.check(
vm.nameProperty() == view.property("name"), "Stored property identity");
((TextField) view.getRoot().lookup("#name")).setText("Typed writeback");
PreviewChecks.check(
model.getName().equals("Typed writeback") && vm.getName().equals(model.getName()),
"Typed writeback");
var child = vm.getHome();
PreviewChecks.check(child.getNext() == child, "Typed cycle identity");
var direct =
ViewLoader.load(
loader.getResource("fixture/components/GeneratedAddress.fxml"),
model.getHome(),
context);
PreviewChecks.check(direct.getViewModel() == child, "Nested/direct identity");
child.setStreet("Changed child");
PreviewChecks.check(
model.getHome().getStreet().equals("Changed child"), "Child writeback");
PreviewChecks.check(
vm.getVisits().getFirst() == model.getHome(), "Collection retains Models");
var again =
ViewLoader.load(
loader.getResource("fixture/components/GeneratedExplicit.fxml"),
model,
context);
PreviewChecks.check(
again.getViewModel() == vm, "Custom satisfies generated superclass request");
var fresh =
ViewLoader.load(
loader.getResource("fixture/components/GeneratedExplicit.fxml"),
new GeneratedPerson(),
context);
PreviewChecks.check(
fresh.getViewModel().getClass() == GeneratedPersonViewModel.class,
"Explicit overrides configured default");
vm.setHome(null);
PreviewChecks.check(
model.getHome() == null && vm.getHome() == null, "Null nested writeback");
var employee = new GeneratedEmployee();
var inherited =
ViewLoader.load(
loader.getResource("fixture/components/Generated.fxml"), employee, context);
PreviewChecks.check(
inherited.getViewModel() instanceof GeneratedEmployeeViewModel,
"Most-specific generated default");
var evm = (GeneratedEmployeeViewModel) inherited.getViewModel();
evm.setSalary(55);
evm.setName("Inherited");
PreviewChecks.check(
employee.getSalary() == 55 && employee.getName().equals("Inherited"),
"Inherited typed access");
vm.addMessage(new Message(Message.Severity.INFO, "Generated notice", "name"));
PreviewChecks.check(
view.property("latestMessage").getValue().equals("Generated notice"),
"Shared messages");
return null;
});
System.out.println("PASS generated " + PreviewChecks.checks);
} finally {
Platform.exit();
}
}
}
@@ -41,6 +41,10 @@ public final class PreviewChecks {
public static void main(String[] args) throws Exception {
String mode = args[0];
if (mode.equals("generated")) {
GeneratedViewModelChecks.run();
return;
}
if (mode.equals("viewmodels")) {
ViewModelRuntimeChecks.run();
return;