Florian Wolters
Florian Wolters

Reputation: 4080

Create a null-safe BooleanBinding with JavaFX 8

I need help creating a null-safe BooleanBinding. It has to be null-safe since I can not provide default values for all attributes in the model (one reason: the model contains enumerations). My first approach has been as follows:

    executeButtonDisabled.bind(missionProperty().isNotNull().and(missionProperty().get().statusProperty().isNotEqualTo(MissionStatus.CREATED)));
    final BooleanBinding isNotExecutingBinding = missionProperty().isNotNull().and(missionProperty().get().statusProperty().isNotEqualTo(MissionStatus.EXECUTING));
    completeButtonDisabled.bind(isNotExecutingBinding);
    cancelButtonDisabled.bind(isNotExecutingBinding)

But that approach does not work because the complete expression is evaluated which results in a NullPointerException (but it correctly updates the buttons, when a property is provided). Now I am trying to use the Bindings class as suggested in JavaFX binding and null values, but I can't make it work. Here's my current approach:

    final BooleanBinding isNotCreatedBinding = Bindings.createBooleanBinding(
            () -> mission.isNull().getValue()
                    ? true
                    : missionProperty().get().statusProperty().isNotEqualTo(MissionStatus.CREATED).getValue());
    final BooleanBinding isNotExecutingBinding = Bindings.createBooleanBinding(
            () -> mission.isNull().getValue()
                    ? true
                    : missionProperty().get().statusProperty().isNotEqualTo(MissionStatus.EXECUTING).getValue());

    executeButtonDisabled.bind(isNotCreatedBinding);
    completeButtonDisabled.bind(isNotExecutingBinding);
    cancelButtonDisabled.bind(isNotExecutingBinding);

But this does not work and I do not understand why. It seems that the property binding for modelProperty() does not work here! Can you explain to me how-to convert the first working solution (at least without null) to a proper null-safe solution?

Edit 2016-04-26: The suggested solution does not work, therefore I created a simple fully-working example:

Mission.java:

package de.florianwolters.example.javafx.bindings;

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

public class Mission {

    enum Status {
        CREATED,
        EXECUTING,
        COMPLETED,
        CANCELED;
    }

    private final StringProperty shortName = new SimpleStringProperty();

    private final ObjectProperty<Status> status = new SimpleObjectProperty<>();

    public Mission(final String shortName) {
        this.setShortName(shortName);
        this.setStatus(Status.CREATED);
    }

    public String getShortName() {
        return shortNameProperty().get();
    }

    public void setShortName(final String shortName) {
        shortNameProperty().set(shortName);
    }

    public StringProperty shortNameProperty() {
        return shortName;
    }

    public Status getStatus() {
        return statusProperty().get();
    }

    public void setStatus(final Status status) {
        statusProperty().set(status);
    }

    public ObjectProperty<Status> statusProperty() {
        return status;
    }
}

MissionDetailsViewModel.java:

package de.florianwolters.example.javafx.bindings;

import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.SimpleObjectProperty;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public final class MissionDetailsViewModel {

    /**
     * The logger used for logging in the `MissionDetailsViewModel` class.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(
        MissionDetailsViewModel.class);

    private ObjectProperty<Mission> mission = new SimpleObjectProperty<>();

    private final ReadOnlyBooleanWrapper executeButtonDisabled = new ReadOnlyBooleanWrapper(true);

    private final ReadOnlyBooleanWrapper completeButtonDisabled = new ReadOnlyBooleanWrapper(true);

    private final ReadOnlyBooleanWrapper cancelButtonDisabled = new ReadOnlyBooleanWrapper(true);

    /**
     * Constructs a `MissionDetailsViewModel`.
     */
    public MissionDetailsViewModel(final ObjectProperty<Mission> mission) {
        this.mission.bind(mission);
//        partialWorkingBinding();
        notWorkingBinding();
    }

    private void notWorkingBinding() {
        final BooleanBinding isNotCreatedBinding = Bindings.createBooleanBinding(
            () -> missionProperty().isNull().get()
                ? true
                : missionProperty().get().statusProperty().isNotEqualTo(Mission.Status.CREATED).get(),
            missionProperty());

        final BooleanBinding isNotExecutingBinding = Bindings.createBooleanBinding(
            () -> mission.isNull().get()
                ? true
                : missionProperty().get().statusProperty().isNotEqualTo(Mission.Status.EXECUTING).get(),
            missionProperty());

        executeButtonDisabled.bind(isNotCreatedBinding);
        completeButtonDisabled.bind(isNotExecutingBinding);
        cancelButtonDisabled.bind(isNotExecutingBinding);
    }

    private void partialWorkingBinding() {
        executeButtonDisabled.bind(missionProperty().isNotNull().and(missionProperty().get().statusProperty().isNotEqualTo(Mission.Status.CREATED)));
        final BooleanBinding isNotExecutingBinding = missionProperty().isNotNull().and(missionProperty().get().statusProperty().isNotEqualTo(Mission.Status.EXECUTING));
        completeButtonDisabled.bind(isNotExecutingBinding);
        cancelButtonDisabled.bind(isNotExecutingBinding);
    }

    public boolean isExecuteButtonDisabled() {
        return executeButtonDisabledProperty().get();
    }

    public ReadOnlyBooleanProperty executeButtonDisabledProperty() {
        return executeButtonDisabled;
    }

    public boolean isCompleteButtonDisabled() {
        return completeButtonDisabledProperty().get();
    }

    public ReadOnlyBooleanProperty completeButtonDisabledProperty() {
        return completeButtonDisabled;
    }

    public boolean isCancelButtonDisabled() {
        return cancelButtonDisabledProperty().get();
    }

    public ReadOnlyBooleanProperty cancelButtonDisabledProperty() {
        return cancelButtonDisabled;
    }

    public Mission getMission() {
        return missionProperty().get();
    }

    public void setMission(final Mission mission) {
        missionProperty().set(mission);
    }

    public ObjectProperty<Mission> missionProperty() {
        return mission;
    }
}

