IT Mill Toolkit Release 5 introduces support for multiple application-level windows that can be used just like the main window. All such windows use the same application session. Each window is identified with a URL that is used to access it. This makes it possible to bookmark application-level windows. Such windows can even be created dynamically based on URLs.
Application-level windows allow several uses important for the usability of browser-based applications.
Because of the special nature of AJAX applications, these uses require some caveats. We will go through them later in Section 3.4.4, “Caveats in Using Multiple Windows”.
Creating a new application-level window is much like creating a child
window, except that the window is added with
addWindow()
to the application object instead of
the main window.
public class WindowTestApplication extends Application { public void init() { final Window main = new Window ("Window Test Application"); setMainWindow(main); /* Create a new window. */ final Window mywindow = new Window("Second Window"); /* Manually set the name of the window. */ mywindow.setName("mywindow"); /* Add some content to the window. */ mywindow.addComponent(new Label("This is a second window.")); /* Add the window to the application. */ addWindow(mywindow); } }
This creates the window object that a user can view by opening the URL in
a browser. Creating an application-level window object does not open a new
browser window automatically to view the object, but if you wish to open
one, you have to do it explicitly as shown below. An application-level
window has a unique URL, which is based on the application URL and the
name of the window given with the setName()
method. For example, if the application URL is
http://localhost:8080/myapp/
and the window name is
mywindow
, the URL for the window will be
http://localhost:8080/myapp/mywindow/
. If the name of a window
is not explicitly set with setName()
, an
automatically generated name will be used. The name can be retrieved with
the getName()
method and the entire URL with
getURL()
.
There are three typical ways to open a new window: using the
open()
method of Window
class, a Link
, or referencing it from HTML or
JavaScript code written inside a Label
component.
The Window
open()
method
takes as parameters a resource to open and the target name. You can use
ExternalResource
to open a specific URL, which you
get from the window to be opened with the
getURL()
method.
/* Create a button to open a new window. */ main.addComponent(new Button("Click to open new window", new Button.ClickListener() { public void buttonClick(ClickEvent event) { // Open the window. main.open(new ExternalResource(mywindow.getURL()), "_new"); } }));
The target name is one of the default HTML target names (_new
, _blank
, _top
, etc.) or a custom target name. How the window is
exactly opened depends on the browser. Browsers that support tabbed
browsing can open the window in another tab, depending on the browser
settings.
Another typical way to open windows is to use a Link
component with the window URL as an
ExternalResource
.
/* Add a link to the second window. */ Link link = new Link("Click to open second window", new ExternalResource(mywindow.getURL())); link.setTargetName("second"); link.setTargetHeight(300); link.setTargetWidth(300); link.setTargetBorder(Link.TARGET_BORDER_DEFAULT); main.addComponent(link);
Using a Link
allows you to specify parameters for
the window that opens by clicking on the link. Above, we set the
dimensions of the window and specify what window controls the window
should contain. The Link.TARGET_BORDER_DEFAULT
specifies to use the default, which includes most of the usual window
controls, such as the menu, the toolbar, and the status bar.
Another way to allow the user to open a window is to insert the URL in
HTML code inside a Label
. This allows even more
flexibility in specifying how the window should be opened.
/* Add the link manually inside a Label. */ main.addComponent(new Label("Second window: <a href='" +mywindow.getURL()+"' target='second'>click to open</a>", Label.CONTENT_XHTML)); main.addComponent(new Label("The second window can be accessed through URL: " +mywindow.getURL()));
When an application-level window is closed in the browser the
close()
method is called just like for a child
window and the Window
object is purged from the
application.
You can create a window object dynamically by its URL path by overriding
the getWindow()
method of the
Application
class. The method gets a window name as
its parameter and must return the corresponding
Window
object. The window name is determined from
the first URL path element after the application URL (window name may not
contain slashes). See the notes below for setting the actual name of the
dynamically created windows below.
The following example allows opening windows with a window name that
begins with "planet-
" prefix. Since the method is
called for every browser request for the application,
we filter only the requests where a window with the given name does not
yet exist.
public class WindowTestApplication extends Application { ... @Override public Window getWindow(String name) { // If a dynamically created window is requested, but it does // not exist yet, create it. if (name.startsWith("planet-") && super.getWindow(name) == null) { String planetName = name.substring("planet-".length()); // Create the window object. Window newWindow = new Window("Window about " + planetName); // DANGEROUS: Set the name explicitly. Otherwise, an // automatically generated name is used, which is usually safer. newWindow.setName(name); // Put some content in it. newWindow.addComponent(new Label("This window contains details about " + planetName + ".")); // Add it to the application as a regular application-level window. addWindow(newWindow); return newWindow; } // Otherwise the Application object manages existing windows by their name. return super.getWindow(name); }
The window name is and must be a unique indentifier for each
Window
object instance. If you use
setName()
to set the window name explicitly, as
we did above, any browser window that has the same URL (within the same
browser) would open the same window object. This is
dangerous and generally not recommended, because the
browser windows would share the same window object. Opening two windows
with the same static name would immediately lead to a synchronization
error, as is shown in Figure 3.3, “Synchronization Error Between Windows with the Same Name” below. (While also the
window captions are same, they are irrelevant for this problem.)
There are some cases where setting the name explicitly is useful. The launch application below is one example, as it always opens the other windows in a window target that is specific to the window name, thereby never creating two windows with the same URL. Similarly, if you had embedded the application in a browser frame and the link would open the window in a frame, you would not have problems. Having a single window instance for a URL is also useful if the browser crashes and the user opens the window again, as it will have kept its previous (server-side) state.
Leaving the window name to be automatically generated allows opening
multiple windows with the same URL, while each of the windows will have a
separate state. The URL in the location bar stays unchanged and the
generated window name is used only for the Ajax communications to identify
the window object. A generated name is a string representation of a unique
random number, such as "1928676448
". You should be
aware of the generated window names when overriding the
getWindow()
method (and not unintentionally
create a new window instance dynamically for each such request). The
condition in the above example would also filter out the requests for an
already existing window with a generated name.
Figure 3.4, “A Dynamically Created Window” shows a
dynamically created application-level window with the URL shown in the
address bar. The URL for the application is here
http://localhost:8080/tk5/windowexample/
, including the
application context, and the dynamically created window's name is
planet-mars
.
The application knows the windows it already has and can return them after the creation. The application also handles closing and destruction of application-level window objects, as discussed in Section 3.4.3, “Closing Windows”.
Such dynamic windows could be opened as in the following example:
public void init() { final Window main = new Window("Window Test Application"); setMainWindow(main); // Have some IDs for the dynamically creatable windows. final String[] items = new String[] { "mercury", "venus", "earth", "mars", "jupiter", "saturn", "uranus", "neptune" }; // Create a list of links to each of the available window. for (int i = 0; i < items.length; i++) { // Create a URL for the window. String windowUrl = getURL() + "planet-" + items[i]; // Create a link to the window URL. // Using the item ID for the target also opens it in a new // browser window (or tab) unique to the window name. main.addComponent(new Link("Open window about " + items[i], new ExternalResource(windowUrl), items[i], -1, -1, Window.BORDER_DEFAULT)); } }
When the user closes an application-level window, the Client-Side Engine
running in the browser will report the event to the server before the page
is actually removed. You can catch the event with a
Window.CloseListener
, as is done in the example
below.
newWindow.addListener(new Window.CloseListener() { @Override public void windowClose(CloseEvent e) { // Do something. System.out.println(e.getWindow().getName() + " was closed"); // Add a text to the main window about closing. (This does // not update the main window.) getMainWindow().addComponent( new Label("Window '" + e.getWindow().getName() + "' was closed.")); } });
Notice that the change to the server-side state of the main window (or another application-level window) does not refresh the window in the browser, so the change will be unseen until user interaction or polling refreshes the window. This problem and its dangers are discussed in Section 3.4.4, “Caveats in Using Multiple Windows” below.
The close event does not occur if the browser crashes or the connection is otherwise severed violently. In such a situation, the window object will be left hanging, which could become a resource problem if you allow the users to open many such application-level windows. The positive side is that the user can reconnect to the window using the window URL.
For cases where you need communication between windows, we recommend using floating child windows. In IT Mill Toolkit Release 5, an application window can not update the data in other windows. The contents of a window can only be updated when the particular window makes a request to the server. The request can be caused by user input or through polling.
Changing the server-side state of a window while processing a user event from another window can potentially cause serious problems. Changing the client-side state of a window does not always immediately communicate the changes to the server. The server-side state can therefore be out of sync with the client-side state.
The following example creates a second window that changes the contents of the main window, as illustrated in the figure above. In this simple case, changing the main window contents is safe.
// Create a table in the main window to hold items added in the second window final Table table = new Table(); table.setPageLength(5); table.getSize().setWidth(100, Size.UNITS_PERCENTAGE); table.addContainerProperty("Name", String.class, ""); main.addComponent(table); // Create the second window final Window adderWindow = new Window("Add Items"); adderWindow.setName("win-adder"); main.getApplication().addWindow(adderWindow); // Create selection component to add items to the table final NativeSelect select = new NativeSelect("Select item to add"); select.setImmediate(true); adderWindow.addComponent(select); // Add some items to the selection String items[] = new String[]{"-- Select --", "Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"}; for (int i=0; i<items.length; i++) select.addItem(items[i]); select.setNullSelectionItemId(items[0]); // When an item is selected in the second window, add // table in the main window select.addListener(new ValueChangeListener() { public void valueChange(ValueChangeEvent event) { // If the selected value is something else but null selection item. if (select.getValue() != null) { // Add the selected item to the table in the main window table.addItem(new Object[]{select.getValue()}, new Integer(table.size())); } } }); // Link to open the selection window Link link = new Link("Click to open second window", new ExternalResource(adderWindow.getURL()), "_new", 50, 200, Link.TARGET_BORDER_DEFAULT); main.addComponent(link); // Enable polling to update the main window ProgressIndicator poller = new ProgressIndicator(); poller.addStyleName("invisible"); main.addComponent(poller);
The example uses an invisible ProgressIndicator
to implement polling. This is sort of a trick and a more proper API
for polling is under design. Making the progress indicator invisible
requires the following CSS style definition:
.i-progressindicator-invisible { display: none; }