Warning: Declaration of Bootstrap_Walker_Nav_Menu::start_lvl(&$output, $depth) should be compatible with Walker_Nav_Menu::start_lvl(&$output, $depth = 0, $args = Array) in /homepages/10/d412081534/htdocs/clickandbuilds/WordPress/wordpress/wp-content/themes/stanleywp/functions/function-extras.php on line 61
JavaFX – Decouple the View and its behavior to create a testable UI | buildpath.de

JavaFX – Decouple the View and its behavior to create a testable UI

The behavior of an UI is a complex state machine which should be tested. This can be done not only by automated UI-tests, but also with Unit Tests. It’s not easy to write Unit Tests for UIs,  because you need to decouple the state of the UI from its declaration.

One approach is the MVVM pattern. MVVM introduces a ViewModel layer that contains the state and behavior of an UI. The View only represents the ViewModel and reacts on its changes.

Shortfacts MVVM:

  • View does not know the Model
  • Model does not know anybody ;-)
  • ViewModel does not know the View (one difference to MVP)
  • ViewModel knows Model

 

Let’s try it. We want to build a login component which has two text fields (name/password) and a login button. The button should only be active when there is some text in the text fields. You can find the example on GitHub.

Step 1 - Write tests to specify the ViewModel

Because the ViewModel represents the UI state, we create the ViewModel first.Testdriven. Therefore we write an unit test to specify the behavior. The ViewModel should provide three properties which can be accessed

  • userNameProperty() – represents the current user input for the username
  • passwordProperty() - represents the current user input for the password
  • loginPossibleProperty() - represents whether a login is possible based on conditions (username and password not empty)

 

public class LoginViewModelTest {
	@Test
	public void disableLoginButton() throws Exception {
		LoginViewModel loginViewModel = new LoginViewModel();
		// No username and password were set
		Assert.assertFalse(loginViewModel.isLoginPossibleProperty().get());

		loginViewModel.userNameProperty().set("Stefanie Albrecht");
		// username was set, but no password
		Assert.assertFalse(loginViewModel.isLoginPossibleProperty().get());

		loginViewModel.passwordProperty().set("<3");
		// username and password were set
		Assert.assertTrue(loginViewModel.isLoginPossibleProperty().get());
	}
}

Step 2 - Implement the ViewModel, so the tests can pass

Yea – we have a test, but the test won’t compile and pass, if we don’t provide a proper implementation. So lets implement the ViewModel.

The userName and the password properties are representing the current input of upcoming text fields, so they are of the type StringProperty. The loginPossible property signals whether the user provided some input for the username and password field. It should be true, if both other properties have values.

public class LoginViewModel {

	private final StringProperty userName = new SimpleStringProperty();
	private final StringProperty password = new SimpleStringProperty();
	private final ReadOnlyBooleanWrapper loginPossible = new ReadOnlyBooleanWrapper();

	public LoginViewModel() {
                //Logic to check, whether the login is possible or not
		loginPossible.bind(userName.isNotEmpty().and(password.isNotEmpty()));
	}

	public StringProperty userNameProperty() {
		return userName;
	}

	public StringProperty passwordProperty() {
		return password;
	}

	public ReadOnlyBooleanProperty isLoginPossibleProperty() {
		return loginPossible.getReadOnlyProperty();
	}
}

Step 3 - Implement the View and bind it to the ViewModel

We can implement the View after the unit test for the ViewModel passed. I suggest to create a FXML-file by using the Scene Builder first. Now we create the JavaFX-Controller which is also part of the View layer in MVVM. You can view both files in the next code box by clicking on the tabs of the box. Let’s connect the View and the ViewModel by data binding, which ensures the propagation of changes in the properties. As we want the login button to be deactivated when there is no input in the text fields, we have to bind the disableProperty() of the login button to the isLoginPossibleProperty() in the ViewModel.

Login with Scene Builder

public class LoginView {

	@FXML
	private TextField userNameTextField;

	@FXML
	private PasswordField passwordTextField;

	@FXML
	private Button loginButton;

	@FXML
	void initialize() {
                //Create the ViewModel - In production this should be done by dependency injection
		LoginViewModel loginViewModel = new LoginViewModel();
                //Connect the ViewModel
		userNameTextField.textProperty().bindBidirectional(loginViewModel.userNameProperty());
		passwordTextField.textProperty().bindBidirectional(loginViewModel.passwordProperty());
		loginButton.disableProperty().bind(loginViewModel.isLoginPossibleProperty().not());
	}
}
<AnchorPane xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="de.saxsys.mvvm.LoginView">
 <children>
  <VBox alignment="CENTER_RIGHT" spacing="10.0">
   <children>
    <TextField fx:id="userNameTextField" alignment="BOTTOM_RIGHT" promptText="Username" />
    <PasswordField fx:id="passwordTextField" alignment="CENTER_RIGHT" promptText="Password" />
    <Button fx:id="loginButton" mnemonicParsing="false" onAction="#onLoginButtonPressed" text="Login" textAlignment="CENTER" />
   </children>
   <padding>
    <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
   </padding>
  </VBox>
 </children>
 <opaqueInsets>
 <Insets />
 </opaqueInsets>
</AnchorPane>

If the user enters a username or password the changes will be propagated by data binding from the View to the ViewModel. The ViewModel reevaluates the defined binding of the isLoginPossibleProperty(). A possible change of the isLoginPossibleProperty()  will be propagated to the disabledProperty() of the loginButton .

As you can see, we removed every logic from the view. The view is stupid and the behavior testable.

The example can be found on GitHub.

Share Button

5 Comments

  1. Pingback: JavaFX links of the week, July 22 // JavaFX News, Demos and Insight // FX Experience

  2. Uwe

    Thanks for this nice example of MVMM.

    How would you implement a more complex example in which the view contains a TreeView element that show elements of a more sophisticated model? Would the view know these model elements such that you would have something like TreeView in the view? Or would you use TreeView to keep the view free from any model references? This would result in some overhead when managing the items in the TreeView, I suppose.

    1. sialcasa

      Hi Uwe,

      thanks for your response. I’m going to prepare another blog post regarding your question.

      Cheers,
      Alex

      1. bennet

        First of all .. your listed Code and your uploaded one are different.
        The Action #onLoginButtonPressed in the LoginView.fxml is completely missing and if you would implement such an Action, you would have to place it into the LoginView.java.. this leads to controller logic in your View class.

        Best regards,
        Bennet

        1. sialcasa

          Hi Bennet,

          thanks for the hint. I hope the differences are not that big.

          No you don’t have to implement the action in the controller. You have several options to separate this from the view.

          • delegate the call by calling a method on the VM
          • offer a callback from the VM to the View which can be called on buttonclick
          • use a BooleanProperty in the VM which you set true if the button was clicked, then you can handle the change in the VM
          • use an event-bus to communicate the button-click somewhere else in the application

          I recommend you to read Microsoft’s documentation about MVVM to get an idea how to implement more complex hierarchies of views.

          Cheers,
          Alex

  3. Robin

    Hello,

    Thanks for the brief tutorial! I have a question though:
    In which layer do i put my business logic? EventHandlers and such? Are they still in the controller? (So I need to test it anyway?)

    Thanks

Leave a Reply to sialcasa Cancel reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>