How to implement Checkbox / Radio cell depending on the data type in JavaFX

561 views Asked by At

I'm writing a simple JavaFX application which consists of 2 parts. - Left SplitPane is a ListView (a list of question) - When click on a question, the right SplitPane will be populated with a TableView (a list of answers which has 2 columns: type and name Depend on the question type, the type column will be checkboxes or radioboxes.

Example:

  • Question 1 - type checkbox -- [] Answer 1 -- [] Answer 2 -- [] Answer 3

  • Question 2 - type radio -- O Answer 4 -- O Answer 5

When I check answer 1 & 2 from question 1 then navigate to question 2 and left come back, only the Answer 2 is checked.

I've already debugged my code and there is no problem with my data list

My problem is when I tick more than 1 checkboxes from question 1 and then navigate to question 2 which has radioboxes then comeback, only the last checkbox will be checked.

I found out 1 problem is the ToogleGroup which used in the question 2. When I remove it, the problem is gone but now I can tick both Answer 4 & 5 which is wrong.

Here is my code:

public class SampleController implements Initializable {

@FXML
private ListView<Question> list;

@FXML
private TableView<Answer> tbDetails;

@FXML
private TableColumn tcAction;

@FXML
private TableColumn<Answer, String> tcName;

private List<Question> questions;

@Override
public void initialize(URL location, ResourceBundle resources) {
    questions = new ArrayList<Question>();

}

public void afterInit() {
    ObservableList<Question> data = FXCollections.observableArrayList();

    // Question 1
    Question question = new Question();
    question.setIdx(0);
    question.setName("Question 1");
    question.setType(0);

    List<Answer> answers = new ArrayList<Answer>();
    answers.add(new Answer("Answer 1", false));
    answers.add(new Answer("Answer 2", false));
    answers.add(new Answer("Answer 3", false));
    answers.add(new Answer("Answer 4", false));
    answers.add(new Answer("Answer 5", false));

    question.setAnswers(answers);

    questions.add(question);

    // Question 2
    question = new Question();
    question.setIdx(1);
    question.setName("Question 2");
    question.setType(1);

    answers = new ArrayList<Answer>();
    answers.add(new Answer("Answer 6", false));
    answers.add(new Answer("Answer 7", false));

    question.setAnswers(answers);

    questions.add(question);

    data.addAll(questions);

    if (list != null) {
        list.setItems(data);
        list.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
        list.setCellFactory(new Callback<ListView<Question>, ListCell<Question>>() {

            @Override
            public ListCell<Question> call(ListView<Question> param) {
                final ListCell<Question> cell = new ListCell<Question>() {
                    @Override
                    public void updateItem(Question item, boolean empty) {
                        super.updateItem(item, empty);
                        if (item != null) {
                            setText(item.getName());
                        }
                    }
                };
                return cell;
            }
        });
        list.getSelectionModel().selectedItemProperty().addListener(listItemSelected);
    }

    tcName.setCellValueFactory(new PropertyValueFactory<Answer, String>("name"));
    tcAction.setCellValueFactory(new PropertyValueFactory("correct"));
}

/**
 * Listen to changes in the list selection, and updates the table widget and
 * DeleteIssue and NewIssue buttons accordingly.
 */
private final ChangeListener<Question> listItemSelected = new ChangeListener<Question>() {

    @Override
    public void changed(ObservableValue<? extends Question> observable, Question oldValue, Question newValue) {
        questionUnselected(oldValue);
        questionSelected(newValue);
    }

};

/**
 * Called when a question is unselected.
 *
 * @param oldQuestion Old question
 */
private void questionUnselected(Question oldQuestion) {
    if (oldQuestion != null) {
        tbDetails.getSelectionModel().clearSelection();

        int questionListIdx = oldQuestion.getIdx();
        Question question = questions.get(questionListIdx);
        question.setAnswers(tbDetails.getItems());
    }
}

/**
 * Called when a question is selected.
 *
 * @param newQuestion New question
 */
private void questionSelected(Question newQuestion) {
    if (newQuestion != null) {
        final ToggleGroup radioGrp = new ToggleGroup();
        tcAction.setCellFactory(new Callback<TableColumn, TableCell>() {

            @Override
            public TableCell call(TableColumn p) {
                if (newQuestion.getType() == 1) {
                    return new RadioCell(radioGrp);
                }
                return new CheckboxCell();
            }

        });

        ObservableList<Answer> data = FXCollections.observableArrayList();
        int questionListIdx = newQuestion.getIdx();
        Question question = questions.get(questionListIdx);

        data.addAll(question.getAnswers());
        tbDetails.setItems(data);
    }
}

public class RadioCell extends TableCell<Answer, Boolean> {

private RadioButton radioBtn;

public RadioCell(ToggleGroup group) {
    this.radioBtn = new RadioButton();
    this.radioBtn.setAlignment(Pos.CENTER);
    this.radioBtn.setToggleGroup(group);

    setAlignment(Pos.CENTER);
    setGraphic(radioBtn);
}

public RadioCell() {
    this.radioBtn = new RadioButton();
    this.radioBtn.setAlignment(Pos.CENTER);
    //this.radioBtn.setToggleGroup(group);

    setAlignment(Pos.CENTER);
    setGraphic(radioBtn);
}

@Override
protected void updateItem(Boolean item, boolean empty) {
    super.updateItem(item, empty);
    if (empty) {
        setText(null);
        setGraphic(null);
    } else {
        paintCell();
    }
}

private void paintCell() {

    if (radioBtn == null) {
        radioBtn = new RadioButton();
    }

    radioBtn.selectedProperty().addListener(new ChangeListener<Boolean>() {

        @Override
        public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
            setItem(newValue);

            if (getTableRow().getIndex() >= 0) {
                ((Answer) getTableView().getItems().get(getTableRow().getIndex())).setCorrect(newValue);
            }
        }

    });

