Binding to JavaFX properties of an object that can be switched
Asked Answered
B

3

11

Binding to properties of an object which is itself wrapped in a property seems like something one does a lot in typical applications, is there a better way to do this in JavaFX than what I do below?

Some more details to explain: I want to make GUI in JavaFX 2.2, for managing a number of items. I've created a small example to test everything, in which the items are persons. The set of persons is shown in a custom way (not a list or tree, but I don't think that matters here), and I can select a single one.

In a side panel I can edit the currently selected person. Updates are immediately visible in the set of persons, and when I select another person, the edit panel is updated.

JavaFX's bidirectional binding seems perfect for this purpose. I currently have this for the fx:controller of the "person editing" Pane:

public class PersonEditor implements ChangeListener<Person> {
    @FXML private TextField nameField;
    @FXML private TextField ageField;
    @FXML private TextField heightField;

    public void setSelection(ObjectProperty<Person> selectedPersonProperty) {
        selectedPersonProperty.addListener(this);
    }

    @Override
    public void changed(ObservableValue<? extends Person> observable, Person oldVal, Person newVal) {
        if (oldVal != null) {
            nameField.textProperty().unbindBidirectional(oldVal.nameProperty());
            ageField.textProperty().unbindBidirectional(oldVal.ageProperty());
            heightField.textProperty().unbindBidirectional(oldVal.heightProperty());
        }
        if (newVal != null) {
            nameField.textProperty().bindBidirectional(newVal.nameProperty());
            ageField.textProperty().bindBidirectional(newVal.ageProperty());
            heightField.textProperty().bindBidirectional(newVal.heightProperty());
        }
    }
}

I'm wondering if there is a nicer way, perhaps something in JavaFX to do bind to properties of an object that can change? I don't like the fact that I have to manually unbind all properties, it feels like duplicate code. Or is this as simple as it can be in JavaFx?

Backbreaking answered 16/5, 2013 at 14:21 Comment(0)
B
2

It seems this is not something that can be done more elegantly in JavaFX. Binding and unbinding seems the cleanest way.

I did implement one way to do this myself. Not sure if I'll eventually end up using it (as it just replaces the code duplication with hard to read code). But it works, and it is an answer to my own question, so I've added it here.

The new PersonEditor class:

public class PersonEditor implements Initializable {
    private SelectedObjectPropertyBinder<Person> selectedObjectPropertyBinder = 
          new SelectedObjectPropertyBinder<Person>();

    @FXML private TextField nameField;
    @FXML private TextField ageField;
    @FXML private TextField heightField;

     @Override
    public void initialize(URL url, ResourceBundle rb) {
        selectedObjectPropertyBinder.getBinders().add(
            new ObjectPropertyBindHelper<Person>(nameField.textProperty()) {
                @Override public Property objectProperty(Person p) 
                { return p.nameProperty(); }
        });
        selectedObjectPropertyBinder.getBinders().add(
            new ObjectPropertyBindHelper<Person>(ageField.textProperty()) {
                @Override public Property objectProperty(Person p) 
                { return p.ageProperty(); }
        });
        selectedObjectPropertyBinder.getBinders().add(
            new ObjectPropertyBindHelper<Person>(heightField.textProperty()) {
                @Override public Property objectProperty(Person p) 
                { return p.heightProperty(); }
        });
    }

    public void setSelection(ObjectProperty<Person> selectedPersonProperty) {
        selectedObjectPropertyBinder.
            setSelectedObjectProperty(selectedPersonProperty);
    }
}

The helper classes:

public class SelectedObjectPropertyBinder<T> implements ChangeListener<T> {
    private List<ObjectPropertyBindHelper<T>> binders = 
           new ArrayList<ObjectPropertyBindHelper<T>>();

    public void setSelectedObjectProperty(Property<T> selectionProperty) {
        selectionProperty.addListener(this);
    }

    public List<ObjectPropertyBindHelper<T>> getBinders() {
        return binders;
    }

    @Override
    public void changed(ObservableValue<? extends T> observable, 
                        T oldVal, T newVal) {
        if (oldVal != null)
            for (ObjectPropertyBindHelper b : binders)
                b.unbindBi(oldVal);
        if (newVal != null)
            for (ObjectPropertyBindHelper b : binders)
                b.bindBi(newVal);
    }
}

public abstract class ObjectPropertyBindHelper<T> {
    private Property boundProperty;

    public ObjectPropertyBindHelper(Property boundProperty) {
        this.boundProperty = boundProperty;
    }
    public void bindBi(T o) {
        boundProperty.bindBidirectional(objectProperty(o));
    }
    public void unbindBi(T o) {
        boundProperty.unbindBidirectional(objectProperty(o));
    }
    public abstract Property objectProperty(T t);
    public Property getBoundProperty() {
        return boundProperty;
    }
}

