FXML Patterns / best practices
Daniel Zwolenski
zonski at googlemail.com
Thu Feb 23 00:05:09 PST 2012
I'm spawning off a new thread for this, so Greg's discussion on the
Controller base class can continue uninterrupted.
I just wrote a huge email outlining all the pros/cons of clean MVP and
how/where I think FXML is causing me to compromise, etc. It was too long
and too complicated, so back to basics. Below I define a very pure MVP
implementation (in psuedo code) of a Person Component using pure Java. I'll
avoid defining interfaces at this point for ease, but ideally I would do
this as well (so I can use mock objects when testing). I've used a
standard, common, popular pattern that I believe a lot of people would like
to use if they could.
What I'd like to know is how people intend to implement something like the
below using FXML. I'm looking for either the closest approximation of the
below using FXML, or if you think you have a better pattern, then how you
would implement this. Based on that I'd then like to look at what
advantages/disadvantages your proposed design has over the 'pure' MVP
approach below (or tell me what you'd do differently if you don't think the
below is pure MVP!).
The key benefits that I see from the below architecture are:
1. *Seperation of concerns*: the relevant implementation details are very
well contained within each of the correct elements, view in View,
presentation logic in Presenter, model stuff in Model.
2. *Unit Testability*: the well-defined units are easily decoupled from
each other and can be tested in isolation allowing for clean unit tests
3. *Reusability*: each element of the MVP bundle is reusable, so you could
attach a different View (e.g. a mobile specific one) to the Presenter, etc
Can we retain these benefits in an FXML-based architecture, or do we have
to give them up?
*Model*
class PersonModel {
private LongProperty personId;
private TextProperty firstName;
private TextProperty lastName;
private ObjectProperty<Gender> gender;
... constructor, getters, setters ...
}
*View*
class PersonView extends VBox {
@Inject private PersonModel model;
@Inject private PersonPresenter presenter;
private TextField firstNameField;
private TextField lastNameField;
private ChoiceBox genderField;
private Button deleteButton;
private Button saveButton;
@PostConstruct void init() {
firstNameField = new TextField();
firstNameField.textProperty().bindBidirectional(model.firstNameProperty());
getChildren().add(firstNameField);
lastNameField = new TextField();
lastNameField.textProperty().bindBidirectional(model.lastNameProperty());
getChildren().add(lastNameField);
genderField= new ChoiceBox(Gender.values());
genderField.selectedValueProperty().bindBidirectional(model.genderField());
getChildren().add(genderIcon);
deleteButton= new Button("Delete");
deleteButton.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent event) {
presenter.delete();
}
});
getChildren().add(saveButton);
saveButton= new Button("Save");
saveButton.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent event) {
presenter.save();
}
});
getChildren().add(saveButton);
}
}
*Presenter*
class PersonPresenter {
@Inject private PersonModel model;
@Inject private PersonView view;
@Inject private PersonService personService; // e.g. an RMI-like
handler onto a server
// this is the external method called by whatever wants to show a person
void showPerson(final long personId) {
new Thread(new Task<PersonDTO>() {
PersonDTO call() {
return personService.getPersonDetails(personId);
}
void onSuccess(PersonDTO details) {
model.setPersonId(details.getPersonId());
model.setFirstName(details.getFirstName());
model.setLastName(details.getLastName());
model.setGender(details.getGender());
}
}).run();
}
void delete() {
final long personId = model.getPersonId();
new Thread(new Task<Void>() {
void call() {
personService.deletePerson(personId);
return null;
}
void onSuccess(Void result) {
// probably navigate away from the current view
}
}).run();
}
void save() {
final PersonDTO details = new PersonDTO(
model.getPersonId(),
model.getFirstName(),
model.getLastName()
model.getGender()
);
new Thread(new Task<PersonDTO>() {
void call() {
return personService.updatePerson(details);
}
void onSuccess(Void result) {
// probably show a nice confirmation message
}
}).run();
}
}
The above assumes the existence of some sort of factory method that wires
everything up. This would ideally be Spring or Guice but could just as
easily be done by hand like so:
public PersonPresenter createPersonPresenter() {
PersonModel model = new PersonModel();
PersonView view = new PersonView();
PersonPresenter presenter = new PersonPresenter();
view.setModel(model);
view.setPresenter(presenter);
presenter.setModel(model);
presenter.setView(view);
presenter.setPersonService(getPersonService());
return presenter;
}
More information about the openjfx-dev
mailing list