[JavaFX] Mehrere Controller von der Main abrufen?


15.07.2022, 16:34

Das brauche ich, weil ich teilweise mit drei Controller-Klassen auf eine andere Klasse zugreifen will:

public void Zugriff(Controller1 c1, Controller2 c2, Controller3 c3){
c1.textField.setText("Aus Controller1");
c2.textField.setText("Aus Controller2");
c3.textField.setText("Aus Controller3");
new AndereMethode(c1, c2, c3);
}

Das sind natürlich nur Beispiele. Dieses .setText() ginge ja sonst ganz einfach.

Und in der Main-Klasse muss ich z. B. Controller3 verwenden:

new EineMethode(getInstance(), getController3());

15.07.2022, 18:01

Wenn ich die Container in der "Haupt"-Controller-Klasse registrieren lasse, werden die variablen zwar (lila) eingefärbt, aber es gibt ebenfalls ein NullPointerException, weil sich der Container in einer anderen fxml-Datei befindet (also nicht in der Main.fxml).

Controller.java: (Hier registriert)

public Button testButton;

AndererController.java: (Und hier wird die Methode durch das Klicken auf den Button ausgeführt)

// On Action
public void OnActionMethode(ActionEvent e){
...
}

18.07.2022, 23:30

Für alle, die dasselbe/ein ähnliches Problem haben: Hier findet ihr den Lösungsansatz.


18.07.2022, 23:38
Der Name für das Feld setzt sich aus dem Namen der ID und dem Wort Controller zusammen.

1 Antwort

Vom Fragesteller als hilfreich ausgezeichnet
Warum sagt der mir, dass andererController null ist?

Weil du ihm (jedenfalls deiner Beschreibung zu Folge) nie einen Wert zuordnest. Der Unterschied zum mainController-Feld ist der, dass du dieses Feld in start definierst. Sobald der FXMLLoader das View Main.fxml lädt, generiert er auch die entsprechende Controller-Instanz, die du über die getController-Methode abgreifst.

Wie gehts richtig?

Angenommen, dies wäre deine Main.fxml:

<AnchorPane xmlns:fx="http://javafx.com/fxml" fx:controller="app.MainController">
  <children>
    <fx:include source="NestedView.fxml" fx:id="nestedView" />
  </children>
</AnchorPane>

Dann kommst du im MainController über ein Binding an die Controller-Instanz des NestedView.

public class MainController {
  @FXML
  private NestedViewController nestedViewController;

  /* ... */
}

Der Name für das Feld setzt sich aus dem Namen der ID und dem Wort Controller zusammen.

verreisterNutzer  15.07.2022, 19:50

In welchem Controller muss ich dann die Container (TextField, Button, etc.) registrieren? Irgendwas mache ich falsch, sodass es ständig null ist ...

0
regex9  15.07.2022, 22:00
@verreisterNutzer

Jede View (FXML) ist an einen Controller gebunden. Die Komponenten in der FXML, die du mit einer ID ausgestattet hast, können an Felder diesen Controllers gebunden werden. Das Beispiel in dem Tutorial, was ich dir schon verlinkt hatte (Nested Controllers) zeigt diesen Anwendungsfall. Dort wird eine counter.fxml in einer anderen FXML eingebunden. Der Controller für die counter.fxml bindet sein Feld counter an das Textfeld in der FXML.

1
verreisterNutzer  15.07.2022, 23:53
@regex9

Ich glaube, ich habe ein anderes Problem. Ich kann zwar z. B. die NestedController-Klasse in der MainController-Klasse aufrufen, aber Folgendes:

Ich habe in der NestedController-Klasse Objekte aus der (ist sage mal) NestedView.fxml registriert, aber wenn ich in einer anderen Klasse aus auf dieses Objekt, das in der NestedController-Klasse liegt, zugreifen möchte (bspw. button.setText();), dann wirft der mir ein NullPointerException raus.

Ich weiß nicht, ob das damit zutun hat, was du mir versuchst zu erklären. Dieses Binding und Splitten ist mir leider noch ein Neuland.

Die NestedView.fxml ist aber ohne Probleme in der Main.fxml included, sowie die NestedController-Klasse zur NestedView.fxml gehört.

0
regex9  16.07.2022, 00:29
@verreisterNutzer

Das kann verschiedene Gründe haben.

  • ID und Feldname passen nicht überein oder das Feld ist statisch. Obwohl ich meine, dass da bereits beim Laden der FXML ein Fehler geworfen werden müsste.
  • Du greifst auf ein selbst erstelltes Controller-Objekt zu. Das lässt sich allerdings ausschließen, so lange du deine Controller-Instanzen nur aus einem Binding (so wie im Beispiel) beziehst oder vom FXMLLoader.
  • Du greifst zu einem Zeitpunkt auf das Feld zu, zu dem es noch nicht befüllt wurde. Das halte ich aktuell am wahrscheinlichsten.