As scottb pointed out in his answer, binding link this is not always what you want anyway. If you want to be able to cancel/commit changes, you could implement that using something like this as well (but it probably will be even harder to read the resulting code).

Backbreaking answered 30/5, 2013 at 8:53 Comment(0)
F
3

Personally, I do not find the code you have written in your event handler to be that unwieldy or kludgy. This is the sort of thing that event handlers typically do in GUI's, imo.

Ask yourself, though ... is binding really necessary in your circumstance?

If you must have real-time updates for the edits you've made in one panel to be reflected in another then you have probably implemented the easiest solution. There are difficulties inherent in this kind of UI design however and it may not be the best for all situations. What if the user needs to cancel the edits he's made? Do you have a method for rolling back the edits if he's changed his mind? Sometimes, real-time changes from editing are not desireable and in such cases binding data model objects to UI objects may not be a good idea.

Fredette answered 16/5, 2013 at 14:36 Comment(1)
Hmmm, yes, I see you point about binding not always being needed. But in that case, I'd end up with a similar situation: I'd implement it by making 2 helper methods, 1 method "selectedPersonToFields" and 1 method "fieldsToSelectedPerson". In both, you copy data from the set of fields to the set of person properties (or reversed). Once again, that's 2 times almost the same code! Somehow I would like to avoid that in both situations. I feel it could be more elegant. I'm looking for some existing JavaFX way to do this, or some pattern or something I could implement to do this...Backbreaking
B
2

It seems this is not something that can be done more elegantly in JavaFX. Binding and unbinding seems the cleanest way.

I did implement one way to do this myself. Not sure if I'll eventually end up using it (as it just replaces the code duplication with hard to read code). But it works, and it is an answer to my own question, so I've added it here.

The new PersonEditor class:

public class PersonEditor implements Initializable {
    private SelectedObjectPropertyBinder<Person> selectedObjectPropertyBinder = 
          new SelectedObjectPropertyBinder<Person>();

    @FXML private TextField nameField;
    @FXML private TextField ageField;
    @FXML private TextField heightField;

     @Override
    public void initialize(URL url, ResourceBundle rb) {
        selectedObjectPropertyBinder.getBinders().add(
            new ObjectPropertyBindHelper<Person>(nameField.textProperty()) {
                @Override public Property objectProperty(Person p) 
                { return p.nameProperty(); }
        });
        selectedObjectPropertyBinder.getBinders().add(
            new ObjectPropertyBindHelper<Person>(ageField.textProperty()) {
                @Override public Property objectProperty(Person p) 
                { return p.ageProperty(); }
        });
        selectedObjectPropertyBinder.getBinders().add(
            new ObjectPropertyBindHelper<Person>(heightField.textProperty()) {
                @Override public Property objectProperty(Person p) 
                { return p.heightProperty(); }
        });
    }

    public void setSelection(ObjectProperty<Person> selectedPersonProperty) {
        selectedObjectPropertyBinder.
            setSelectedObjectProperty(selectedPersonProperty);
    }
}

The helper classes:

public class SelectedObjectPropertyBinder<T> implements ChangeListener<T> {
    private List<ObjectPropertyBindHelper<T>> binders = 
           new ArrayList<ObjectPropertyBindHelper<T>>();

    public void setSelectedObjectProperty(Property<T> selectionProperty) {
        selectionProperty.addListener(this);
    }

    public List<ObjectPropertyBindHelper<T>> getBinders() {
        return binders;
    }

    @Override
    public void changed(ObservableValue<? extends T> observable, 
                        T oldVal, T newVal) {
        if (oldVal != null)
            for (ObjectPropertyBindHelper b : binders)
                b.unbindBi(oldVal);
        if (newVal != null)
            for (ObjectPropertyBindHelper b : binders)
                b.bindBi(newVal);
    }
}

public abstract class ObjectPropertyBindHelper<T> {
    private Property boundProperty;

    public ObjectPropertyBindHelper(Property boundProperty) {
        this.boundProperty = boundProperty;
    }
    public void bindBi(T o) {
        boundProperty.bindBidirectional(objectProperty(o));
    }
    public void unbindBi(T o) {
        boundProperty.unbindBidirectional(objectProperty(o));
    }
    public abstract Property objectProperty(T t);
    public Property getBoundProperty() {
        return boundProperty;
    }
}

As scottb pointed out in his answer, binding link this is not always what you want anyway. If you want to be able to cancel/commit changes, you could implement that using something like this as well (but it probably will be even harder to read the resulting code).

Backbreaking answered 30/5, 2013 at 8:53 Comment(0)
A
0

