sialcasa / mvvmFX

an Application Framework for implementing the MVVM Pattern with JavaFX
Apache License 2.0
489 stars 105 forks source link

Refresh a ComboBox #598

Closed svedie closed 5 years ago

svedie commented 5 years ago

Hello Team,

I have an view (Article) with an ComboBox which items can be created by the user from an another view(Category). The initialisation works fine and according to the mvvmFX framework rules.

Now for example if the user wants to create a new article and misses some category, he can create a new category from a different view(Category). The "update" event (@Observes UpdateEvent event) works fine and the ObservableList is updated with the new values. The problem is that the ComboBox in the Article view contains still the old items and does not refresh the items.

I have tried to bindBidirectional the itemsProperty and the valueProperty, but both do not change the items in the ComboBox.

Do I miss something or is this not possible by the framework or JavaFX?

Greetings, Sven

manuel-mauky commented 5 years ago

Hi,

with ComboBox (and the same is true for ListView and some other controls) you don't bidirectionally bind the items property but instead you should use setItems. The reason is that the ComboBox doesn't have any way of adding items from within the control itself. There is not data-flow from the comboBox to the outside but only from outside into the combobox.

I've created a small example to show this:

public class BlahViewModel implements ViewModel {

    private ObservableList<String> items = FXCollections.observableArrayList();
    private StringProperty newCategory = new SimpleStringProperty("");

    public void addCategory() {
        if(newCategory.get() != null && !newCategory.get().trim().isEmpty()) {
            items.add(newCategory.get());
            newCategory.setValue("");
        }
    }

    public StringProperty newCategoryProperty() {
        return newCategory;
    }

    public ObservableList<String> itemsProperty() {
        return items;
    }
}
public class BlahView implements FxmlView<BlahViewModel> {

    @FXML
    private TextField newCategory;
    @FXML
    private ComboBox<String> combobox;

    @InjectViewModel
    private BlahViewModel viewModel;

    public void initialize() {
        combobox.setItems(viewModel.itemsProperty());
        newCategory.textProperty().bindBidirectional(viewModel.newCategoryProperty());
    }

    public void onNewCategory() {
        viewModel.addCategory();
    }
}

In the initialize method you can see that I'm using setItems here. However, for the valueProperty (not shown in this example) you have to use bindDirectional because here the data flows in both directions.

svedie commented 5 years ago

Hi Lestard,

I use two different ViewModels for articles and categories. So, if create an category and fire an udpate event in the Category ViewModel, it will be triggered by the Article ViewModel, but it uses an new or empty Article ViewModel.

If I use the @Singleton annotation, then it works. I see the updated combobox, but I can only open one article(category) tab at a time.

Do you know maybe, how can I access the right Article ViewModel?

Here is the code, how I the views and viewmodels are implemented (shortened):

public class KategorieView implements FxmlView<KategorieViewModel> {

  @FXML
  private TextField name;
  @FXML
  private ComboBox<Kategorie> hauptkategorie;

  @InjectViewModel
  private KategorieViewModel viewModel;

  private ValidationVisualizer validationVisualizer = new ControlsFxVisualizer();

  public void initialize() {
    hauptkategorie.setItems(viewModel.getTypes());
    hauptkategorie.valueProperty().bindBidirectional(viewModel.hauptkategorieProperty());
    name.textProperty().bindBidirectional(viewModel.nameProperty());

    validationVisualizer.initVisualization(viewModel.getNameValidator(), name, true);
  }
}
@ScopeProvider({ToolbarDatasetScope.class})
public class KategorieViewModel extends AbstractViewModel<Kategorie> implements ViewModel {

  private ModelWrapper<Kategorie> kategorieWrapper = new ModelWrapper<>();
  private CompositeValidator validator = new CompositeValidator();
  private Validator nameValidator = new EmptyValueValidator(nameProperty());

  @Inject
  private KategorieService service;

  @InjectScope
  private KategorieScope scope;

  public KategorieViewModel() {
    validator.addValidators(nameValidator);
  }