MissionDetailsViewModelTest.java:

package de.florianwolters.example.javafx.bindings;

import static eu.lestard.assertj.javafx.api.Assertions.assertThat;
import javafx.beans.property.SimpleObjectProperty;

import org.junit.Before;
import org.junit.Test;

public final class MissionDetailsViewModelTest {
    private Mission mission;
    private MissionDetailsViewModel viewModel;

  @Before
  public void setUp() {
        mission = new Mission("My Short Name");
    viewModel = new MissionDetailsViewModel(new SimpleObjectProperty<Mission>(mission));
  }

  @Test
  public void testInitialValues() {
        assertThat(viewModel.executeButtonDisabledProperty()).isFalse();
        assertThat(viewModel.completeButtonDisabledProperty()).isTrue();
        assertThat(viewModel.cancelButtonDisabledProperty()).isTrue();
    }

    @Test
  public void testMissionStatusSetToExecuting() {
        mission.setStatus(Mission.Status.EXECUTING);
        assertThat(viewModel.executeButtonDisabledProperty()).isTrue();
        assertThat(viewModel.completeButtonDisabledProperty()).isFalse();
        assertThat(viewModel.cancelButtonDisabledProperty()).isFalse();
    }

    @Test
  public void testMissionStatusSetToCompleted() {
        mission.setStatus(Mission.Status.COMPLETED);
        assertThat(viewModel.executeButtonDisabledProperty()).isTrue();
        assertThat(viewModel.completeButtonDisabledProperty()).isTrue();
        assertThat(viewModel.cancelButtonDisabledProperty()).isTrue();
    }

    @Test
  public void testMissionStatusSetToCanceled() {
        mission.setStatus(Mission.Status.CANCELED);
        assertThat(viewModel.executeButtonDisabledProperty()).isTrue();
        assertThat(viewModel.completeButtonDisabledProperty()).isTrue();
        assertThat(viewModel.cancelButtonDisabledProperty()).isTrue();
    }
}

The unit test fails with the code above (the method notWorkingBinding() is used) but works with the method partialWorkingBinding(). What am I doing wrong?

Upvotes: 1

Views: 2135

Answers (2)

jns
jns

Reputation: 6952

You set up the calculation function for isNotCreatedBinding, but you didn't set the dependencies for the binding. You need to add mision as dependency:

 Bindings.createBooleanBinding(
        () -> mission.isNull().getValue()
                ? true
                : missionProperty().get().statusProperty().isNotEqualTo(MissionStatus.CREATED).getValue(), mission);

EDIT

You need to listen to the statusProperty instead of missionProperty, which will not work with createBooleanBinding when missionProperty().get() == null.

But you can use a When binding:

(is causing a NullPointerException as already mentioned in the question)

   BooleanBinding isNotCreatedBinding = new When(mission.isNotNull()).then(mission.get().statusProperty().isNotEqualTo(Mission.Status.CREATED)).otherwise(false);

Or a more low-level solution:

 missionProperty().addListener((ov, m, m1) -> {
            if (m1 != null) {
                executeButtonDisabled.bind(m1.statusProperty().isNotEqualTo(Mission.Status.CREATED));
            }else {
                executeButtonDisabled.unbind();
                executeButtonDisabled.set(false);
            }
        });

Upvotes: 3

James_D
James_D

Reputation: 209225

Tomas Mikula's ReactFX framework (v 2.0) has this functionality built in:

import org.reactfx.value.Val;

import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;

public class NestedBindingTest {
    public static void main(String[] args) {
        BooleanProperty disable = new SimpleBooleanProperty();
        disable.addListener((obs, wasDisabled, isNowDisabled) -> 
            System.out.println("disable: "+wasDisabled+" -> "+isNowDisabled));

        ObjectProperty<Item> item = new SimpleObjectProperty<>();

        Val<Item.Status> status = Val.flatMap(item, Item::statusProperty);
        disable.bind(status.map(s -> s == Item.Status.PENDING).orElseConst(true));

        Item i = new Item();
        System.out.println("Setting item");
        item.set(i);

        System.out.println("Setting item status to PENDING");
        i.setStatus(Item.Status.PENDING);

        System.out.println("Setting item status to READY");
        i.setStatus(Item.Status.READY);

        System.out.println("Setting item to null");
        item.set(null);
    }

    public static class Item {
        public enum Status {PENDING, READY}

        private final ObjectProperty<Status> status = new SimpleObjectProperty<>();

        public final ObjectProperty<Status> statusProperty() {
            return this.status;
        }


        public final NestedBindingTest.Item.Status getStatus() {
            return this.statusProperty().get();
        }


        public final void setStatus(final NestedBindingTest.Item.Status status) {
            this.statusProperty().set(status);
        }



    }
}

Upvotes: 2

Related Questions