The question is asked long time ago :) anyway, recently I was looking for solution and implemented this way - the singleton Form class implements abstract class:

    public abstract class InputForm {

    private ArrayList<BindingPair> bindedProps;
    private ArrayList<ListenerPair> listenerPairs;

    public InputForm() {
        bindedProps = new ArrayList();
        listenerPairs = new ArrayList();
    }

    public void resetBindings() {
        unbindAll();
        bindedProps = new ArrayList();
    }

    public void resetListeners() {
        removeAllListeners();
        listenerPairs = new ArrayList();
    }

    private void unbindAll() {
        for (BindingPair pair : bindedProps) {
            unbind(pair);
        }
    }

    private void removeAllListeners() {
        for (ListenerPair listenerPair : listenerPairs) {
            removeListener(listenerPair);
        }
    }

    public void bind(BindingPair bindingPair) {
        BindingUtils.bind(bindingPair);
        bindedProps.add(bindingPair);
    }

    public void addListener(ListenerPair listenerPair) {
        ListenerUtils.addListener(listenerPair);
        listenerPairs.add(listenerPair);
    }

    private void unbind(BindingPair bindingPair) {
        BindingUtils.unbind(bindingPair);
    }

    private void removeListener(ListenerPair listenerPair) {
        ListenerUtils.removeListener(listenerPair);
    }
}

public class BindingUtils {

    public static void bind(BindingPair bindingPair) {
        if (bindingPair.isBidirectionalBinding()) {
            if (bindingPair.getStringConverter() != null)
                Bindings.bindBidirectional(bindingPair.propertyToBindProperty(), bindingPair.propertyBindToProperty(), bindingPair.getStringConverter());
            else
                bindingPair.propertyToBindProperty().bindBidirectional(bindingPair.propertyBindToProperty());
        } else {
            bindingPair.propertyToBindProperty().bind(bindingPair.propertyBindToProperty());
        }
    }

    public static void unbind(BindingPair bindingPair) {
        if (bindingPair.isBidirectionalBinding()) {
            bindingPair.propertyToBindProperty().unbindBidirectional(bindingPair.propertyBindToProperty());
        } else {
            bindingPair.propertyToBindProperty().unbind();
        }

    }
}

public class ListenerUtils {

    public static void addListener(ListenerPair listenerPair) {
        if (listenerPair.propertyProperty() != null)
            listenerPair.propertyProperty().addListener(listenerPair.getListener());
        if (listenerPair.roPropertyProperty() != null)
            listenerPair.roPropertyProperty().addListener(listenerPair.getListener());
    }

    public static void removeListener(ListenerPair listenerPair) {
        if (listenerPair.propertyProperty() != null)
            listenerPair.propertyProperty().removeListener(listenerPair.getListener());
        if (listenerPair.roPropertyProperty() != null)
            listenerPair.roPropertyProperty().removeListener(listenerPair.getListener());
    }
}


public class BindingPair {

    private Property propertyToBind;
    private Property propertyBindTo;

    private boolean bidirectionalBinding;
    private StringConverter stringConverter;

    public BindingPair(Property propertyToBind, Property propertyBindTo, boolean bidirectionalBinding, StringConverter stringConverter) {
        this.propertyToBind = propertyToBind;
        this.propertyBindTo = propertyBindTo;
        this.bidirectionalBinding = bidirectionalBinding;
        this.stringConverter = stringConverter;
    }

    public Property propertyToBindProperty() {
        return propertyToBind;
    }

    public Property propertyBindToProperty() {
        return propertyBindTo;
    }

    public boolean isBidirectionalBinding() {
        return bidirectionalBinding;
    }

    public StringConverter getStringConverter() {
        return stringConverter;
    }
}

public class ListenerPair {

    private Property property = null;
    private ChangeListener listener;
    private ReadOnlyObjectProperty roProperty = null;

    public ListenerPair(Property property, ChangeListener listener) {
        this.property = property;
        this.listener = listener;
    }

    public ListenerPair(ChangeListener listener, ReadOnlyObjectProperty roProperty) {
        this.listener = listener;
        this.roProperty = roProperty;
    }

    public Property propertyProperty() {
        return property;
    }

    public ChangeListener getListener() {
        return listener;
    }

    public Object getRoProperty() {
        return roProperty.get();
    }

    public ReadOnlyObjectProperty roPropertyProperty() {
        return roProperty;
    }
}

In Form class (implements InputForm) there are two methods which are called every time new object is "rendered" through this form:

private void doBinding() {

        resetBindings();

        bind(new BindingPair(dataXlsFileUrlProp, getControllerMain().getSceneRenderer().getLoadedSceneInfo().xlsDataFileProperty(), false, null));

    }

    private void addListeners() {

        resetListeners();

        addListener(new ListenerPair(dataXlsFileUrlProp, (observable, oldValue, newValue) -> {
            //do smth
        }));
    }

Hope somebody will help this.

Abundance answered 14/2, 2021 at 20:47 Comment(2)
This does not provide an answer to the question. Once you have sufficient reputation you will be able to comment on any post; instead, provide answers that don't require clarification from the asker.Emanuele
@Tyler2P: Why do you say that? This doesn’t look like a comment, nor does it ask for clarification.Diarrhea

© 2022 - 2024 — McMap. All rights reserved.