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.