Erst wenn eine View geladen wird, wird wie oben geschrieben auch ihr Controller-Objekt generiert. In diesem Zuge werden zudem die Felder gesetzt / gebunden. Das passiert allerdings erst nach dem Konstruktoraufruf des Controllers. Falls du direkt nach Anlegen des Controllers auf gebundene Felder zugreifen möchtest, nutze die initialize-Methode.

Ein Beispiel (auf das Wesentliche reduziert):

NestedView.fxml:

<AnchorPane xmlns="..." xmlns:fx="..." fx:controller="NestedViewController">
  <children>
    <Button fx:id="button" text="Click me" />
  </children>
</AnchorPane>

NestedViewController:

public class NestedViewController {
  @FXML
  private Button button;

  public void setButtonText(String text) {
    button.setText(text);
  }
}

OuterView.fxml:

<AnchorPane xmlns="..." xmlns:fx="..." fx:controller="OuterViewController">
  <children>
    <fx:include fx:id="nestedView" source="NestedView.fxml" />
  </children>
</AnchorPane>

OuterViewController:

public class OuterViewController {
  @FXML
  private NestedViewController nestedViewController;

  @FXML
  private void initialize() {
    // now FXML elements are bound
    nestedViewController.setButtonText("I do nothing");
  }
}

start-Methode:

FXMLLoader viewLoader = new FXMLLoader(getClass().getResource("OuterView.fxml"));
Parent root = viewLoader.load();

Scene scene = new Scene(root, 200, 200);
// ...
stage.setScene(scene);
stage.show();

Sobald die äußere View geladen wurde, besteht auch Zugriff auf die innere View und ihren Controller. Demzufolge kann beispielsweise der Buttontext geändert werden.

1
verreisterNutzer  16.07.2022, 13:20
@regex9

Da ich sämtlichen Anleitungen jetzt nachgegangen bin, aber ich immer noch auf dieser NullPointerException sitze, schicke ich dir mal meinen Teil-Code. Ich habe den Code teilweise einfach nur noch abgeschrieben, aber der SecondController ist immer noch null, wenn ich ihn in der MainController-Klasse versuche zu registrieren.

Mein jetziger Versuch: (von hier)

MainController:

@FXML
MainController mainController;
@FXML
SecondController secondController;

public AnchorPane mainAnchorPane;
public AnchorPane secondAnchorPane;

public void inizialize(){
mainController = this;
secondController.init(this);
}

Main.fxml:

<AnchorPane fx:id="mainAnchorPane" xmlns="..." xmlns:fx="..."   fx:controller="com.github.gqs.xyz.Controller.MainController">
<children>
        <fx:include source="SecondView.fxml"/>
// Andere Elemente
</children>
</AnchorPane>

SecondController:

private MainController mainController;

public Button XY;

public void testAction(ActionEvent e){
new TestKlasse(mainController, e);
}

public void init(MainController mainController){
mainController = mainController;
}

SecondView.fxml:

<AnchorPane xmlns="..." xmlns:fx="..." fx:controller="com.github.gqs.xzy.Controller.SecondController">
ButtonXY ...
</AnchorPane>
Fehler:
Caused by: java.lang.NullPointerException: Cannot invoke "com.github.gqs.xyz.Controller.SecondController.init(com.github.gqs.xyz.Controller.MainController)" because "this.secondController" is null
at com.github.gqs.xyz.Controller.MainController.initialize(MainController.java:766)

Vorhaben:

Ich klicke auf einen Button XY in SecondView.fxml. Diese SecondView.fxml ist in der Main.fxml included. Der Button XY ruft die Methode testAction in SecondController auf. Diese Methode ruft eine andere Klasse TestKlasse auf. In dieser TestKlasse wird z. B. der Text von TextFieldXY, das in der Main.fxml liegt, verändert.

Ich weiß nicht, wo mein Gedankenfehler liegt. Aber irgendwas übersehe ich, sodass SecondController secondController; ständig null ist ...

Ich dachte halt, man kann Methoden im SecondController haben, die nicht alle erst einzeln im MainController angesteuert werden müssen oder so.

Irgendetwas übersehe ich und am Ende ist es bestimmt wieder nur eine Zeile.

0
regex9  16.07.2022, 14:00
@verreisterNutzer

