简体   繁体   中英

Setting displayed value of ChoiceBox when calling setValue()

I want to store objects in a ChoiceBox to mirror a lookup table with (id, name) fields in order to be able to easily retrieve the id when later saving the record while the choice displays the text. For example, if I have a ChoiceBox with Category objects, when the user selects a category in the the dropdown, the text will display the name of that Category -- the display and selection will match. Eventually, when the model saves the data, it will pull out the id field of the Category object as a parameter in the SQL query.

My code at the bottom of this post shows my attempts to understand what is going on.

The challenge is that the text portion of the Choice is not necessarily updated when the value changes, either with an explicit setValue() call or a an indirect one when I bind the Choice to an ObjectProperty.

I believe I understand why. If I call setValue() with a Category object identical to one in the ObservableList of the Choice (the same object, not another instance with the same field values), the text portion displays the correct value. However, if I call setValue() with another instance, it doesn't work, even if the field values are the same. I did an Override on the equals() of the class I'm using, but that didn't help. Is there another method ChoiceBox uses to compare objects?

Do you have any suggestions to work around this issue? I want to avoid coding other listeners, or search functions every time I implement this use case.

Maybe a simple way would be to use the id from the database to get one of the objects in the pick list used to build the combo and set that as the value of my model's property. For instance, if the id is 1, my model's category field would be set to = picklist[1] (a Category object). Since the choice's ObservableList would be initialized with the same picklist, the objects would be identical and the problem would be solved. (As in the third scenario in my code.)

/*
 * If I set the value of a ChoiceBox to exact copy of an object
 * in the ObservableList, the text portion is updated accordingly
 *
 * If is set the value with another instance of the same class
 * with the same values, but not the same exact object,
 * the text portion is not updated. 
 * 
 */
package javafxapplication1;

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ChoiceBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class JavaFXApplication1 extends Application {

    class TestClass {
        // public to simplify demo - normally private

        public int id;
        public String name;

        public TestClass(int id, String name) {
            this.id = id;
            this.name = name;
        }

        public boolean equals(TestClass other) {
            // not called ...
            System.out.println("TestClass.equals() was called");
            return this.id == other.id && this.name.equals(other.name); 
        }

        @Override
        public String toString() {
            return "[id = " + id + "  name = " + name + "]";
        }
    }

    @Override
    public void start(Stage primaryStage) {

        ChoiceBox<String> cbS = new ChoiceBox<>();
        ObservableList<String> valuesS = FXCollections.observableArrayList();
        valuesS.addAll("A", "B", "C");
        cbS.setItems(valuesS);

        // a text converter would make the choice display only text
        // no the toString() of the class

        /*
        * SET INITIAL VALUE BEFORE SHOWING VIEW 
        * no issues with String choice
         */
        cbS.setValue("A");
        System.out.println("Initial String value: " + cbS.getSelectionModel().getSelectedItem());

        Button btn = new Button();
        btn.setText("Change String Value");
        btn.setOnAction((ActionEvent event) -> {
            cbS.setValue("C");
            System.out.println("setValue(\"C\") was called");
            System.out.println("New String value is " + cbS.getSelectionModel().getSelectedItem());
        });

        ChoiceBox<TestClass> cbO = new ChoiceBox<>();
        ObservableList<TestClass> valuesO = FXCollections.observableArrayList();
        TestClass specificInstance = new TestClass(3, "Exact instance");
        valuesO.addAll(new TestClass(1, "Aba"), new TestClass(2, "Ubu"), specificInstance);
        cbO.setItems(valuesO);
        cbO.setValue(new TestClass(1, "Aba"));
        System.out.println("Initial Object value: " + cbO.getSelectionModel().getSelectedItem());


        Button btnO = new Button();
        btnO.setText("Change Object Value");
        /*
        * SET VALUE TO OTHER INSTANCE WITH SAME FIELD VALUES
        */
        btnO.setOnAction((ActionEvent event) -> {
            cbO.setValue(new TestClass(2, "Ubu"));
            System.out.println("setValue(same-values-object)");
            System.out.println("New Object value is " + cbO.getSelectionModel().getSelectedItem());
        });

        /*
        * SET VALUE TO ONE OF THE OBJECTS IN THE CHOICE
        */
        Button btnO2 = new Button();
        btnO2.setText("Change Object Value");
        btnO2.setOnAction((ActionEvent event) -> {
            cbO.setValue(specificInstance);
            System.out.println("setValue(exact-instance-of-object) was called");
            System.out.println("New Object value is " + cbO.getSelectionModel().getSelectedItem());
        });

        VBox root = new VBox();
        root.getChildren().addAll(cbS, btn, cbO, btnO, btnO2);

        Scene scene = new Scene(root, 300, 250);

        primaryStage.setTitle("Test setValue()");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        launch(args);
    }

}

The problem is simply that you haven't overridden the equals(...) method, which has a signature

public boolean equals(Object other) 

By defining it with a different signature

public boolean equals(TestClass other)

you have overloaded it. (It's good practice to always use the @Override annotation when you intend to override a method. The compiler will generate an error if you are not actually overriding the method, so you can see the issue immediately.) The machinery that checks whether the value you pass to setValue(...) is in the backing list (probably via a call to List.contains(...) ) will use the equals(...) method defined in Object if you haven't overridden it.

If you actually override equals(...) :

@Override
public boolean equals(Object other) {
    System.out.println("TestClass.equals() was called");

    if (this == other) return true ;

    // cue endless arguments about whether to use instanceof or getClass()...
    if (! (other instanceof TestClass)) return false ;

    TestClass tc = (TestClass) other ;
    return this.id == tc.id && Objects.equals(this.name, tc.name); 
}

then this will work as you want it to.

Note also that you should always override hashCode() when you override equals(...) :

@Override
public int hashCode() {
    return Objects.hash(id, name);
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM