Implementing User Interface Flow
(UI Flow) and dialog navigation is very simple: every result from a
method call is displayed as a new page. The browser will be
automatically redirected to the appropriate page for the model that
was returned:
public <ModelOfPage> someAction() { return new <ModelOfPage>(); }
As an alternative, there is an API
function available for that:
GuiService.get().showPage(new <ModelOfPage>());
The GuiService can be called from
anywhere you want. It stores the actions you put into it and
performs them after your method returned, before the response is
rendered.
8.1) Modal Dialogs
Modal dialogs can be displayed in two different ways. The API-free
and therefore more test-friendly and portable way is to annotate a
method with
@ModalOpener
which causes the methods return value not to be displayed as a new
page (as above) but as a modal dialog inside the initiating page:
@ModalOpener public <ModelOfPanel> someAction() { return new <ModelOfPanel>(); }
Same as above, you also have the
alternative of an API function available for that:
GuiService.get().showModalPanel(new <ModelOfPanel>());
There is also another function
available that lets you specify a size for the modal dialog. Due to
the asynchronous nature of web applications, this method does not
block during display of the modal dialog (as you might be used to
from desktop UI frameworks). So we cannot return any feedback
result from the modal with this API call. Though there is an
elegant solution to this.
Getting Feedback From Modals
As usual, every action method of the modal model class becomes a
button in the dialog. By default, clicking one of the buttons
behaves as normal (which might redirect to another page or open
another modal if annotated as a modal opener). If you want the
modal to be closed upon a button press, you have to annotate it
with
@ModalCloser
. If the initiating domain object is supposed to be manipulated by
the modal operation, there are two options:
- The modal object must become aware of the initiator,
usually by simply passing the initiator in the modally displayed
objects constructor.
- Override some methods and hook into the modal models
action methods to manipulate the outer model from inside an
anonymous class that extends the modal class.
The second approach provides a way
to create modal classes that can be reused anywhere, since they
themselves are not aware of their caller. Only the caller makes
them aware in their extension inside himself.
Message & Confirm Boxes
Message boxes and confirm boxes require no special functionality in
the framework. You just implement them as a generated panel model
and do anything you want as you are used to. Though for convenience
there is a
ModalMessage
class that can be reused for the most common dialog needs. Just
create an anonymous class as an extension for it and customize it
as you want in the override. The framework even allows you to
rename the buttons (via title utility methods), hiding them (via
hide utility methods) or prevent them from closing the modal
despite the original class having them annotated as a modal closer
(by removing the annotation from the overridden method). The only
limitation is that you cannot add components, since they will not
be picked up by the HTML generator when defined inside anonymous
inner classes. In this case you should rather implement a new modal
class that fits your needs. See the Form Input page from the Wicket Examples
Rebuilt item in the top menu. There you can see an example of a
modified
ModalMessage
class that is displayed when the reset button detects model changes
that might get lost (via dirty check). Click
here to have a look at the source code of this example, look out
for the reset method implementation.
8.2) Tabbed Panes
A tabbed pane consists of the
following elements:
- Inside the model that should contain a tabbed pane, you
have to define a property that is annotated with
@Tabbed
(either the getter, the field or the type of the field should be
annotated).
- The tabbed pane type should consist of properties for each
panel that should be tabbed.
- For each of those panel properties, you implement a model
for a generated panel as you are used to.
For an example of this, have a look at the Tabbed Panel page from the Wicket Examples
Rebuilt item in the top menu. It also demonstrates how utility
elements can be used to hide/disable tabs and how the order of the
tabs can be specified with the @ColumnOrder annotation (which also
works for setting the order of table columns). Click
here for the source code of this example.
8.3) Wizards
Wizards in this frameworks sense are nothing special, since they
just define action methods for next and previous steps. You can
implement them both as pages and as modals. You make each steps
model aware of its previous one and next one and provide thus a UI
flow that results in a wizard. To see an example of this, have a
look at the Wizard page from
the Wicket Examples Rebuilt item in the top menu. Click
here for the source code of this example. You can also go a step
further than the example and reuse the navigation buttons of your
step panels by putting them in a separate generated panel that you
use via composition in your step pages/panels (adding them via a
BindingInterceptor after manually adding them to the HTML files as
a Wicket tag).
8.4) Try It
The following class shows a query
page which provides a set of car rows. To show the details of a car
row, each car row object must provide an appropriate method which
simply returns the car page model itself. This will already cause a
UI flow.
public class CarSearch implements Serializable {
private ArrayList<CarSearchRow> cars;
public CarSearch() {
cars = new ArrayList<CarSearchRow>();
cars.add(new CarSearchRow(new CarDetails(this, "LunarIndustries", "XY-ZA 123")));
// keep filling up the list of cars
}
public ArrayList<CarSearchRow> getCars() { return cars; }
}
public class CarSearchRow implements Serializable {
private final CarDetails details;
public CarSearchRow(final CarDetails details) {
this.details = details;
}
public CarDetails details() { return details; }
// uninteresting getters omitted
public String getTrips() {
return details.getTabs().getTripInfo().getTripBook().size() + " ("
+ details.getTabs().getTripInfo().getDistanceInKMSum() + " KM)";
}
}
The above is generated as a page
for an overview of all cars. It displays them in a table and
provides a page redirect in the "details" action column. The car
details page model is defined as a tabbed pane and provides a
button to navigate back.
public class CarDetails implements Serializable {
private final CarSearch parent;
private final CarTabs tabs;
public CarDetails(final CarSearch parent, final String brand, final String licenseNumber) {
this.parent = parent;
this.tabs = new CarTabs(brand, licenseNumber);
}
@Tabbed
public CarTabs getTabs() { return tabs; }
public CarSearch back() { return parent; }
}
public class CarTabs implements Serializable {
private final CarInfo carInfo;
private final TripInfo tripInfo;
public CarTabs(final String brand, final String licenseNumber) {
this.carInfo = new CarInfo(brand, licenseNumber);
this.tripInfo = new TripInfo();
}
public CarInfo getCarInfo() { return carInfo; }
public TripInfo getTripInfo() { return tripInfo; }
}
The tab panel models themselves
just display the parts of the information they are supposed to. The
trip info tab also has a button to show a modal dialog to add a new
trip to the list by hooking into the ok action method.
public class CarInfo implements Serializable {
private String state;
private String licenseNumber;
private String brand;
public CarInfo(final String brand, final String licenseNumber) {
this.state = "off";
this.brand = brand;
this.licenseNumber = licenseNumber;
}
// uninteresting getters and setters omitted
}
public class TripInfo implements Serializable {
private Collection<TripRow> tripBook;
public TripInfo() {
this.tripBook = new ArrayList<TripRow>();
}
// uninteresting getters and setters omitted
@ModalOpener
public NewTrip newTrip() {
return new NewTrip() {
@Override
@ModalCloser
public void ok() {
super.ok();
tripBook.add(new TripRow(this));
}
};
}
@Hidden
public Integer getDistanceInKMSum() {
int sum = 0;
for (final TripRow trip : tripBook) {
sum += trip.getDistanceInKM();
}
return sum;
}
}
public class TripRow implements Serializable {
private final NewTrip details;
public TripRow(final NewTrip details) {
this.details = details;
}
public Integer getDistanceInKM() { return details.getDistanceInKM(); }
// uninteresting getters and setters omitted
}
While the new trip modal class just
defines its modal closer action methods and adds some validation
for the inputs.
public class NewTrip implements Serializable {
@NotNull
private String from;
@NotNull
private String to;
@NotNull
@Min(1)
private Integer distanceInKM;
// uninteresting getters and setters omitted
@ModalCloser
public void ok() {}
@Forced
@ModalCloser
public void cancel() {}
}
This time we embedded the sample as
an iframe instead of as a panel like the previous ones. So the
navigation is triggering actual page redirects without having to
leave this chapter. It also shows how you can implement pages
without having a navigation bar. Also it is to be noted that the
navigation remembers previous page state (like the previously
selected tab for a car details page) and synchronizes new
information back to the car search page (like a new trip or changed
car info). This works in this example without any persistence layer
due to the magic of the NoWicket page model session cache. You can
try it below.
As a gimmick we also demonstrate a
validation on the trip book table. Wicket normally does not support
this easily, but NoWicket does it out of the box for you. When
trying to leave the trip info tab of a car details page, it will
not let you leave the panel until you have added a trip.