1) Das fx:include-Element hat bei dir keine ID. Schau dir mein OuterView.fxml-Beispiel an. Ohne ID gibt es für den FXMLLoader keinen Grund, nach einem passenden Feld zu suchen und dieses an das Element zu binden.

2) Was ich bei deiner MainController-Klasse nicht verstehe, ist das mainController-Feld. Du hast innerhalb der Klasse doch schon via this Zugriff auf die MainController-Instanz.

Noch einmal zur Versinnbildlichung: Jedes View (FXML) hat einen Controller. In deinem Projekt definierst du nur die Klasse für den Controller und führst den Typ in der FXML an (fx:controller-Attribut). Das konkrete Controller-Objekt wird intern (vom FXMLLoader) angelegt, darum brauchst du dich nicht kümmern. Letzten Endes gibt es also von jeder Controller-Klasse immer nur eine (intern gehandhabte) Instanz und die ist mit einem View verbunden.

Man kann zwar auch eigene Controller-Objekte anlegen oder bspw. mehrere Views an einen bestimmten Controller binden. Das ist aber kein Ziel im Konzept von JavaFX.

3) Bezüglich der SecondView: Ich würde dazu raten, das Feld XY als private zu deklarieren (Prinzip der Kapselung). Natürlich braucht es dann noch eine Annotation:

@FXML
private Button XY;

Bei deiner init-Methode hast du einen Fehler. Lokale Variablen (dazu gehören auch Parameter) überdecken stets globale Variablen (also auch Felder) Du überschreibst den Parameter mit sich selbst.

Richtig wäre:

public void init(MainController mainController) {
  this.mainController = mainController;
}

Mit this beziehst du dich ganz konkret auf das Feld. Alternativ könntest du dem Parameter (oder dem Feld) auch einen anderen Namen geben, sodass es zu keiner Überdeckung kommen kann.

Beispiel:

public void init(MainController controller) {
  mainController = controller;
}
1
verreisterNutzer  16.07.2022, 15:11
@regex9

Ich habe die anderen Dinge, die du mir vorgeschlagen hast (z. B. "Du hast innerhalb der Klasse doch schon via this Zugriff auf die MainController-Instanz."), alle so übernommen.

Trotzdem machen mir

SecondController secondController;

public void initialize() {
this.secondController.init(this); // DIESE ZEILE
...}

Probleme.

Angeblich soll dann

Parent root = loader.load();

in der Main Probleme machen - aber auch nur, wenn ich den oberen Code einfüge.

Ich sitze, seit dem du mir das Splitten vorgeschlagen hast, dran und ich probiere solange rum, bis es funktioniert.

Ganze Fehlermeldung

Caused by: javafx.fxml.LoadException:
&
Caused by: java.lang.reflect.InvocationTargetException
&
Caused by: java.lang.NullPointerException: Cannot invoke "com.github.gqs.xyz.Controller.SecondController.init(com.github.gqs.xyz.Controller.Controller)" because "this.secondController" is null
0
regex9  17.07.2022, 17:00
@verreisterNutzer

Den Grund, woran es bei dir noch scheitert, kann ich nicht festmachen. Vielleicht ist die ID in der FXML noch falsch, vielleicht schreibst du in die falsche FXML, ... mehr als Prüfen/Vergleichen kann ich dir daher im Moment nicht vorschlagen.

1
verreisterNutzer  17.07.2022, 17:32
@regex9

Auf jeden Fall erst mal vielen Dank!

Die IDs habe ich jetzt mehr als 100-mal verglichen, daran lag es nicht.

Jemand anderes meinte, die Controller müssen sich untereinander kennen - "das soll mit einer ViewBinder-Klasse gehen, der alle Controller kennt und die Bindings zur View regelt. Der soll dann in der fxml erzeugt werden und im Konstruktor oder sonst wo werden die Verbindungen der Controller miteinander geteilt."

Ich muss also nur noch herausfinden, wie ich das bei mir einbaue, denn unter "ViewBinder" finde ich nichts im Internet.

0
regex9  17.07.2022, 18:49
@verreisterNutzer

Das Minimalbeispiel aus meinem Kommentar oben reicht aus, um den geschachtelten Controller zu erhalten. Drumherumkonstrukte, die versuchen, etwas zu implementieren, was schon gegeben ist, würde ich nicht empfehlen.

1
verreisterNutzer  17.07.2022, 23:42
@regex9
Der Name für das Feld setzt sich aus dem Namen der ID und dem Wort Controller zusammen.

Ich habe einfach nur den Namen der Variable von optionsController zu optionsAnchorPaneController ändern müssen ... oh man.

