When implementing user interfaces via MVP or MVC, you will find following classes useful in addition to model, view and presenter (controller):
- Action - an action can have 0-n visual representations,
being button, main menu item, context menu item or just keyboard shortcut.
It is the "model" behind these "views",
holding the callback, label, tooltip, icon, accelerator key.
Disabling the action would disable all its visual representations.
Actions are part of the presenter, their visual representations are part of the view. - Command (Edit) - actions can use
commands
to track the changes a user performs, feeding an
undo-manager
to make edit-sessions undo/redo-able step by step.
A command records the concrete execution of an action,
that means it knows the state of the data before and after,
mostly represented by the execution's parameter values.
Edits (commands) can be part of a two-way data-binding (read/write), or the presenter. - Selection - describing selected list-, table- or tree-items
which are part of the view, but referring to model-items.
Typically this is standard parameter for many commands, e.g. you select some rows and then delete them.
Selections should also be usable for selecting items in a view.
Selections come from the view, but must then refer to the model by means of the presenter.
This Blog is about Commands. In context of UI-programming, Edit is a better name for them, because there is also a command-pattern that refers to client-server communication. I will show a Vaadin application that lets edit a person-form, featuring "Undo" and "Redo" buttons. Please refer to my passed Vaadin Blog for how to get and set-up a Vaadin application that lets launch different examples.
Example Application
Here is a screenshot of the Demo
application.
The "Undo"- and "Redo"-buttons will allow to roll UI changes back and forward. Initially they are both disabled, because no change has been done yet. As soon as you change one of the editable fields and it loses the input focus, the "Undo" button should become enabled. As soon as you press "Undo" once, the "Redo" button should become enabled.
To add a little complexity, the "Age" field is a read-only field that always has to be in sync with the "Date of Birth" field. In other words, as soon as you change the day of birth of the person, its age will be calculated automatically.
Package Structure
The files shown here do not include the
launching Vaadin application (ExamplesUi
).
As all of model, view and presenter became more complex here,
I decided to put all of them into their own packages.
The Demo
application is in the viewimpl
package,
because it depends on the Vaadin windowing-system.
So you will need to enter
http://localhost:8080/ExamplesUi?class=vaadin.examples.mvp.commands.viewimpl.Demo
on your browser's address line to see the application.
The view interfaces that abstract the windowing-system are in the view
package.
The presenter is implemented using a read-only and a read/write data-binding in package presenter
.
Besides the Person
bean, the model
package also contains the Age
business logic,
as it is not so easy to calculate the age of a person.
This is needed by the view, implemented reusable in the model, and applied by the binding.
Support for undoable edits is in package command
.
Because I reused the
Swing UndoManager,
I needed to override it. For a clear separation of concerns I created two sub-classes of it.
Demo Application
Here is the source of the Demo
application.
As expected, it builds together the MVP by using a windowing-system specific view,
and windowing-system agnostic presenter and model.
package vaadin.examples.mvp.commands.viewimpl; import java.text.SimpleDateFormat; import com.vaadin.ui.*; import vaadin.examples.mvp.commands.model.Person; import vaadin.examples.mvp.commands.presenter.PersonPresenter; import vaadin.examples.mvp.commands.view.PersonView; public class Demo extends VerticalLayout { public Demo() { final PersonView<Component> view = new PersonViewImpl(); final PersonPresenter<Component> presenter = new PersonPresenter<Component>(view); final Person model = new Person( "John Doe", new SimpleDateFormat("yyyy-MM-dd").parse("1582-10-04")); // 5-14 do not exist! presenter.setModel(model); addComponent(view.getAddableComponent()); } }
Mind that the days from 1582-10-05 until including 1582-10-14 do not exist in Gregorian calendar! That way we can check the reliability of Vaadin date picker :-)
MVP
Model
We will walk now from model to view and finally come to presenter and commands.
The model is a normal Java bean, not having a listener mechanism, as it is common with MVP. It has just two properties, and one read-only property. For better control it outputs itself on every value-change.
package vaadin.examples.mvp.commands.model; import java.text.DateFormat; import java.util.Date; public class Person { private String name; private Date dateOfBirth; public Person() { } public Person(String name, Date dateOfBirth) { this.name = name; this.dateOfBirth = dateOfBirth; } public String getName() { return name; } public void setName(String name) { this.name = name; System.out.println(this); } public Date getDateOfBirth() { return dateOfBirth; } public void setDateOfBirth(Date dateOfBirth) { this.dateOfBirth = dateOfBirth; System.out.println(this); } public Integer getAge() { return (getDateOfBirth() != null) ? new Age().get(getDateOfBirth(), new Date()) : null; } @Override public String toString() { final DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM); return getClass().getSimpleName()+ " name="+getName()+ ", dateOfBirth="+(getDateOfBirth() != null ? dateFormat.format(getDateOfBirth()) : "null"); } }
The read-only bean property age
(no setter) is implemented by following business-logic class:
package vaadin.examples.mvp.commands.model; import java.util.*; /** * Calculates the age in years. * @see http://stackoverflow.com/questions/1116123/how-do-i-calculate-someones-age-in-java */ public class Age { public int get(Date birthDay, Date referenceDay) { if (birthDay == null) return 0; if (referenceDay == null) referenceDay = new Date(); // today final Calendar birth = Calendar.getInstance(); birth.setTime(birthDay); final Calendar today = Calendar.getInstance(); today.setTime(referenceDay); if (today.getTime().before(birth.getTime())) throw new IllegalArgumentException("Given today is before date of birth!"); final int age = today.get(Calendar.YEAR) - birth.get(Calendar.YEAR); final int todayMonth = today.get(Calendar.MONTH); final int birthMonth = birth.get(Calendar.MONTH); if (todayMonth < birthMonth || (todayMonth == birthMonth && today.get(Calendar.DAY_OF_MONTH) < birth.get(Calendar.DAY_OF_MONTH))) return age - 1; // whole year not finished return age; } }
This exists separately from Person
because not only persons have an age.
View Interfaces
Don't be confused by a read-only view that has setters. Consider the direction the presenter takes: it will set values into a read-only view, but it will not retrieve values from a read-only view. Thus it has just setters, no getters.
package vaadin.examples.mvp.commands.view; import java.util.Date; import vaadin.examples.mvp.ReadOnlyView; /** * A read-only view allows just to render values (set), not to edit them (get). * @param <W> component of the windowing-system this view refers to. */ public interface PersonReadOnlyView<W> extends ReadOnlyView<W> { void setName(String name); void setDateOfBirth(Date dateOfBirth); void setAge(Integer ageInYears); }
The editable view extends the read-only view and adds the getters. Further it listens to field-changes, and the undo- and redo-actions. Last but not least it must provide means to set the undo- and redo-actions disabled when there is nothing to undo or redo, to be called by the presenter (binding).
package vaadin.examples.mvp.commands.view; import java.util.Date; /** * A read/write view allows to render values (set) and edit them (get). * @param <W> component of the windowing-system this view refers to. */ public interface PersonView<W> extends PersonReadOnlyView<W> { interface Listener { void nameChanged(); void dateOfBirthChanged(); void undo(); void redo(); } void addListener(Listener listener); String getName(); Date getDateOfBirth(); void setUndoEnabled(boolean enable); void setRedoEnabled(boolean enable); }
These are the responsibilities the presenter will work with. It does not import any of the implementations behind these interfaces.
Vaadin View Implementations
Here come the Vaadin implementations of the view interfaces.
The PersonReadOnlyViewImpl
provides all input fields protected final
for sub-classes.
Setting the field values is straight forward, just the age-field must be kept read-only always.
package vaadin.examples.mvp.commands.viewimpl; import java.util.Date; import com.vaadin.data.util.ObjectProperty; import com.vaadin.ui.*; import vaadin.examples.mvp.commands.view.PersonReadOnlyView; public class PersonReadOnlyViewImpl implements PersonReadOnlyView<Component> { protected final TextField nameField; protected final TextField ageField; protected final DateField dateOfBirthField; protected final AbstractOrderedLayout formLayout; public PersonReadOnlyViewImpl() { formLayout = new FormLayout(); formLayout.setMargin(true); nameField = new TextField("Name"); nameField.setNullRepresentation(""); // else "null" will be displayed dateOfBirthField = new DateField("Date of Birth"); dateOfBirthField.setDateFormat("yyyy-MM-dd"); ageField = new TextField("Age"); ageField.setReadOnly(true); formLayout.addComponent(nameField); formLayout.addComponent(dateOfBirthField); formLayout.addComponent(ageField); } @Override public Component getAddableComponent() { return formLayout; } @Override public void setName(String name) { nameField.setPropertyDataSource(new ObjectProperty<String>(name, String.class)); } @Override public void setDateOfBirth(Date dateOfBirth) { dateOfBirthField.setPropertyDataSource(new ObjectProperty<Date>(dateOfBirth, Date.class)); } @Override public void setAge(Integer ageInYears) { ageField.setReadOnly(false); ageField.setValue(ageInYears != null ? "" + ageInYears : ""); ageField.setReadOnly(true); } }
The editable sub-class of this adds value-change listeners to all fields,
and action-listeners to the buttons.
It sets the fields to be unbuffered, for Vaadin this means that they will propagate
their values to any value-change listener immediately when they lose the input-focus,
i.e. the user clicks the mouse outside of the field, or presses TAB.
If they were buffered, a commit()
call would be necessary to do that.
package vaadin.examples.mvp.commands.viewimpl; import java.util.*; import com.vaadin.ui.*; import vaadin.examples.mvp.commands.view.PersonView; public class PersonViewImpl extends PersonReadOnlyViewImpl implements PersonView<Component> { private final AbstractOrderedLayout layout; private final Button undo; private final Button redo; private final List<Listener> listeners = new ArrayList<Listener>(); public PersonViewImpl() { addValueChangeListeners(); undo = new Button("Undo"); redo = new Button("Redo"); final Component toolbar = createToolbar(); layout = new VerticalLayout(); layout.addComponent(formLayout); layout.setExpandRatio(formLayout, 1f); // give the form most space layout.addComponent(toolbar); // give toolbar its natural height } @Override public Component getAddableComponent() { return layout; } @Override public void addListener(Listener editorListener) { listeners.remove(editorListener); listeners.add(editorListener); } @Override public String getName() { return nameField.getValue(); } @Override public Date getDateOfBirth() { return dateOfBirthField.getValue(); } @Override public void setUndoEnabled(boolean enable) { undo.setEnabled(enable); } @Override public void setRedoEnabled(boolean enable) { redo.setEnabled(enable); } private void addValueChangeListeners() { nameField.setBuffered(false); // deliver value on focus-lost nameField.addValueChangeListener( (event) -> { for (Listener listener : listeners) listener.nameChanged(); } ); dateOfBirthField.setBuffered(false); dateOfBirthField.addValueChangeListener( (event) -> { for (Listener listener : listeners) listener.dateOfBirthChanged(); } ); } private Component createToolbar() { undo.addClickListener( (event) -> { for (Listener listener : listeners) listener.undo(); } ); redo.addClickListener( (event) -> { for (Listener listener : listeners) listener.redo(); } ); final AbstractOrderedLayout toolbar = new HorizontalLayout(); toolbar.setSpacing(true); toolbar.setMargin(true); toolbar.addComponent(undo); toolbar.addComponent(redo); return toolbar; } }
Mind how elegant the lambda-callbacks look. By the way, you won't be able to compile that source with a Java below 1.8 !
Presenter
The most complex always is the presenter. This one has been split across several classes that all have their own responsibility. The presenter itself just allocates a binding and delegates everything to it.
In this case also the undo- and redo-callbacks are implemented by the binding, because they are so tightly connected to the value-change of the edited properties.
package vaadin.examples.mvp.commands.presenter; import vaadin.examples.mvp.commands.model.Person; import vaadin.examples.mvp.commands.view.PersonView; public class PersonPresenter<W> { private Binding<W> binding; public PersonPresenter(PersonView<W> view) { assert view != null; binding = new UndoableBinding<W>(view); view.addListener(binding); } public void setModel(Person person) { binding.setModel(person); } }
The basic binding provides setting all model values into the view,
and receiving a value-change callback per property, where the new value is pushed to the model.
In case of dateOfBirth
also the age
is calculated and pushed to the view.
package vaadin.examples.mvp.commands.presenter; import vaadin.examples.mvp.commands.model.Person; import vaadin.examples.mvp.commands.view.PersonView; abstract class Binding<W> implements PersonView.Listener { final PersonView<W> view; private Person model; public Binding(PersonView<W> view) { this.view = view; } final Person getModel() { return model; } void setModel(Person person) { this.model = person; view.setName(person.getName()); view.setDateOfBirth(person.getDateOfBirth()); } @Override public void nameChanged() { getModel().setName(view.getName()); } @Override public void dateOfBirthChanged() { getModel().setDateOfBirth(view.getDateOfBirth()); view.setAge(getModel().getAge()); } }
The undoable binding adds all UndoManager
functionality by overrides.
When receiving a value-change callback, an edit is generated and added to the undo-manager.
This leads to recursion when "Undo" is triggered by the user, because then the old value is set into the view,
and that leads to a recursive call to the listener which would again add a new edit.
Thus we need to distinguish between value-change callbacks that are triggered by the user
and such that were triggered by the undo-manager.
The undoManager.isExecutingEdit()
gives us that.
package vaadin.examples.mvp.commands.presenter; import java.util.Date; import vaadin.examples.mvp.commands.command.*; import vaadin.examples.mvp.commands.model.Person; import vaadin.examples.mvp.commands.view.PersonView; class UndoableBinding<W> extends Binding<W> { private StatefulUndoManager undoManager = new NonRecursiveUndoManager(); public UndoableBinding(PersonView<W> view) { super(view); } @Override void setModel(Person person) { super.setModel(person); undoManager.discardAllEdits(); adjustUndoRedoButtons(); } @Override public void nameChanged() { if (undoManager.isExecutingEdit() == false) undoManager.addEdit( new ValueChangeCommand<String>( getModel().getName(), view.getName(), (value) -> view.setName(value) ) ); adjustUndoRedoButtons(); super.nameChanged(); } @Override public void dateOfBirthChanged() { if (undoManager.isExecutingEdit() == false) undoManager.addEdit( new ValueChangeCommand<Date>( getModel().getDateOfBirth(), view.getDateOfBirth(), (value) -> view.setDateOfBirth(value) ) ); adjustUndoRedoButtons(); super.dateOfBirthChanged(); } @Override public void undo() { undoManager.undo(); } @Override public void redo() { undoManager.redo(); } private void adjustUndoRedoButtons() { view.setUndoEnabled(undoManager.canUndo()); view.setRedoEnabled(undoManager.canRedo()); } }
Controlling the enabled-state of the undo-buttons and forwarding their callback to the undo-manager complete this presentation logic.
Undoable Edits
As simple as all other classes are the ones in command
package.
First there is a generic edit that is used to record value-changes.
The embedded interface ValueSetter
enables us to write
the elegant lambdas that set values in UndoableBinding
.
package vaadin.examples.mvp.commands.command; import javax.swing.undo.*; public class ValueChangeCommand<T> extends AbstractUndoableEdit { public interface ValueSetter<T> { void setValue(T oldValue); } private final T oldValue, newValue; private final ValueSetter<T> valueSetter; public ValueChangeCommand(T oldValue, T newValue, ValueSetter<T> valueSetter) { assert valueSetter != null; this.oldValue = oldValue; this.newValue = newValue; this.valueSetter = valueSetter; } @Override public void undo() throws CannotUndoException { super.undo(); valueSetter.setValue(oldValue); } @Override public void redo() throws CannotRedoException { super.redo(); valueSetter.setValue(newValue); } }
Next there are two sub-classes of Swing UndoManager
.
The StatefulUndoManager
sets a flag when it calls some undoable edit.
This flag is exposed and enables the binding to find out whether a new edit should be created on value-change.
The Swing UndoManager
is not a super-hero implementation, but it works, also without graphics environment.
You can test this by setting -Djava.awt.headless=true
as command-line argument to your JVM.
package vaadin.examples.mvp.commands.command; import javax.swing.undo.*; public class StatefulUndoManager extends UndoManager { private boolean executingEdit; // = false by default public synchronized boolean isExecutingEdit() { return executingEdit; } @Override public synchronized void undo() throws CannotUndoException { executingEdit = true; try { super.undo(); } finally { executingEdit = false; } } @Override public synchronized void redo() throws CannotRedoException { executingEdit = true; try { super.redo(); } finally { executingEdit = false; } } }
Even more simple is NonRecursiveUndoManager
.
It uses the state of its super-class to avoid adding edits when being in execution-state.
package vaadin.examples.mvp.commands.command; import javax.swing.undo.UndoableEdit; public class NonRecursiveUndoManager extends StatefulUndoManager { /** Overridden to not add any edit while an edit is executing. */ @Override public synchronized boolean addEdit(UndoableEdit edit) { return isExecutingEdit() ? false : super.addEdit(edit); } }
This functionality is not really needed here, but could be useful when callers don't want to care about the state actively.
Summary
These classes are small and do not aggregate responsibilities. Together they refine MVP functionality. Commands can be constructed for any purpose, e.g. deleting list items, moving or copying nodes, etc. They are mementos, i.e. state snapshots. In relation with an undo-manager they can serve for rolling back and forward user actions.
Keine Kommentare:
Kommentar veröffentlichen