JavaFx how to avoid creating one huge controller - java

I have an app in JavaFX, which has main scene with menu and toolbar, and smaller scenes, which are injected into this main scene, after one of menu buttons are being pressed.
Now, HomeCntroller is responsible for either scene components: Home Scene (with toolbar and menu), and injected scene. This leads me to create massive, huge and very unprofessional controller if number of injected scenes is more than one.
How to split controller responsibility?
Now my Controller looks like this:
changeDashboardPane method injects smaller Pane into my main HomePane.
#Component
#RequiredArgsConstructor(onConstructor = #__(#Autowired) )
public class HomeController extends AbstractController {
private static final Logger LOG = Logger.getLogger(HomeController.class);
private final BudgetProfileService budgetProfileService;
#FXML
private Label usernameLabel;
#FXML
private ComboBox<String> budgetProfilesComboBox;
#FXML
private AnchorPane dashBoardPane;
#FXML
public void initialize() {
refreshUsernameLabel();
getAllBudgetProfiles();
changeDashboardPane(PaneFactoryKeys.FINANCES_PANE);
}
private void refreshUsernameLabel() {
String username = UserAccountProvider.getLoggedUser().getUsername();
usernameLabel.setText(username);
}
private void getAllBudgetProfiles() {
List<String> budgetProfileNames = budgetProfileService.getAllBudgetProfileNames();
if (!budgetProfileNames.isEmpty()) {
budgetProfilesComboBox.getItems().clear();
budgetProfilesComboBox.getItems().addAll(budgetProfileNames);
}
}
#FXML
public void handleFinancesButtonAction() {
changeDashboardPane(PaneFactoryKeys.FINANCES_PANE);
}
#FXML
public void handlePeriodButtonAction() {
changeDashboardPane(PaneFactoryKeys.PERIOD_PANE);
}
#FXML
public void handleStatisticsButtonAction() {
changeDashboardPane(PaneFactoryKeys.STATISTICS_PANE);
}
#FXML
public void handleSettingsButtonAction() {
changeDashboardPane(PaneFactoryKeys.SETTINGS_PANE);
}
private final void changeDashboardPane(String paneFactoryKey) {
double injectedPanePosition = 0.0;
Pane paneToChange = getPaneFromFactory(paneFactoryKey);
dashBoardPane.getChildren().clear();
AnchorPane.setTopAnchor(paneToChange, injectedPanePosition);
dashBoardPane.getChildren().add(paneToChange);
}
}
To get this more clear, screens:
without injected second pane
with injected second pane
Any ideas guys?

I would recommend you to divide your main scene in smaller ones, for example you can have a tools scene, a header scene, a content scene and so on. Then you should have one controller for every scene.
After that I would use a publisher-subscriber pattern to deal with behaviors, like when you press a button on settings scene, it triggers an event that other scenes listen to and then they handle it changing their state accordingly.
I hope it was clear and can help!

Create multiple controllers , multiple FXML files - to continue on my answer that i provided you before, JavaFX how to inject new FXML content to current Scene each of those views that have separate fxml file also has
fx:controller="appplication.ExampleViewController"
attached to it.So what you do is create main controller as was mentioned , that is basically the FRAME CONTAINER that encapsulates controls to change your dynamic container.If your application is really ui rich and have a lot of functionality in one controller , you can break down your view even further:
For instance take out menu and put it into separated controller , and insert it into your main view with main controller
/same way as in method setView()/
, what you are doing is just taking it away to keep controller code smaller, YOU DONT DECREASE/INCREASE SCENE GRAPH THIS WAY, doesnt have a drawback its just a personal preference.
You gonna end up with more fxml files and controllers in the end.Its all the same thing as from your previous question there is no additional code needed you can actually reuse what was already provided.
Data between controllers are passed thru MODEL. - look more into MVC dont work with application data in controllers only care about view or passing them from/into model