Jetzt stellt sich mir die Frage: Muss ich in der initialize-Methode des MainControllers für jede Klasse, wo ich den OptionsController ansprechen möchte, dritteOderVierterController.setMainController(this); schreiben, wenn ich den Controller nicht als Parameter von dem OptionsController/MainController aus übergebe?

0
regex9  18.07.2022, 02:04
@verreisterNutzer

Es gibt unterschiedliche Wege, die OptionsController-Instanz weiterzugeben.

a) In einer Methode des MainController, die erst aufgerufen wird, sobald die View geladen wurden. Innerhalb dieser muss allerdings die Controller-Instanz bekannt sein, die den OptionsController erhalten soll.

otherController.setOptionsController(optionsAnchorPaneController);

b) Im OptionsController selbst. Es gilt an sich dasselbe, wie bei Option a.

otherController.setOptionsController(this);

c) Wenn der MainController eine Getter-Methode bekommt, die die OptionsController-Instanz liefert, braucht der Zielcontroller lediglich eine Referenz auf die MainController-Instanz.

Das heißt, eine Weitergabe könnte bspw. auch in der start-Methode deiner Anwendung stattfinden.

FXMLLoader viewLoader = new FXMLLoader(getClass().getResource("Main.fxml"));
Parent root = viewLoader.load();

MainController mainController = viewLoader.getController();
// receive otherController from somewhere ...
otherController.setOptionsController(mainController.getOptionsController());

Die Schwierigkeit hierbei läge allerdings eher darin, eine Instanz des Zielcontrollers (otherController) zu erlangen. Daher wird sich dieser Lösungsweg eher nur selten anbieten.

d) Bei einer Aufteilung eines Views in Subviews bilden sich zwangsweise Stränge:

MainController ----> SubController1 ---> Subsubcontroller1
 |                    |
 |                    ---------> Subsubcontroller2
 |
 ------------------> SubController2

und man möchte nicht unbedingt durchgehend mit dem Herumreichen von Referenzen beschäftigt sein. Gerade wenn der Subsubcontroller1 mit dem SubController2 kommunizieren möchte, wird es etwas aufwendig.

Man könnte sich nun ein extra Repository anlegen (ein Mediator), bei dem jeder Controller registriert wird. Dafür sollte jeder Controller ein Interface implementieren. Den globalen Zugriff auf das Repository erlaubt das Singleton-Pattern.

Eine Implementation könnte ungefähr so aussehen:

interface IController {
}

class ControllerRepository {
  private static ControllerRepository instance = new ControllerRepository();

  private final Map<Class, IController> controllers;
  
  protected ControllerRepository() {
    this.controllers = new HashMap<>();
  }

  public static ControllerRepository getInstance() {
    return instance;
  }

  public void register(IController controller) {
    Class key = controller.getClass();

    if (!controllers.containsKey(key)) {
      controllers.put(key, controller);
    }
  }

  public IController getController(Class type) {
    return controllers.get(type);
  }
}

Gerade bei so einer Lösung muss man allerdings darauf achten, zu welchem Zeitpunkt man auf die Controller zugreift und dass jeder Controller spätestens nach dem Laden seiner View registriert wird.

e) Noch andere Ansätze zum Datenaustausch wären DI oder ein EventBus.

1
verreisterNutzer  18.07.2022, 10:11
@regex9

Ich finde die Methode mit dem Mediator gut. Auch, weil ich diese Methode noch nicht kannte.

Müsste das dann wiefolgt sein?:

// Main.fxml:
<AnchorPane ... fx:controller="IController"> ... </AnchorPane>

// Main.java:
public void start(...) {
Controller1 controller1;
Controller2 controller2;
// ...
ControllerRepository.register(controller1);
ControllerRepository.register(controller2);
// ...
}

Und in anderen Klassen:

// Controller oder NichtController-Klasse.java:
ControllerRepository controller1 = ControllerRepository.getController(Controller1);
ControllerRepository controller2 = ControllerRepository.getController(Controller2);
controller1.xy();
controller2.xy();
// ..

Ich bin gerade nicht zu Hause, deswegen kann ich das nicht testen.

0
verreisterNutzer  18.07.2022, 23:26
@regex9

Okay, ich habe es jetzt wie folgt geregelt:

Main.fxml:

// Alles wie gehabt
<AnchorPane ...>
<children>
<fx:include="optionsAnchorPane" .../>
<children> // Wird "<children/>" überhaupt benötigt? Denn funktionieren tut es ja auch ohne.
</AnchorPane>

MainController.java:

@FXML
private OptionsController optionsAnchorPaneController;

