Discover custom ViewModels and validate Computed bindings in shared hosts

This commit is contained in:
2026-11-13 13:27:23 +01:00
parent 5a208d2336
commit 99d93a11fc
46 changed files with 1397 additions and 64 deletions
@@ -11,6 +11,7 @@ final class BindingBrowser extends VBox {
final ComboBox<BindingMetadata.Attribute> paths = new ComboBox<>();
final ComboBox<String> directions = new ComboBox<>();
final TableView<BindingMetadata.Target> targets = new TableView<>();
final ListView<BindingMetadata.Computed> methods = new ListView<>();
final Label status = new Label("Build and Refresh, then discover binding targets.");
private boolean updating;
@@ -21,6 +22,8 @@ final class BindingBrowser extends VBox {
directions.setId("binding-direction");
targets.setId("binding-targets");
status.setId("binding-status");
methods.setId("computed-methods");
methods.setPrefHeight(100);
paths.setPrefWidth(320);
directions.getItems().setAll("view", "model", "both");
directions.setValue("both");
@@ -62,7 +65,9 @@ final class BindingBrowser extends VBox {
8, refresh, new Label("Model path"), paths, new Label("Direction"), directions),
status,
targets,
use);
use,
new Label("Computed sources (public Controller methods)"),
methods);
}
private String path() {
@@ -77,8 +82,16 @@ final class BindingBrowser extends VBox {
}
void invalidate(String message) {
targets.getItems().clear();
status.setText(message);
updating = true;
try {
paths.getItems().clear();
paths.setValue(null);
targets.getItems().clear();
methods.getItems().clear();
status.setText(message);
} finally {
updating = false;
}
}
void show(BindingMetadata metadata, String provenance) {
@@ -92,9 +105,14 @@ final class BindingBrowser extends VBox {
.orElse(null));
directions.setValue(metadata.direction());
targets.getItems().setAll(metadata.targets());
methods.getItems().setAll(metadata.computed());
status.setText(
"Model: "
+ metadata.model()
+ " · ViewModel: "
+ metadata.viewModel()
+ " · Controller: "
+ metadata.controller()
+ " · "
+ provenance
+ "\n"
@@ -748,17 +748,22 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
withDraft(
() -> {
long ticket = ++bindingRequest;
validatingSource = null;
validationPreview = false;
bindingBrowser.invalidate("Discovering declared types in target runtime…");
var captured = draft;
validatingSource = captured.text();
bindingWorkflow
.bindings(last, captured, path, direction)
.whenComplete(
(metadata, error) ->
fx(
() -> {
if (closed || ticket != bindingRequest) return;
if (closed
|| ticket != bindingRequest
|| draft.revision() != captured.revision()) return;
validatingSource = null;
boolean resumePreview = validationPreview;
validationPreview = false;
if (error != null) {
bindingBrowser.invalidate(
"Discovery unavailable: "
@@ -766,6 +771,8 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
+ ". Build and Refresh, then retry.");
return;
}
validatedSource = captured.text();
bindingProblems = metadata.problems();
bindingBrowser.show(
metadata,
(last
@@ -773,6 +780,11 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
: "current successful build")
+ " · draft revision "
+ captured.revision());
bindingEditor.metadata(
metadata,
null,
"Discovery · draft revision " + captured.revision());
if (resumePreview) render();
}));
});
}
@@ -190,6 +190,10 @@ final class DirectBindingEditor extends VBox {
BindingMetadata metadata, DirectBindingDocument.Values requested, String provenance) {
status.setText(
provenance
+ " · ViewModel: "
+ metadata.viewModel()
+ " · Controller: "
+ metadata.controller()
+ "\n"
+ (metadata.problems().isEmpty()
? "No detected binding errors."
@@ -482,6 +482,84 @@ public final class ProjectionHostChecks {
await(() -> fx(() -> source(pane).getText().equals(full)));
}
private static void viewModelBindings(
DesignerPane pane, DocumentSession document, String original) throws Exception {
bindingTab(pane, "Discover targets");
click(pane, "Discover bindings");
await(
() ->
fx(
() ->
((ComboBox<de.neitzel.fxdesigner.protocol.BindingMetadata.Attribute>)
pane.lookup("#binding-path"))
.getItems().stream().anyMatch(a -> a.path().equals("badge"))));
check(
fx(
() ->
((ListView<de.neitzel.fxdesigner.protocol.BindingMetadata.Computed>)
pane.lookup("#computed-methods"))
.getItems().stream().anyMatch(m -> m.method().equals("#greeting"))),
"Computed inventory missing");
check(documentText(document).equals(original), "ViewModel discovery altered FXML");
String invalid = original.replace("method=\"#greeting\"", "method=\"#invalidComputed\"");
check(!invalid.equals(original), "Computed fixture missing");
set(document, invalid);
await(() -> fx(() -> source(pane).getText().equals(invalid)));
bindingTab(pane, "Direct bindings");
click(pane, "Render drafts");
await(() -> bindingStatus(pane, "Computed return type"));
await(
() ->
fx(
() ->
pane.lookupAll(".label").stream()
.anyMatch(
n ->
n instanceof Label l
&& l.getText() != null
&& l.getText().startsWith("Preview blocked:"))));
document
.save(document.snapshot().toCompletableFuture().get().revision())
.toCompletableFuture()
.get();
check(
java.nio.file.Files.readString(
java.nio.file.Path.of(document.snapshot().toCompletableFuture().get().document()))
.equals(invalid),
"Invalid Computed draft could not be saved");
click(pane, "Undo");
await(() -> documentText(document).equals(original));
click(pane, "Redo");
await(() -> documentText(document).equals(invalid));
click(pane, "Undo");
await(() -> documentText(document).equals(original));
String replacement =
java.nio.file.Files.readString(
java.nio.file.Path.of(document.snapshot().toCompletableFuture().get().document())
.resolveSibling("Replacement.fxml"));
set(document, replacement);
await(() -> fx(() -> source(pane).getText().equals(replacement)));
bindingTab(pane, "Discover targets");
click(pane, "Discover bindings");
await(
() ->
fx(
() ->
((Label) pane.lookup("#binding-status"))
.getText()
.contains("fixture.components.ReplacementViewModel")));
check(
fx(
() ->
((ComboBox<de.neitzel.fxdesigner.protocol.BindingMetadata.Attribute>)
pane.lookup("#binding-path"))
.getItems().stream().noneMatch(a -> a.path().equals("name"))),
"Replacement inherited Model attributes");
check(documentText(document).equals(replacement), "Replacement discovery altered FXML");
set(document, original);
await(() -> fx(() -> source(pane).getText().equals(original)));
}
public static void run(DesignerPane pane, DocumentSession document) throws Exception {
String original = document.snapshot().toCompletableFuture().get().text();
// The same real application, now with nested, fully-qualified Component declarations.
@@ -810,6 +888,7 @@ public final class ProjectionHostChecks {
set(document, original);
await(() -> fx(() -> source(pane).getText().equals(original) && object(pane, "card") != null));
viewModelBindings(pane, document, original);
document
.save(document.snapshot().toCompletableFuture().get().revision())
.toCompletableFuture()