To avoid a huge contoller class, as I am using multiple tabs, I split the tabs to single java files.
My solution was to create a cascade of classes:
Base: Containing all defs for FX types
Tab1 extends Base: Tab one implementation
Tab2 extends Tab1: Tab two implementation
Controller extends Tab2 implements Initializable: Implements initialize(URL url, ResourceBundle resourceBundle)
Important:
Any accessed object must be definded in the current tab or before.
Any Objects in Base are available in Controller whereas no object of Controller is accessable in Base, Tab1 or Tab2.
Feel free to add your opinion as comment or submit a improvement.

Related

Vaadin: How to add 3 nested layouts

I'm having issues with my UI in vaadin at the moment. I have my views connected with RouterLayout like this:
-AppView (the main UI) | url: /
--OperationsView (a nested layout inside a container in AppView) | url: /operations
---Operation1View (a nested layout inside a container in OperationsView) | url: /operation1 <-
This isn't working
My declarations before any class are:
AppView declaration
#Route(value = AppView.ROUTE)
OperationsView declaration
#Route(value = OperationsView.ROUTE, layout = AppView.class)
Operation1View declaration
#Route(value = Operation1View.ROUTE, layout = OperationsView.class)
The problem is the third layout doesn't display correctly. It takes the whole page when accesed and mess up everything in the UI when going to another page. Shouldn't the url be: /operations/operation1 and not /operation1? However I can't get it to work correctly. Am I missing something? Or having 3 nested layouts is not possible with vaadin?
A possible solution (?): Should I dismiss the third nested layout and add methods in the second layout to remove the contents in the container and display the items I want? I really don't care about url navigation in this one. This is the last thing I can come up with.
Thanks in advance
Or having 3 nested layouts is not possible with vaadin?
It's possible. But are you implementing a RouterLayoutin both OperationsView and AppView classes?
Take a look into example here: Multiple parent layouts with #ParentLayout. It has a set-up pretty close to yours.
public class MainLayout extends Div implements RouterLayout {
}
#ParentLayout(MainLayout.class)
public class MenuBar extends Div implements RouterLayout {
public MenuBar() {
addMenuElement(TutorialView.class, "Tutorial");
addMenuElement(IconsView.class, "Icons");
}
private void addMenuElement(Class<? extends Component> navigationTarget,
String name) {
// implementation omitted
}
}
#Route(value = "tutorial", layout = MenuBar.class)
public class TutorialView extends Div {
}
#Route(value="icons", layout = MenuBar.class)
public class IconsView extends Div {
}
Shouldn't the url be: /operations/operation1 and not /operation1?
No, as in your #Router annotation you have specified that it's operation1. By specifying a layout you are defining the DOM structure, not the navigation route.From docs :
Sets the parent component for the route target component.When navigating between components that use the same layout, the same component instance is reused. Default layout target is the UI, but the layout should not be a custom UI as UI is a special class used to know where the route stack ends and no parent layouts should be involved.
All layout stacks will be appended to the UI as it represents the Body element.
BUT If you want it to be operation\operation1, you should use a #RoutePrefix instead ParentLayout Route Control
It takes the whole page when accesed and mess up everything in the UI when going to another page
Could you show a screenshot or add some details how it messes up?
Edit:
It's actually turned out to be harder to implement than I anticipated, but this seems to work:
MainView.java
#Route("")
public class MainView extends VerticalLayout implements RouterLayout {
....
OperationsView.java
//This is needed if you want "operations" to be accessible on its own
#Route(value = "operations",layout = MainView.class)
#ParentLayout(MainView.class)
public class OperationsView extends VerticalLayout implements RouterLayout {
Div content=new Div();
public OperationsView(){
System.out.println("operations view");
add(new Label("operations view"));
add(content);
}
}
Operation1View.java
#Route(value="operation1",layout = OperationsView.class)
#RoutePrefix("operations")
public class Operation1View extends VerticalLayout {
public Operation1View(){
add(new Label("Operations view"));
}
}

Display Exact Copy of Stage

I am working on a new project that will need to show a separate stage on the secondary monitor. This will be a non-interactive stage (only used to display nodes). I will follow this approach to handle that part.
However, I also want to have a duplicate copy of that stage visible within a pane in my main app. It would need to update itself at the same time the stage does.
Where would I start learning how to implement this? Does Java provide a built-in API to display realtime screenshots of a stage, by chance?
I am no expert but I think there are two ways to do it.
Method 1
Wrap everything of that stage into a main FXML/controller file/class (as a View). Then you need to load that FXML file twice, once in the new stage, the other in a dedicated space you have prepared in your main stage.
The reference of both controller instances should ideally be held at the same object, either in your application class or the class hosting your main stage.
From there, you can either bind values and let the binding API do the work for you.
Example:
Main View:
public class MainView {
private Model model = new Model();
private Pane space; // Dedicated space
public void spawnView() {
FXMLLoader spawnViewLoader = new FXMLLoader(getClass().getResource("View.fxml"));
Parent spawnView = spawnViewLoader.load(); // Need to catch IOException
ViewController spawnController = spawnViewLoader.getController();
spawnController.setup(model);
new Stage(new Scene(view));
FXMLLoader dupViewLoader = new FXMLLoader(getClass().getResource("View.fxml"));
Parent dupView = dupViewLoader.load(); // Need to catch IOException
ViewController dupController = dupViewLoader.getController();
dupController.setup(model);
space.getChildren().setAll(dupView);
}
}
Model:
public class Model {
private final StringProperty title = new SimpleStringProperty();
private final StringProperty titleProperty() { return title; }
private final String getTitle() { return title.get(); }
private final void setTitle(final String title) { this.title.set(title); }
}
View controller:
public class ViewController {
#FXML private Label label;
private Model model;
public void setup(Model model) {
if (model == null)
throw new NullPointerException();
this.model = model;
label.textProperty().bind(model.titleProperty());
}
// Other stuff
}
Be careful of the binding though - I'm quite sure whatever I wrote is going to cause memory leak; both Views will not clean up as long you're holding a reference of model. You can always do it without binding, but it's going to be more tedious to update values.
Method 2
This method is more complex and is likely to be several frames slower. I'm not going to post sample codes as this is not that straight-forward.
You need to have a reference of the Scene of that stage, and use an AnimationTimer to call the snapshot method of the scene object.
Then you need to use an ImageView in your main stage to display the snapshots returned.
Seriously, I think this method would cause the duplicate View to be several frames slower than the original's.

JavaFX FXML access succeed with lookup() method but not with #FXML annotation

I start exploring the JavaFX FXML application technology.
I use one main Stage accessed in Main class with Main.getStage() that is invoked in the start of application with the overriden method public void start(Stage stage). Having two public static Scene inside to keep the persistence while switching them.
#Override
public void start(Stage stage) throws Exception {
STAGE = stage;
LOGIN = new Scene(FXMLLoader.load(getClass().getResource("Login.fxml")));
REGISTER = new Scene(FXMLLoader.load(getClass().getResource("Register.fxml")));
STAGE.setScene(LOGIN);
STAGE.setTitle("FXApplication");
STAGE.show();
}
public static Stage getStage() {
return STAGE;
}
Both Scenes have the same controller class called MainController. Using:
Button with fx:id="buttonLoginRegister" to go to the REGISTER Scene
Button with fx:id="buttonRegisterBack" to go back to the LOGIN one.
and both having the same onClick event handleButtonAction(ActionEvent event). The TextFields are fields for a username to log in/register.
#FXML private Button buttonLoginRegister;
#FXML private Button buttonRegisterBack;
#FXML private TextField fieldLoginUsername;
#FXML private TextField fieldRegisterUsername;
#FXML
private void handleButtonAction(ActionEvent event) throws IOException {
Stage stage = Main.getStage();
if (event.getSource() == buttonLoginRegister) {
stage.setScene(Main.REGISTER);
stage.show();
// Setting the text, the working way
TextField node = (TextField) stage.getScene().lookup("#fieldRegisterUsername");
node.setText(fieldLoginUsername.getText());
// Setting the text, the erroneous way
// fieldRegisterUsername.setText(fieldLoginUsername.getText());
} else {
stage.setScene(Main.LOGIN);
stage.show();
}
}
My goal is to copy the value from the LOGIN TextField to the one in the REGISTER scene. It works well using the code above. However firstly I tried to access the element in the another Scene with:
fieldRegisterUsername.setText(fieldLoginUsername.getText());
And it's erroneous. To be exact, the fieldRegisterUsername is null.
Why are some elements found with the lookup(String id) method and not with #FXML annotation?
As mentioned in my comment, sharing a controller between different views is rarely a good idea, and I'd strongly advise you to make a separate controller for each view.
As to your problem itself - you have two instances of your controller class, one for each time you call FXMLLoader.load. Presumably, one view has the fieldLoginUsername TextField, while the other has fieldRegisterUsername.
If the condition of the if statement is met, it means the active scene was the Login scene, thus the controller handling it is the one which has fieldLoginUsername, so naturally fieldRegisterUsername will be null.
But on the first line inside the if clause you change the active scene to the Register one, so by the time you call scene#lookup you are referring to the scene whose controller is the Register controller, the one that does have fieldRegisterUsername.
If you were to call scene#lookup before changing the active scene you would find it returns null as well.
If you must use the same class for controller, you probably want to make sure you only have one instance of that class. That would necessitate using FXMLLoader#setController.

How can I pass objects between the JavaFX controllers in this source code?

Link to tutorial explaining the code.
https://www.youtube.com/watch?v=5GsdaZWDcdY
Overview:
class ScreensController extends StackPane //keep pane so we can remove add screens on top/bottom
It has this method for loading multiple screens.
public boolean loadScreen(String name, String resource) {
try { //fxml file
FXMLLoader myLoader = new FXMLLoader(getClass().getResource(resource));
//System.out.println(resource);
Parent loadScreen = (Parent) myLoader.load();//class cast to controlled screen
ControlledScreen myScreenControler = ((ControlledScreen) myLoader.getController());//Returns the controller associated with the root object.
//inject screen controllers to myscreencontroller
myScreenControler.setScreenParent(this);// inject screen controllers to each screen here
addScreen(name, loadScreen);
return true;
} catch (Exception e) {
System.out.println(e.getMessage());
return false;
}
}
Each screen controller has this object
ScreensController myController;
And this method for switching between screens (switch which controller is in control)
#FXML
private void goToScreen2(ActionEvent event){
myController.setScreen(ScreensFramework.screen2ID);
}
public interface ControlledScreen {
//This method will allow the injection of the Parent ScreenPane
public void setScreenParent(ScreensController screenPage);
}
This is a very brief synopsis and I'm sure I'm leaving out integral parts of the code.
My question is basically How can I send something (anything) from one screen to another?
I'd also like to be able to have a universal model object that all the controllers can access and control.
If you take a look at my code here
You will see I am putting all my model objects into the "initialize" method of each SEPARATE controller.
If any of you can give me some pointers or feedback on my code I would really appreciate it. Thanks.

Linking content from different fxml files

I have a JavaFx project I created using SceneBuilder.
I am also using a Guice plugin architecture.
I have one .fxml file that has a pane that I want to be the the content of another .fxml file.
Is there any easy way to link .fxml content from one file to another?
I have not used fx.guice plugin architecture before. Is there an easier way to this with plugin control?
Thanks!!
This was a big problem for us since we also are using Guice and JavaFX.
tl;dr I've attached some code at the bottom that we've been using for a year and a bit now without issue.
edit: I should've mentioned we did all this stuff before fx,guice existed, so this exists entirely outside that, and I probably should be using it.
Yes, but you will have to modify the view tree from java, and you must instantiate two controllers. If you're willing to let the fxml loader instantiate your controllers (we're not, more in a sec), then you simply need to have code along the lines of
solution 1:
loadMergedView(){
fxmlLoader.setLocation(getClass().getResource("/com/yourpkg/YourOuterView.fxml"));
Pane outerRoot = fxmlLoader.load();
fxmlLoader.setLocation(getClass().getResource("/com/yourpkg/YourInnerView.fxml"));
Pane innerView = fxmlLoader.load();
((Region)outerRoot.getChildren().get(2))...getChildren().add(innerView);
}
which isn't nice because it means the FX loader will try to create your controller for you, but you probably want guice to do that
luckily you can call setController (or setControllerFactory) to leverage guice, so now we have
solution 2:
#Inject private OuterController outerController
#Inject private InnerController innerController
loadMergedView(){
fxmlLoader.setLocation(getClass().getResource("/com/yourpkg/YourOuterView.fxml"));
//using the setControllerFactory instead of the setController
//means you can still declare the controller type in FXML
//which is good for our IDE intelliJ and general readability
fxmlLoader.setControllerFactory(() -> outerController);
Pane outerRoot = fxmlLoader.load();
fxmlLoader.setLocation(getClass().getResource("/com/yourpkg/YourInnerView.fxml"));
fxmlLoader.setControllerFactory(() -> innerController);
Pane innerView = fxmlLoader.load();
((Region)outerRoot.getChildren().get(2))...getChildren().add(innerView);
}
which is better but requires a 3rd party to load your components. What you really want, a le dependency-injection, is to have the child view be resolved first and as part of the resolution of the parent view.
For us, this brings us to
Solution 3:
class OuterController{
#FXML Pane rootPane;
#FXML Stuff otherStuffBoundInFXML;
#FXML AnchorPane innerContactPaneOne;
#Inject
public OuterController(InnerController inner, FXMLLoader loader){
loader.setControllerFactory(type -> this);
loader.setLocation(getClass().getResource("/com/yourpkg/YourOuterView.fxml"));
loader.load();
innerContactPaneOne.getChildren().add(inner.getRootView());
}
}
class InnerController{
#FXML Pane innerContentPaneTwo; //this will be a child of PaneOne in OuterController
#FXML Button otherStuff;
#Inject
public InnerController(FXMLLoader loader){
loader.setControllerFactory(type -> this);
loader.setLocation(getClass().getResource("/com/yourpkg/YourInnerVIew.fxml"));
loader.load();
}
public Node getRootView(){
return innerContentPaneTwo;
}
}
//with somebody calling
OuterController rootController = injector.getInstance(OuterController.class);
And finally, in the name of 'convention over configuration', we created a few classes (attached below) that attempt to 'automatically' find the view by reflecting on the controllers name (eg OuterController) and assuming that it will find an FXML view view in the same directory as the controller's class file with the word controller replaced with view (eg OuterView.fxml)). We also leveraged a neat trick in super-ctor-order in java to allow us to have pre-setup FXML values.
So now we get:
solution4 :
class OuterController extends PreloadedFX{
#FXML Pane rootPane;
#FXML Stuff otherStuffBoundInFXML;
#FXML AnchorPane innerContactPaneOne;
#FXML Checkbox importantCheckbox;
#FXML Label importantLabel;
// because of 'PrealoadedFX' getting called first,
// you can actually inline initialize object constants
// like this
private final ObservableBooleanValue isSelected = importantCheckbox.selectedProperty();
// or using an initializer
{
int x = 4;
importantLabel.setText(importantLable.getText() + x);
}
#Inject
public OuterController(InnerController inner, FXMLLoader loader){
super(loader);
innerContactPaneOne.getChildren().add(inner.getRootView());
}
}
class InnerController extends PreloadedFX{
#FXML Pane innerContentPaneTwo; //this will be a child of PaneOne in OuterController
#FXML Button otherStuff;
#Inject
public OuterController(FXMLLoader loader){
super(loader);
}
public Node getRootView(){
return innerContentPaneTwo;
}
}
//with somebody calling
OuterController rootController = injector.getInstance(OuterController.class);
you can get the source code for the PreloadedFX and View-By-Convention code here:
https://gist.github.com/Groostav/ff35eb2d19b348f2e25c
which is as elegant as I've been able to make this particular union of frameworks.
Hope that helps!

Categories