public OptionsController getOptionsController() {
    return optionsAnchorPaneController;
}
// ...
public void initialize(...) {
optionsAnchorPaneController.setMainController(this);
}

OptionsController.java:

Controller controller;
public void setMainController(Controller mainController) {
    this.controller = mainController;
}

IrgendeineAndereKlasse:

OptionsController optionsAnchorPaneController = Main.getInstance().getController().getOptionsController();

So funktioniert es fürs Erste und so lasse ich es auch, solange mir keine bessere Methode den Raum erhellt.

Vielen Dank für deine Geduld, ehrlich!

0
regex9  19.07.2022, 00:12
@verreisterNutzer

Bei Nutzung eines Repositories, wie oben beschrieben, müsstest du jeden Controller bei diesem registrieren. Beispielsweise im Konstruktor.

class SomeController implements IController {
  public SomeController() {
    ControllerRepository.getInstance().register(getClass(), this);
  }

  /* ... */
} 

Ein Zugriff wiederum ist wie immer erst dann möglich, wenn die FXML-Views geladen wurden.

SomeController controller = (SomeController) ControllerRepository.getInstance().getController(SomeController.class);
// Wird "<children/>" überhaupt benötigt? Denn funktionieren tut es ja auch ohne.

FXML basiert auf XML (auch erkennbar an der XML-Deklaration am Anfang jeder FXML-Datei). In XML wird erwartet, dass jedes geöffnete Element auch wieder geschlossen wird.

<children>
  <!-- some child nodes ... -->
</children>

Wenn ein Element keine Kindelemente hat, kann man Start- und Endtag zusammenfassen:

<children />

Wenn du die XML-Regeln nicht beachtest, kann es gut sein, dass der FXML-Parser irgendwann einen Fehler wirft. Wie fehlertolerant er ist, habe ich bisher nicht sonderlich getestet.

1
verreisterNutzer  19.07.2022, 00:44
@regex9

Tut mir leid, wenn man mir anmerkt, dass meine Nerven langsam am Ende sind. Aber ich sitze seit ein paar Tagen von morgens bis abends/nachts da dran und mir fehlt jeder kleine Schritt ... Alles, was mir bisher erklärt wurde, konnte ich auch so aufnehmen. Auch jetzt deine letzte Antwort.

Aber wenn ich jetzt die Controller in ControllerRepository registriere, welchen Controller soll ich dann in der MainView.fxml angeben?

Und

ControllerRepository.getInstance().register(getClass(), this);

scheint mir nicht ganz richtig, denn hier

public void register(IController controller) { ... }

ist ja nur ein Parameter angegeben. Ich schätze mal, dass es

ControllerRepository.getInstance().register(this);

heißen muss oder?

Zudem werden

public class OptionsController() {
public OptionsController() {
    ControllerRepository.getInstance().register(this);
   }
}

und

public class Controller() {
public Controller() {
    ControllerRepository.getInstance().register(this);
   }
}

nicht einfach so aufgerufen und in der Main-Klasse in der Start-Methode geht

new Controller();
new OptionsController();

ja auch nicht einfach so.

0
regex9  19.07.2022, 02:05
@verreisterNutzer
(...) welchen Controller soll ich dann in der MainView.fxml angeben?

Der bleibt so wie gehabt - MainController, oder wie er bei dir heißt.

Das Repository dient nur als zusätzliches Register, um Controller-Instanzen zu sammeln und einfacher verfügbar zu machen. Mehr nicht.

Und (...) scheint mir nicht ganz richtig, (...)

Ja, da habe ich mich vertan.

Zudem werden (...) und (...) nicht einfach so aufgerufen (...)

Die (Standard-)Konstruktoren der Controller werden auf jeden Fall aufgerufen (sofern ihr Typ in einer FXML-Datei angegeben wurde). Das erledigt der FXMLLoader.

(...) Jedes View (FXML) hat einen Controller. In deinem Projekt definierst du nur die Klasse für den Controller und führst den Typ in der FXML an (fx:controller-Attribut). Das konkrete Controller-Objekt wird intern (vom FXMLLoader) angelegt, darum brauchst du dich nicht kümmern. (...)
1
verreisterNutzer  19.07.2022, 09:23
@regex9

Ah, ja, logisch, ich hatte auch bei .getController(XYController.class); den falschen Controller angegeben. Die Konzentration begleitete mich heute Nacht nicht mehr.

0
verreisterNutzer  15.07.2022, 21:09

Das Binding ist mir neu, fällt mir auf. Ich habe versucht, mir ein paar Sachen anzuschauen, aber das für meinen Fall, der ja nun wieder ein ganz anderer als in den Beschreibungen ist, gelingt mir nicht.

0