Overview
Many web applications have a need to jump to a page, or a series of pages, to get information from the user and then return to the original page to use that information. Apache Trinidad calls these "dialogs". Apache Trinidad can show a dialog just by navigating to a new page within your main browser window, or by showing a popup window. Apache Trinidad saves you from the effort of managing the Javascript needed to launch these windows, and encapsulates the differences between popup windows and ordinary navigation; for example, it can switch to using the same window if a client doesn't support opening new windows (a PDA, for example).
This dialog framework can be launched manually with Java APIs, or more easily with ordinary JSF navigation rules. You can support it from your own custom UIComponents and Renderers, or even from a custom RenderKit. It is in part built on lower-level APIs that support "pushing" and "popping" pages that you can reuse on their own to show login pages or save state for multi-page wizards.
Launching Dialogs
The simplest way to interact with the dialog framework is with the functionality built-in to the org.apache.myfaces.trinidad.component.UIXCommand component class. This class supports all the standard "action" and "actionListener" attributes of the JSF UICommand component, and that's how you'll use it most of the time. But UIXCommand - combined with some <navigation-rule> magic - also supports launching dialogs, and lets you know when that dialog finishes with a ReturnEvent.
To specify a rule that launches a dialog, simply use an outcome that begins with "dialog:":
<tr:commandButton text="Show More Information" action="dialog:showDetail"/> ... <navigation-rule> <from-view-id>/*</from-view-id> <navigation-case> <from-outcome>dialog:showDetail</from-outcome> <to-view-id>/showDetail.jspx</to-view-id> </navigation-case> </navigation-rule>
This will show the "/showDetail.jspx" page in the same window. If you wanted to show it in a popup window, just add useWindow="true" to the command:
<tr:commandButton text="Show More Information" partialSubmit="true" useWindow="true" action="dialog:showDetail"/>
Note that we've also set "partialSubmit" on the commandButton to "true"; we highly recommend using this option on buttons that launch dialogs, as it avoids an otherwise unnecessary flash of the main page as the dialog is launched.
You can use a hardcoded outcome from an Apache Trinidad command component, but, just as with any "action", you can also programatically decide whether or not you want to show a dialog (or what dialog to show) from an ordinary "action" method. For example, an action might check if a user's been logged out, and if so, show a warning dialog instead of navigating ordinarily.
<tr:commandButton text="Show More Information" useWindow="true" action="#{backingBean.goSomewhere}"/> ... public String goSomewhere() { if (isUserLoggedOut()) { return "dialog:loggedOutWarning"; } else { // Note that "useWindow" is only relevant if // the outcome begins with "dialog:"; this // will not show a dialog! return "somewhere"; } }
All of these techniques do require that you've defined a navigation rule with a "dialog:" outcome. Later, we'll see how to launch a dialog programatically.
In all of these cases, when you're using a web browser that supports all the features we need for launching dialogs, a new window will appear containing the dialog. But if you're using some other web browser or a device like a PDA that doesn't satisfy our requirements, your dialog code will still work! Instead of launching a new window, Apache Trinidad simply show you the dialog page in the current window after automatically preserving all the state of your current page.
When the dialog finishes (more on what that means in a bit), your command component gets a ReturnEvent delivered to a ReturnListener, if you've registered one (either with addReturnListener or using the "returnListener" property). The dialog can give you a return value, and if it does, that will automatically be handed to you as a property of the ReturnEvent:
<tr:commandButton text="Get Value" action="dialog:getValue" returnListener="#{backing.handleReturn}"/> ... public void handleReturn(ReturnEvent event) { Object returnedValue = event.getReturnValue(); // ... handle that return value as desired ... }
The dialog itself can be written like any other JSF page. The only way in which it differs is that you need to call a special method to tell Apache Trinidad that the dialog is complete - RequestContext.returnFromDialog(). You can call this method whether the dialog was shown in a popup window or not; if it was a popup window, the dialog window will close automatically. You also don't need to call it in the first page you show - you can navigate from the first page in the dialog to as many other pages as you want, and just need to call returnFromDialog() eventually. This method is also what lets you send the "return value" back from your dialog:
<!-- In your dialog page --> <tr:commandButton text="Done" actionListener="#{dialogBacking.done}"/> ... public void done(ActionEvent e) { // Get the return value Object returnValue = ...; // And return it RequestContext.getCurrentInstance().returnFromDialog(returnValue, null); }
Example
Let's work through a full (if simple) example to put the pieces together. We'll write a dialog that lets a user add two numbers, and we'll use that dialog to fill in a field on the original page.
First, let's write the dialog:
<?xml version='1.0' encoding='utf-8'?> <jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" xmlns:f="http://java.sun.com/jsf/core" xmlns:tr="http://myfaces.apache.org/trinidad" version="1.2"> <jsp:directive.page contentType="text/html;charset=utf-8"/> <f:view> <tr:document title="Add dialog"> <tr:form> <!-- Two input fields --> <tr:panelForm> <tr:inputText label="Number 1:" value="#{chooseInteger.value1}" required="true" /> <tr:inputText label="Number 2:" value="#{chooseInteger.value2}" required="true" /> </tr:panelForm> <!-- Two buttons -> <tr:panelGroup layout="horizontal"> <tr:commandButton text="Submit" action="#{chooseInteger.select}"/> <tr:commandButton text="Cancel" immediate="true" action="#{chooseInteger.cancel}"/> </tr:panelGroup> </tr:form> </tr:document> </f:view> </jsp:root>
This is a pretty simple page, with a couple of required input fields and a couple of command buttons. We've put them in a bunch of containers to make the page a bit prettier, but that's all fairly simple. Now, we need to build the "chooseInteger" backing bean. First, we'll add a managed-bean to our faces-config.xml, and also a "dialog:" navigation rule so other pages can get to this dialog:
<managed-bean> <managed-bean-name>chooseInteger</managed-bean-name> <managed-bean-class> org.apache.myfaces.trinidaddemo.ChooseIntegerBean </managed-bean-class> <managed-bean-scope> request </managed-bean-scope> </managed-bean> <navigation-rule> <from-view-id>/*</from-view-id> <navigation-case> <from-outcome>dialog:chooseInteger</from-outcome> <to-view-id>/demos/chooseInteger.jspx</to-view-id> </navigation-case> </navigation-rule>
And now, the code for ChooseIntegerBean:
package org.apache.myfaces.trinidaddemo; import org.apache.myfaces.trinidad.context.RequestContext; public class ChooseIntegerBean { public Integer getValue1() { return _value1; } public void setValue1(Integer value1) { _value1 = value1; } public Integer getValue2() { return _value2; } public void setValue2(Integer value2) { _value2 = value2; } public String cancel() { RequestContext.getCurrentInstance().returnFromDialog(null, null); return null; } public String select() { Integer value = new Integer(getValue1().intValue() + getValue2().intValue()); RequestContext.getCurrentInstance().returnFromDialog(value, null); return null; } private Integer _value1; private Integer _value2; }
This code shows an example of how to call returnFromDialog(). In the cancel() case, we just return null; for select(), we add the two numbers and return that Integer. (We marked both fields as required, which is why I can be lazy and fail to check that getValue1() and getValue2() aren't null.) But note that in neither action do we have any notion of where we're returning this value to - we just rely on the Apache Trinidad framework to figure that out.
Now, let's write a page that uses that dialog:
<?xml version='1.0' encoding='utf-8'?> <jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" xmlns:f="http://java.sun.com/jsf/core" xmlns:tr="http://myfaces.apache.org/trinidad" version="1.2"> <jsp:directive.page contentType="text/html;charset=utf-8"/> <f:view> <tr:document title="Dialog Demo"> <tr:form> <!-- The field for the value; we point partialTriggers at the button to ensure it gets redrawn when we return --> <tr:inputText label="Pick a number:" value="(Empty)" partialTriggers="buttonId" binding="#{launchDialog.input}"/> <!-- The button for launching the dialog: we've also configured the width and height of that window --> <tr:commandButton text="Add" action="dialog:chooseInteger" id="buttonId" windowWidth="300" windowHeight="200" partialSubmit="true" useWindow="true" returnListener="#{launchDialog.returned}"/> </tr:form> </tr:document> </f:view> </jsp:root>
We've kept it pretty simple: a single input field, stored with a "binding", and a button that shows the "chooseInteger" dialog with a hardcoded "dialog:" outcome. About the only un-typical thing in this page is that "returnListener". Here's the managed-bean entry:
<managed-bean> <managed-bean-name>launchDialog</managed-bean-name> <managed-bean-class> org.apache.myfaces.trindaddemo.dialog.LaunchDialogBean </managed-bean-class> <managed-bean-scope> request </managed-bean-scope> </managed-bean>
And, here's the LaunchDialogBean code:
package org.apache.myfaces.trindaddemo.dialog; import org.apache.myfaces.trinidad.component.UIXInput; import org.apache.myfaces.trinidad.event.ReturnEvent; public class LaunchDialogBean { public UIXInput getInput() { return _input; } public void setInput(UIXInput input) { _input = input; } public void returned(ReturnEvent event) { if (event.getReturnValue() != null) { getInput().setSubmittedValue(null); getInput().setValue(event.getReturnValue()); } } private UIXInput _input; }
We include the methods used by "binding" to have access to the input field, and a single returned() method which will be called when the dialog finishes. If the return value is null, then the user cancelled the dialog, and we don't need to do anything. Otherwise, we need to set the value on the input component. It's not quite enough to just call setValue() on the input component. You also have to call setSubmittedValue() with null to clear out any originally submitted value.
Passing Information In and Out
You've already seen how to return a value from a dialog by passing that value to RequestContext's returnFromDialog() method. But how to pass a value in? Apache Trinidad lets you supply your dialogs with a Map of "dialog parameters". (If you haven't read the Communicating Between Pages: pageFlowScope chapter, now might be a good time.)
When Apache Trinidad knows that its about to launch a dialog because of the outcome of an action method, it queues a LaunchEvent. One of the methods one this event is getDialogParameters(), which will give you a Map that you can add parameters to:
<tr:commandButton text="Get Value" action="dialog:getValue" returnListener="#{backing.handleReturn}" launchListener="#{backing.addParameter}"/> ... public void addParameter(LaunchEvent event) { // Pass the current value of the field into the dialog. Object value = getInput().getValue(); event.getDialogParameters().put("inputValue", value); }
When you add parameters in this way, they show up in the dialog on the Apache Trinidad "pageFlowScope", and pages in the dialog can get the value out of the pageFlowScope:
<tr:outputText value="Input value: #{pageFlowScope.inputValue}"/>
The Apache Trinidad pageFlowScope has a very specific interaction with dialogs. The dialog always gets a copy of all values that were in the pageFlowScope of the launching page, plus all the "dialog parameters" in the LaunchEvent. But anything you do to the pageFlowScope inside the dialog ends there - when you return from the dialog, the pageFlowScope will be back the way it was before the dialog started. If you need to pass values out of the dialog, you have to use RequestContext.returnFromDialog(), session scope, or application scope. This approach means that dialogs are isolated from the calling code, and gives dialogs a place to store values without worrying about overwriting something that the parent window needed.
inputListOfValues
One pattern is so common that we've given it its own component: <tr:inputListOfValues>. This is an input component that can launch a dialog and automatically accept its return value, so instead of the prior example combining an <tr:inputText> and <tr:commandButton>, you simply need:
<tr:inputListOfValues label="Pick a number:" value="(Empty)" action="dialog:chooseInteger" windowWidth="300" windowHeight="200"/>
This component will automatically handle launching the dialog, receiving the ReturnEvent, and updating the value of the field, all without any code on your part. (We don't need "partialSubmit" or "useWindow"; those are automatically set by <tr:inputListOfValues>.)
Manually Launching A Dialog
You can also programatically launch a dialog without any navigation rule using the RequestContext.launchDialog() method:
/** * Launch a dialog, optionally raising it in a new window. * * The dialog will receive a new pageFlowScope map, * which includes all the values of the currently available pageFlowScope * as well as a set of properties passed to this function in * the dialogParameters map. Changes to this newly * created scope will not be visible once the dialog returns. * * @param dialogRoot the UIViewRoot for the page being launched * @param source the UIComponent that launched the dialog and * should receive the ReturnEvent when the dialog is complete. * @param dialogParameters a set of parameters to populate the * newly created pageFlowScope * @param useWindow if true, use a popup window for the dialog * if available on the current user agent device * @param windowProperties the set of UI parameters used to * modify a popup window. The set of properties that are * supported will depend on the RenderKit, but * common examples include "width" and "height". */ public abstract void launchDialog( UIViewRoot dialogRoot, UIComponent source, Map dialogParameters, boolean useWindow, Map windowProperties);
<!-- Send a PollEvent after 10 minutes --> <tr:poll pollListener="#{backing.sessionAboutToExpire}" interval="600000"/> ... public void sessionAboutToExpire(PollEvent event) { FacesContext context = FacesContext.getCurrentInstance(); // Create the dialog UIViewRoot ViewHandler viewHandler = context.getApplication().getViewHandler(); UIViewRoot dialog = viewHandler.createView(context, "/sessionExpiring.jspx"); Map properties = new HashMap(); properties.put("width", new Integer(250)); properties.put("height", new Integer(150)); RequestContext requestContext = RequestContext.getCurrentInstance(); requestContext.launchDialog(dialog, null, // not launched from any component null, // no particular parameters true, // show it in a dialog properties); } }
Supporting Dialogs in Custom Components
The Apache Trinidad dialog framework can be used inside your own components and renderers. You can use it in one of two ways. First, in a component that already supports ActionEvents, you can add direct support for LaunchEvents and ReturnEvents at the component level. Less obviously, you can take advantage of the dialog framework to let a Renderer launch a dialog and handle return values without ever interfering with the component. This latter technique is especially handy if you want to bundle some dialogs with your renderer, like a calendar dialog with a date-editing component.
First, let's look at the case where you already support ActionEvents and are adding ReturnEvents and LaunchEvents to your component. First, add code for ReturnListeners and LaunchListeners, which looks like the code for all other Faces listeners:
public YourComponent implements ActionSource { ... public void addReturnListener(ReturnListener listener) { addFacesListener(listener); } public void removeReturnListener(ReturnListener listener) { removeFacesListener(listener); } public ReturnListener[] getReturnListeners() { return (ReturnListener[]) getFacesListeners(ReturnListener.class); } public MethodBinding getReturnListener() { return _returnListener; } public void setReturnListener(MethodBinding returnListener) { _returnListener = returnListener; } // and exactly the same thing for LaunchListeners... public void addReturnListener(LaunchListener listener) { addFacesListener(listener); } // etc... }
Next, we need to detect ReturnEvents. Here, we'll use a method on an DialogService, which you get from an RequestContext: getReturnEvent(UIComponent source):
public void decode(FacesContext context) { RequestContext requestContext = RequestContext.getCurrentInstance(); ReturnEvent event = requestContext.getDialogService().getReturnEvent(this); if (event != null) event.queue(); else super.decode(context); }
The getReturnEvent() method sees if there is a pending ReturnEvent for your component. If there is, you can queue it and forget about any further decoding.
All the rest of the interesting code lives in broadcast(). It's a lot of code, so we'll walk through it.
public void broadcast(FacesEvent event) throws AbortProcessingException { // Perform special processing for ActionEvents: tell // the RequestContext to remember this component instance // so that the NavigationHandler can locate us to queue // a LaunchEvent. if (event instanceof ActionEvent) { RequestContext requestContext = RequestContext.getCurrentInstance(); requestContext.getDialogService().setCurrentLaunchSource(this); try { // Perform standard superclass processing super.broadcast(event); // Notify the specified action listener method (if any), // and the default action listener _invokeListener(event, getActionListener()); FacesContext context = getFacesContext(); ActionListener defaultActionListener = context.getApplication().getActionListener(); if (defaultActionListener != null) { defaultActionListener.processAction((ActionEvent) event); } } finally { requestContext.getDialogService().setCurrentLaunchSource(null); } } else { // Perform standard superclass processing super.broadcast(event); if (event instanceof LaunchEvent) { _invokeListener(event, getLaunchListener()); LaunchEvent launchEvent = (LaunchEvent) event; RequestContext requestContext = RequestContext.getCurrentInstance(); boolean useWindow = Boolean.TRUE.equals(getAttributes().get("useWindow")); launchEvent.launchDialog(useWindow); } else if (event instanceof ReturnEvent) { _invokeListener(event, getReturnListener()); getFacesContext().renderResponse(); } } } // // Helper function for invoking a MethodBinding for a listener // private final void _invokeListener( FacesEvent event, MethodBinding method) throws AbortProcessingException { if (method != null) { try { FacesContext context = getFacesContext(); method.invoke(context, new Object[] { event }); } catch (EvaluationException ee) { Throwable t = ee.getCause(); // Unwrap AbortProcessingExceptions if (t instanceof AbortProcessingException) throw ((AbortProcessingException) t); throw ee; } } }
broadcast() basically handles each type of event one-by-one: first ActionEvents, then LaunchEvents, and finally ReturnEvents. In all three cases, we call super.broadcast(), which will call any listeners registered with an addXyzListener() method, and then call _invokeListener() to handle the associated MethodBinding.
The rest of the ActionEvent handling is mostly code required by JSF: in particular, we call through to the "default ActionListener". This is the code that invokes the Action and calls the NavigationHandler. But, in addition, we use RequestContext.getDialogService().setCurrentLaunchSource(). This call is needed so that code from a NavigationHandler can correctly queue a LaunchEvent; to do that, it needs the source UIComponent, and it does not typically have access to the source component. This method gives it that access. (If you do not call this method, you don't get a LaunchEvent back, and the NavigationHandler will simply launch a non-popup-window dialog directly.)
The LaunchEvent handling code - after letting listeners get a crack to add parameters, etc. - uses a utility method on the LaunchEvent to launch the dialog. In this example, we're checking the "useWindow" attribute to see if we want to use a window for the dialog. This isn't required at all - you could use a different attribute, or always pass true, etc.; you can pick any strategy you want for your component.
Finally, in ReturnEvent, we don't do much beyond letting listeners detect the return, but we do call FacesContext.renderResponse(), because you don't generally want to update the model immediately upon returning from a dialog, but you're free to design your component otherwise.
Supporting Dialogs in Custom Renderers
In a custom Renderer, there may be no LaunchEvent or ActionEvent, and there's not really a ReturnEvent either, since the UIComponent has to contain code for listeners and to handle broadcast(). Even so, we can still use the dialog framework to show a dialog from our renderer and handle returning from the dialog, all without any code from the page author using the renderer. We'll need only two functions on RequestContext, both of which we've already seen: launchDialog and getReturnEvent. For an example, let's imagine we've already get a Renderer for an input field, and we want to add add a button that launches a dialog, and uses the value to fill in that dialog.
<!-- An input field that uses /myDialog.jsp to get the value --> <my:inputFromDialog viewId="/myDialog.jsp"/>
First, we'll add some encodeEnd() output to put in a button:
public class InputFromDialogRenderer { public void encodeEnd(FacesContext context, UIComponent component) throws IOException { // ... any pre-existing encodeEnd() code // Write out <input type="submit" name="[clientId]:button" value="dialog"> ResponseWriter out = context.getResponseWriter(); out.startElement("input", component); out.writeAttribute("type", "submit", null); String clientId = component.getClientId(context); out.writeAttribute("name", clientId + ":button", null); out.writeAttribute("value", "dialog", null); out.endElement("input"); }
The rest of the code goes in decode(). It'll need to detect both when we want to launch a dialog, and when we're returning from a dialog:
public void decode(FacesContext context, UIComponent component) { // Get the EditableValueHolder interface, and get the // RequestContext EditableValueHolder evh = (EditableValueHolder) component; RequestContext requestContext = RequestContext.getCurrentInstance(); // If there's a ReturnEvent waiting for us, just use its value // and finish ReturnEvent event = requestContext.getDialogService().getReturnEvent(component); if (event != null) { evh.setSubmittedValue(event.getReturnValue()); // Don't try writing to the model - just redisplay the // page with this value context.renderResponse(); return; } Map parameters = context.getExternalContext().getRequestParameterMap(); String id = component.getClientId(context) + ":button"; // See if the user pressed a button if ("dialog".equals(parameters.get(id))) { // Get the right viewId String viewId = (String) component.getAttributes().get("viewId"); UIViewRoot viewRoot = context.getApplication().getViewHandler().createView(context, viewId); // Pass the current value to the dialog Map dialogParameters = new HashMap(); dialogParameters.put("inputValue", evh.getValue()); // And give it a width and height (hardcoded in this example) Map windowProperties = new HashMap(); windowProperties.put("width", new Integer(300)); windowProperties.put("height", new Integer(200)); requestContext.launchDialog(viewRoot, this, dialogParameters, true, windowProperties); } else { // Old code for decode() goes here.. ... } }
First, we check for a ReturnEvent. Our component here doesn't support ReturnEvents, so we're not going to queue this event. Instead, we'll just grab the dialog's return value and call setSubmittedValue() to save it off. Otherwise, we see if the button was clicked, and, if so, create a UIViewRoot and ask the RequestContext to show it in a dialog. We're also passing the current value of our input field off to the dialog as a dialog parameter. And that's it: lots of Apache Trinidad code will work behind the scenes to pop up your dialog, but the Renderer code is simple, and the page author doesn't need to do a thing - the input component will just deliver an ordinary ValueChangeEvent the next time the page is submitted.
Supporting Dialogs in a Custom RenderKit
(Not documented yet: see DialogRenderKitService and ExtendedRenderKitService interfaces.)
Low-level APIs: pushView() and popView()
In part, the dialog code relies on a couple of methods on the DialogService API:
/** * Push a UIViewRoot onto a stack in preparation * for navigating to a dialog. */ public abstract void pushView(UIViewRoot viewRoot); /** * Pop a UIViewRoot from a stack. If navigateToPopped is true, * this method may result in calls to * FacesContext.renderResponse() or even * FacesContext.responseComplete(). * * @param navigateToPopped If true, navigate to the view popped * of the stack with FacesContext.setViewRoot(). * If false, simply drop the view. */ public abstract void popView(boolean navigateToPopped);
When you use these, you can't get a ReturnEvent, and you don't get any special support for pageFlowScope isolation, but if you don't need either of these, it can provide a very handy way to, for example, jump to a login page:
public String doSomethingNeedingLogin() { if (!isUserLoggedIn()) { // Push the current view root FacesContext context = FacesContext.getCurrentInstance(); RequestContext.getCurrentInstance().getDialogService(). pushView(context.getViewRoot()); // And log in return "login"; } else { ... } }
Now, in our login page, we just need to return to where we came from:
public String logIn() { if (loginWasSuccessful()) { // Pop back RequestContext.getCurrentInstance().getDialogService(). popView(true); // And return null, because popView() already did the navigation return null; } else { ... } }
Dialog Caveats
Users of the Apache Trinidad dialog framework should be aware of a few current limitations.
First, while we can detect that a particular user agent - like a PDA - cannot launch windows, and fall back to single-window behaviors - we cannot currently detect popup blockers (like the current Mozilla/FireFox popup blocker) that may or may not be enabled on a particular user. Supporting this would be rather tricky, because popup blockers don't give the server any sort of advanced knowledge that would let us know to switch our server-side behavior. The current solution is, unfortunately, asking users to disable popup blocking for your site. Long term, we might try detecting support for popups up front, or some other hackery, and in the longer term, we may move to using inline DIVs and IFRAMEs to show dialogs; one nice thing about the dialog framework is that it would hide any such changes from programmers.
Second, we do not currently support the use of <redirect/> in association with navigation rules used to show a popup window. (The behavior you'll see is that you navigate directly to the page instead of showing a popup window.)