Devlog - Creating a MVCI JavaFX Application
Tags: java, javafx, docker, mvci
Published: 2024-05-27 16:24:49

## Introduction
In this Devlog, I plan to detail the application I have been developing and the lessons I have learnt from choosing JavaFX as my platform to build my application on.

{% embed https://youtu.be/6MlOMs-bs5g %}

List of dependancies used: [Source Code](https://github.com/FamaZeleke/devInspector/blob/main/pom.xml)

## Concept   
The project began as a CLI-style mini-Java application. At my workplace, we use Docker extensively, so I aimed to build a tool to support developers with a similar tech stack. The initial application used the Scanner class to take user input and execute commands like `docker ps`.
```
public static void dockerMenu(Scanner input) {
        out.println("╔══════════════════════════════╗");
        out.println("║        Docker Commands       ║");
        out.println("╚══════════════════════════════╝");
        out.println("     1. ➤ Option 1: Docker PS");
        out.println("     2. ➤ Option 2: Docker Images");
        out.println("     3. ➤ Option 3: Back");
        out.println("\nPlease select an option (1-3): ");
        int selection = input.nextInt();
        input.nextLine();

        String selectedOption = getAndHandleUserInput(selection, 3);
        if (selectedOption != null) {
            out.println("Selected option: " + selectedOption);
            switch (selectedOption) {
                case "selectedMenu1": {
                    DockerCLI.runDockerCommand("ps");
                    break;
                }
                case "selectedMenu2": {
                    DockerCLI.runDockerCommand("images");
                    break;
                }
            }
        }
    }
```

Whilst useful and easily extensible, I felt the need to still access docker to do more complicated actions. This became the basis of the problem that my application will grow to solve - how to interact with docker without using the docker desktop or docker CLI. 

My solution was to build an application that will streamline how a user interacts with docker through a neat GUI built with JavaFX. In time, I will add more features that will go beyond what Docker Desktop already does, but to keep the scope focused, I decided to remake the core features in the application first.

## Planning the UI
I used Figma, a fantastic tool for design and prototyping, to create quick wireframes for guiding the UI development. This approach prevents the common pitfall of building an ad-hoc UI that can quickly become unmanageable. Instead, it provides constraints and clear goals for development. [Link to Figma Board](https://www.figma.com/design/0P3mFhyRBIZXaJDxASDNxN/JavaFX?node-id=56-475&t=HtN4ksTcmBvtVYsS-1)

![Wireframes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3caf7mxv9fxkyr8bgt7c.png)



## First Blocker
JavaFX has a preference to be ran inside a modular project:
```
module nivohub.digitalartifactmaven {
    requires javafx.controls;
    requires javafx.fxml;
    requires docker.client;

    exports nivohub.devInspector;
    exports nivohub.devInspector.view;
    exports nivohub.devInspector.model;
    exports nivohub.devInspector.controller;
}
```
As the project grew, I encountered split package issues with the Docker Java API. These issues arose because the Docker Java API library has the same package names split across multiple modules that conflict with the strict module boundaries enforced in a modular project - [Solutions that may work for you in this event](https://msayag.github.io/SplitPackage/)


Since I could not find an easy solution for using this library in a modular project, I opted to forgo the benefits introduced in Java 9 and made my project non-modular by creating a launcher class to bypass the module system constraints [Deploying non-modular JavaFX applications](https://nikhiladigaz.medium.com/deploying-non-modular-javafx-applications-using-maven-and-launch4j-fb95b8ab0739).
```
package nivohub.devInspector;

public class MainApplicationLauncher {
    public  static void main (String[] args) {
        AppRoot.main(args);
    }
}

```
I was not too worried about this because the system architecture I chose inherently provides benefits like encapsulation, separation of concerns, and reinforces single responsibility when implemented correctly. Therefore, managing the visibility of internal classes seemed excessive for this project.

## Wrestling with Model-View-Controller
I initially built the project with an MVC system where the view - consumes the model's data and the controller - takes actions from the user, driven by events in the view, which then in turns updates the model to be represented in the view.
```
public class DockerController {
    private final DockerScene view;
    private final DockerManager model;

    public DockerController(DockerScene view, DockerManager model) {
        this.view = view;
        this.model = model;

        populateImageSelection();
        setupImageSelectionListener(view);
        handleRunButtonAction();
    }
```
### This introduced a few challenges:

**1. Limited documentation on MVC in JavaFX**

The first issue in choosing this system for my application was the limited information available online on how to properly implement MVC in JavaFX - this, coupled with contradictions online and my own misunderstandings of MVC led to tightly coupled classes. For instance, in the DockerScene class, the controller setup is tightly coupled, requiring type checks and casting:
```
   public DockerScene(AppMenu appMenu) {
        super(appMenu);
        this.appMenu = appMenu;
    }

    @Override
    public void setController(Object controller) {
        if (controller instanceof DockerController) {
            this.controller = (DockerController) controller;
        } else {
            throw new IllegalArgumentException("Controller must be a DockerController");
        }
    }
```
**2. Model and Domain Objects**

Another challenge was managing the complexity of the Model. The Model contains all the data and logic to interface with external parts of the system, such as databases or external APIs.

In my **DockerManager class**, the model not only holds data but also interacts directly with the Docker API, creating a potential single point of failure and making the system more difficult to test and maintain.


![MVC Model](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6z19vz9116qvknxxxx1k.png)

**3. Difficulty in Scaling and Maintaining the Application**

Another significant challenge with MVC in JavaFX is scaling and maintaining the application as it grows. The tight coupling between controllers and views, along with complex models, makes the application harder to extend with new features. 

As the application grows, the interdependencies between different parts of the system become more pronounced, making it difficult to isolate and modify individual components without affecting others. This complexity can lead to increased development time, more bugs, and higher maintenance. [Cohesion and Coupling](https://dev.to/m__mdy__m/cohesion-and-coupling-in-javascript-2efg)

## Model-View-Controller-Interactor

To address the challenges I faced with MVC in JavaFX, I transitioned to the Model-View-Controller-Interactor (MVCI) framework.
[Fantastic resource on MVCI in JavaFX](https://www.pragmaticcoding.ca/javafx/Mvci-Introduction)

![MVCI System](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tv4gvea2ovhemf3k1213.png)

Unlike MVC, MVCI includes an Interactor component that manages the application's business logic and interactions with external systems, like APIs or databases. 

This separation ensures that the Model only contains state data, making the system easier to test and maintain. 

The Interactor handles complex operations, reducing the tight coupling between components and allowing for more scalable and modular application architecture.

### Model
In this example below our **UserModel** is a POJO that takes advantage of JavaFX's bindable properties to store and allow observation of our data to our view:
```
public class UserModel {
    private final StringProperty fullName = new SimpleStringProperty("");
    private final StringProperty inputPassword = new SimpleStringProperty("");
    private final BooleanProperty loginFailed = new SimpleBooleanProperty(this, "loginFailed", false);
    private final BooleanProperty authenticated = new SimpleBooleanProperty(this, "authorized", false);
    private final String password = "letmein";
    private String platform;
    private String osArch;

    public BooleanProperty loginFailedProperty(){
        return loginFailed;
    }

    public BooleanProperty authenticatedProperty() {
        return authenticated;
    }

    public StringProperty fullNameProperty() {
        return fullName;
    }

    public StringProperty inputPasswordProperty() {
        return inputPassword;
    }

    public String getFullName(){
        return fullName.get();
    }

    public String getPassword() {
        return password;
    }

    public String getInputPassword() {
        return inputPassword.get();
    }
```
The UserModel maintains user data and state, such as fullName, inputPassword, loginFailed, and authenticated. 

### View
The view establishes bindings to properties in the model to represent the data to the user. 

In the example below the **LoginViewBuilder** binds the input properties of the name and password to the simple properties in **UserModel**.

It can also take parameters such as a Runnable or Consumer to handle user events and inform the controller... whilst remaining agnostic of what class provides this:
```
public class LoginViewBuilder implements Builder<Region> {

    private final UserModel model;
    private final Runnable loginHandler;

    public LoginViewBuilder(UserModel model, Runnable loginHandler) {
        this.model = model;
        this.loginHandler = loginHandler;
    }

    @Override
    public Region build() {
        GridPane results = new GridPane();
        results.setPadding(new Insets(25));

        results.setHgap(10);
        results.setVgap(10);
        results.setAlignment(Pos.CENTER);

        results.add(welcomeLabel(), 0, 0);
        results.add(boundNameLabel(), 1, 0, 2, 1);
        results.add(nameLabel(), 0, 1);
        results.add(nameInput(), 1, 1);
        results.add(passwordLabel(), 0, 2);
        results.add(passwordInput(), 1, 2);
        results.add(createLoginButton(), 1, 3);
        results.add(errorMessage(), 1, 4);

        return results;
    }


    private Node createLoginButton() {
        Button loginButton = new Button("Login");
        loginButton.setOnAction(event -> loginHandler.run());
        return loginButton;
    }

    private Node passwordInput(){
        return boundPasswordField(model.inputPasswordProperty(), "Enter Password", loginHandler);
    }

    private Node nameInput(){
        return boundTextField(model.fullNameProperty(), "Enter Full Name", loginHandler);
    }
```
I am also implementing an interface provided by JavaFX called **Builder<>** which allows the controller to expose a method to create a view without having to reference the view or its dependancies!

```
public abstract class BaseController {
    protected Builder<Region> viewBuilder;

    public Region getView() {
        return viewBuilder.build();
    }
}
```

### Controller

The controller's job is simply to distributes _'jobs'_ to interactors and other controllers.

In this example, the **LoginController** instantiates the interactor and the view. By implementing the BaseController, we inherit the public method to build the view from the controller as well:
```
public class LoginController extends BaseController{
    private final LoginInteractor interactor;
    private final ApplicationController applicationController;

    public LoginController(UserModel model, ApplicationController applicationController) {
        interactor = new LoginInteractor(model);
        viewBuilder = new LoginViewBuilder(model, this::loginUser);
        this.applicationController = applicationController;
    }

    private void loginUser() {
        Task<Boolean> task = new Task<>() {
            @Override
            protected Boolean call() throws PasswordException {
                return interactor.attemptLogin();
            }
        };
        task.setOnSucceeded(e -> {if (Boolean.TRUE.equals(task.getValue())){
            interactor.updateFailedLogin(false);
            applicationController.loadMainView();
        }
        });
        task.setOnFailed(e -> {
            interactor.updateFailedLogin(true);
        });
        task.run();
    }
}
```
In more complex/fat controllers, using interfaces allows us to effectively share methods across the application to other controllers:
```
public interface DockerInterface {
    void connectDocker();
    void disconnectDocker();
    void startContainer(String containerId);
    void stopContainer(String containerId);
    void removeContainer(String containerId);
    void stopThread();
}
```
For example, the **MenuBarController** uses the interface implementations from the **DockerController** without being aware of the controller itself:
```
public class MenuBarController extends BaseController{
    private final MenuBarInteractor interactor;

    public MenuBarController(ApplicationModel applicationModel, DockerInterface dockerInterface, DockerModel dockerModel, ApplicationInterface applicationInterface) {
        interactor = new MenuBarInteractor(applicationModel);
        viewBuilder = new MenuBarBuilder(this::handleView, dockerInterface, dockerModel, applicationInterface);
    }
```
This interface is then used in the view to handle user-driven events like connecting and disconnecting the Docker connection (This is also how I handle the user input to change the views with the MenuBar !):
```
Node connectDockerButton = styledRunnableButton("Connect", dockerInterface::connectDocker);
                connectDockerButton.disableProperty().bind(dockerModel.dockerConnectedProperty());

Node disconnectDockerButton = styledRunnableButton("Disconnect", dockerInterface::disconnectDocker);
                disconnectDockerButton.disableProperty().bind(dockerModel.dockerConnectedProperty().not());
```
### Interactor
The interactor bridges our system. Below is the **LoginInteractor**, which has two public methods: `attemptLogin()` and `updateFailedLogin()`. The interactor handles the business logic, such as verifying user credentials against the stored password in the model, and updates the state accordingly. 
```
public class LoginInteractor {
    private final UserModel model;

    public LoginInteractor(UserModel model) {
        this.model = model;
        model.authenticatedProperty().bind(Bindings.createBooleanBinding(this::isPasswordValid, model.inputPasswordProperty()));
    }
    private boolean isPasswordValid(){
        return model.getPassword().equals(model.getInputPassword());
    }

    public boolean attemptLogin() throws PasswordException {
        if (isPasswordValid()){
            return true;
        } else {
            throw new PasswordException("Invalid password");
        }
    }

    public void updateFailedLogin(Boolean result){
        model.loginFailedProperty().set(result);
    }
}
```
These methods are called from the **LoginController** as a Task and on success/failure of a Task. This maintains a separation of concerns between workflow control/job distribution and business logic.

Interactors however, allow us to uniquely interact with the outside e.g. domain objects. For example, methods for Docker communication are encapsulated in a domain object [**DockerEngine**](https://github.com/FamaZeleke/devInspector/blob/main/src/main/java/nivohub/devinspector/docker/DockerEngine.java), initialised by the **DockerInteractor**, which accesses the necessary methods within the business logic.

![Docker DOM](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ebryyjlv00okdpt6heph.png)

That wraps up the crux of the MVCI system. PragmaticCoding is a blog written by a retired expert in the field of which I learnt most of this system from: [ProgramaticCoding](https://www.pragmaticcoding.ca/javafx/mvci/)

## Tasks and Threads

One key constraint in using JavaFX is that it handles updates to the UI/Scene graph on a single thread known as the FXAT (JavaFX Application Thread). Meaning all changes to the UI must be done on the FXAT.

It also has no concept of time and is simply a queue of jobs to be executed in order. PragmaticCoding covers this in detail here: [FXAT](https://www.pragmaticcoding.ca/javafx/elements/fxat#what-is-the-fxat-anyway)

Since my application revolves around communicating with Docker, which includes potential long running tasks, it would not be performant to execute those tasks on the FXAT...

This is where Tasks come in:
```
  @Override
    public void connectDocker() {
        interactor.addToOutput("Connecting to Docker...");
        Task<Void> task = new Task<>() {
            @Override
            protected Void call() throws DockerNotRunningException {
                interactor.connectDocker();
                return null;
            }
        };
        task.setOnSucceeded(e -> {
            interactor.addToOutput("Connected to Docker");
            interactor.updateModelConnection(true);
            interactor.listContainers();
            interactor.listImages();
        });
        task.setOnFailed(e -> interactor.addToOutput("Failed to connect to Docker :"+e.getSource().getException().getMessage()));
        new Thread(task).start();
    }
```

> Task is an abstract generic class that has methods for communicating between a background thread and the FXAT and a single abstract method name call().

In my example above, the **task** containing the call to **connectDocker()** runs on a new background thread (this is because I am using apacheHttp5 to communicate with docker for window users which is a long running task). On success or failure, it updates the UI on the FXAT e.g. `interactor.addToOutput("Connected to Docker")`

This prevents the long running tasks from blocking the UI and making it unresponsive as it is ran on a background thread and while still being able to update UI on the FXAT when the task gets completed.

### Future Plans

This DevLog covers the current state of my project. Moving forward, I plan to enhance this application by integrating industry-leading libraries like Guice for dependency injection, which will not only simplify writing tests but also improve overall code modularity and manageability. I also will aim to address potential threading issues, such as memory leaks and deadlocks, by implementing thread pooling or exploring green threads. This will ensure more efficient and safe multi-threading within the application.

I attempted to manage distribution in a .app for MacOS users and provided a .bat for Windows but there are simpler packaging solutions that can make this application run across platforms like making an executable jar.

Beyond that, I want to extend the features of the application to provide a unique use case that is differentiated from the Docker Desktop Client. The investment so far has given me an extensible foundation to do so.