  @Override
  public void initialize() {
    if (scope.getKategorieProperty().get() == null) {
      scope.setKategorieProperty(new Kategorie());
    }

    toolbarScope.validRecordProperty().bind(validator.getValidationStatus().validProperty());
    toolbarScope.subscribe(ToolbarConstants.SAVE, (s, objects) -> save());
    toolbarScope.subscribe(ToolbarConstants.DEACTIVATE, (s, objects) -> deactivate());
    toolbarScope.subscribe(ToolbarConstants.DELETE, (s, objects) -> delete());
  }

  @Override
  public void updateForm(Kategorie kategorie) {
  }

  @Override
  public void save() {
  }

  @Override
  public void deactivate() {
  }

  @Override
  public void delete() {
  }

  ObservableList<Kategorie> getTypes() {
  }

  ObjectProperty<Kategorie> hauptkategorieProperty() {
    return kategorieWrapper
        .field("hauptkategorie", Kategorie::getHauptkategorie, Kategorie::setHauptkategorie);
  }

  StringProperty nameProperty() {
    return kategorieWrapper.field("name", Kategorie::getName, Kategorie::setName);
  }

  ValidationStatus getNameValidator() {
    return nameValidator.getValidationStatus();
  }
}
public class ArtikelView implements FxmlView<ArtikelViewModel> {

  @FXML
  private ComboBox<Kategorie> kategorie;
  @FXML
  private TextField name;

  @InjectViewModel
  private ArtikelViewModel viewModel;

  @Inject
  private Stage stage;

  private ValidationVisualizer validationVisualizer = new ControlsFxVisualizer();

  public void initialize() {
    kategorie.setItems(viewModel.getKategorien());
    kategorie.valueProperty().bindBidirectional(viewModel.kategorieProperty());
    name.textProperty().bindBidirectional(viewModel.nameProperty());

    validationVisualizer.initVisualization(viewModel.getNameValidator(), name, true);
  }
}
@ScopeProvider({ToolbarDatasetScope.class})
public class ArtikelViewModel extends AbstractViewModel<Artikel> implements ViewModel {

  private ModelWrapper<Artikel> artikelWrapper = new ModelWrapper<>();
  private CompositeValidator validator = new CompositeValidator();
  private ObservableList<Kategorie> kategorien = FXCollections.observableArrayList();
  private Validator nameValidator = new EmptyValueValidator(nameProperty());

  @InjectScope
  private ArtikelScope scope;

  @Inject
  private ArtikelService artikelService;

  @Inject
  private KategorieService kategorieService;

  public ArtikelViewModel() {
    validator.addValidators(nameValidator);
  }

  @Override
  public void initialize() {
    updateKategorien();
  }

  @Override
  public void updateForm(Artikel article) {
  }

  @Override
  public void save() {
  }

  @Override
  public void deactivate() {
  }

  @Override
  public void delete() {
  }

  StringProperty nameProperty() {
    return artikelWrapper.field("name", Artikel::getName, Artikel::setName);
  }

  ObjectProperty<Kategorie> kategorieProperty() {
    return artikelWrapper.field("kategorie", Artikel::getKategorie, Artikel::setKategorie);
  }

  ObservableList<Kategorie> getKategorien() {
    return kategorien;
  }

  public void onUpdateKategorieEvent(@Observes KategorieUpdateEvent event) {
    updateKategorien();
  }

  private void updateKategorien() {
    kategorien.clear();
    List<Kategorie> hauptkategorien = kategorieService.findAllHauptkategorien();
    List<Kategorie> sortierteKategorien = new ArrayList<>();
    hauptkategorien
        .forEach(kategorie-> CommonUtils.createKategorieGraph(kategorie, sortierteKategorien));
    kategorien.addAll(sortierteKategorien);
  }
svedie commented 5 years ago

Ok, found two solutions: You can use @Singleton on the ModelView or find the JavaFX element via stage.getScene().lookup() in the ViewModel.

manuel-mauky commented 5 years ago

Cool to see that you've found a solution. Indeed, it can be tricky to handle multiple viewmodels of the same kind. Another possible solution might be to create unique IDs in your ViewModels and to use them as correlator. When you send the event you also send the ID so that the receiver of the event can know which viewModel was sending the event. I'm not sure how this would work in your specific use-case but I've used this technique in similar situations in the past.

svedie commented 5 years ago

Thank you for your idea.