    radioBtn.setSelected(getValue());

    setText(null);
    setGraphic(radioBtn);
}

private Boolean getValue() {
    return getItem() == null ? false : getItem();
}

public class CheckboxCell extends TableCell<Answer, Boolean> {

private CheckBox checkbox;

public CheckboxCell() {
    this.checkbox = new CheckBox();
    this.checkbox.setAlignment(Pos.CENTER);

    setAlignment(Pos.CENTER);
    setGraphic(checkbox);
}

@Override
protected void updateItem(Boolean item, boolean empty) {
    super.updateItem(item, empty);
    if (empty) {
        setText(null);
        setGraphic(null);
    } else {
        paintCell();
    }
}

private void paintCell() {
    if (checkbox == null) {
        checkbox = new CheckBox();
    }

    checkbox.selectedProperty().addListener(new ChangeListener<Boolean>() {

        @Override
        public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
            setItem(newValue);

            if (getTableRow().getIndex() >= 0) {
                ((Answer) getTableView().getItems().get(getTableRow().getIndex())).setCorrect(newValue);
            }
        }

    });

    checkbox.setSelected(getValue());
    setText(null);
    setGraphic(checkbox);
}

private Boolean getValue() {
    return getItem() == null ? false : getItem();
}

Thanks in advance

2

There are 2 answers

3
Jens-Peter Haack On BEST ANSWER

Using your cell implementation with a RadioButton will not work as expected due to:

The cells are not 1:1 related to items in the table, they are only view-ports. In the following view:

enter image description here

The list of Answer items has 5 members, but the view only has 3 cells to view them. So your RadioButtons will NOT be 1:1 related to Answer items. Anyhow using any state, like the once allocated RadioButton in your cell will not work due to the view-port function of a cell, the view will assign different Answer items dynamicall to the same cell. So you might:

a) implement it in the model

I recommend to implement a toggle function in your model (Answer and Question), in a way that Answers instances will ask their parent Question instances to clear 'correct' properties of other Answers if they are of type RADIOBUTTON (1). (Question akt as ToggleGroup, Answers akt like Toggles)

b) implement it in the view

Add an onAction listener to the Checkbox and trigger a 'clear' of all other correct values with the same Question parent if of type (1)...

private void questionSelected(Question newQuestion) {
    if (newQuestion != null) {
        currentQuestionIsToggle = newQuestion.getType() == 1;

        ObservableList<Answer> data = FXCollections.observableArrayList();
        data.addAll(newQuestion.getAnswers());
        tbDetails.setItems(data);
    }
}

    ...
    // Only once for the tbDetails table:
    tcAction.setCellValueFactory(p -> p.getValue().correctProperty());
    tcAction.setCellFactory(p -> {
        CheckBoxTableCell box = new CheckBoxTableCell(){
            @Override public void updateItem(Object on, boolean empty) {
                super.updateItem(on, empty);
                if (on != null) {
                    Answer answer = tbDetails.getItems().get(getIndex());
                    answer.correctProperty().addListener((a) -> {
                        if (answer.getCorrect() && currentQuestionIsToggle) {
                            for (Answer other : tbDetails.getItems()) {
                                if (other != answer) other.setCorrect(false);
                            }
                        }
                    });
                }
            }
        };
        return box;
    });
1
Jens-Peter Haack On

With implementing it in the model I mean that your classes implementing your model of the problem (Question and Answer) might be modified to ensure the radio mode if the question is of that kind:

enum QuestionType { Multiple, Single };
public static class Question {
    IntegerProperty myIdx = new SimpleIntegerProperty();
    StringProperty myName = new SimpleStringProperty();
    QuestionType myType = QuestionType.Multiple;

    ObservableList<Answer> myAnswers = FXCollections.observableArrayList();

    public Question() {
        myAnswers.addListener((ListChangeListener.Change<? extends Answer> l) -> {myAnswers.forEach(a -> a.setQuestion(this));});
    }

    public void applyCorrectPolicy(Answer on) {
        switch (myType) {
            case Single:
                if (on.getCorrect()) {
                    myAnswers.forEach(a -> {if (a != on) a.setCorrect(false);});
                }
                break;
            default: break;
        }
    }

    public void setIdx(int idx) { myIdx.set(idx); }
    public void setType(QuestionType type) { myType = type;}
    public void setName(String name) { myName.set(name); }
    public void setAnswers(Collection<Answer> list) { myAnswers.clear(); myAnswers.addAll(list); }

    public int getIdx() { return myIdx.get(); }
    public QuestionType getType() { return myType;}
    public String getName() { return myName.get(); }
    public Collection<Answer> getAnswers() { return myAnswers; }
}

public static class Answer {
    Question       myQuestion;
    StringProperty myName = new SimpleStringProperty();
    BooleanProperty myCorrect = new SimpleBooleanProperty();

    public Answer(String name, boolean check) {
        myName.set(name);
        myCorrect.set(check);
        myCorrect.addListener(l -> {
            if (myQuestion != null) myQuestion.applyCorrectPolicy(this);
        });
    }

    public QuestionType getType() { return myQuestion.getType(); }

    public void setQuestion(Question question) { myQuestion = question; }

    public BooleanProperty correctProperty() { return myCorrect; }
    public StringProperty  nameProperty()    { return myName; }

    public void setCorrect(boolean correct) { myCorrect.set(correct); }
    public boolean getCorrect() { return myCorrect.get(); }
}

Now your view does not require to ensure the RadioButtons / CheckButton depending on the question type. In this case I would implement that behaviour in the model.