Challenges
While this application is simplistic, a number of challenges required careful design and coding. To effectively support more than one user interface channel, the design needed to clearly separate the view from other aspects of the application. Perhaps the greatest challenge in designing a service that will be deployed on small handheld devices is the limitations of those devices: small screens, numeric keypads, limited memory and processing power. Usability is also a big issue that I'll discuss later.
Multichannel Interface
It's becoming increasingly important to open up access to your applications. Web access started this trend with mobile data, voice, and even web TV channels now entering the mix. In this environment, flexibility to add new channels and an efficient way to maintain current channels are imperative. Following are some things you can do to achieve these goals:
Separate all functions that are view-independent from the view processing functions. In the Real Estate Assistant service, the business logic, validation, and even workflow methods are independent of the user interface channel.
Use templates for the user interface. The Real Estate Assistant uses a set of XSL style sheets for each supported channel. This approach easily extends to VoiceXML, XHTML Basic, and future XML display markup languages. It could even be used to support third-party application access (web services).
Use as much common processing as possible for view-related tasks. In the Real Estate Assistant, channel-specific viewers inherit common functionality from a channel-independent parent class.
All view-related functions are located in the view package. The Workflow and HttpServlet objects in the controller package are clients of the viewer objects, using them to display requested pages and error messages. The HttpServlet decides which viewer to use for a given user request. Apart from this selection, no channel-specific viewer code is found in the controller classes.
We've already looked at the XSL style sheets, so let's look at how the channel-dependent viewers work. All common view functionality is located in the Viewer class. This includes all the XSL-generation functionality and display of error pages. The only work that the channel-dependent HTML and WML viewers do is to specify the location of their style sheets, set their response type, and generate simple "canned" message pages.
Mobile Device Constraints
Mobile device constraints include user interface and memory limitations. To deal with the difficulty of text input on mobile devices, we use drop-down lists for all user input. As long as the choices are limited in number, this is a good way to interact with the user. Another consideration is username and password. Because we use HTTP Basic authentication, the user will be prompted for a username and password each time he or she logs onto the service. These values should be chosen so that they can be entered easily on a handset keypad (for example, numeric passwords and short usernames).
The memory constraints of first-generation WAP handsets are very restrictive. To be absolutely sure of avoiding display problems, WML deck sizes should not be much greater than 500 characters. We deal with this by placing only five properties on each deck and caching the remaining results on the server in the user's session. This approach works fine when the total number of possible matches is not too large; in cases where the results set is large, we would probably leave most of the results in the database and retrieve only a portion of these into a local cache (assuming that the database query language supports this feature).
Caching is done by the Workflow object:
Results results = MlsBusinessObject.doQuery(query); if (results.properties.length > resultsPerPage) { // save results request.getSession().setAttribute(LAST_RESULTS, results); Results subset = results.getSubset(0, resultsPerPage); viewer.viewPage(request, response, RESULTS_PAGE, subset, Viewer.CACHE); }
If the number of properties returned from the MLS business object is more than will fit on one page, we store the entire results set in the session (request.getSession) by setting the lastResults attribute (defined by the string constant LAST_RESULTS). We then get a subset of the properties by calling the getSubset method of the results object:
/** * Return a subset of the properties beginning at * 'start' and of length 'length'. Set attributes of the subset to * indicate if there are more results in the parent results set and to * specify the start index * @param start the starting index * @param length the number of properties to return * @return a subset of the properties */ public Results getSubset(int start, int length) { Results subset = new Results(); int l = this.properties.length - start; if (l > length) { l = length; subset.more = true; } else subset.more = false; subset.properties = new Property[l]; for (int i=0; i<l; i++) subset.properties[i] = properties[start+i]; subset.startIndex = start; return subset; }
The specified subset of properties is copied into subset. Flags are set to indicate whether there are more properties in the parent results set and also to indicate the start index of this subset. Figure 3 shows a results screen in which there are more results to retrieve.
Figure 3 Mobile interface partial results summary.
Clicking Next runs the fetchNext workflow operation:
/** * Operation method to fetch a partial results set from * a previous query. * @param request the Http request from the user agent * @param response where the response is to be written * @param viewer a viewer to display the output * @return true */ public boolean fetchNext(HttpServletRequest request, HttpServletResponse response, Viewer viewer) throws IOException { try { Results results = (Results)request.getSession().getAttribute(LAST_RESULTS); // Retrieve number of results per page set in initial query int resultsPerPage = getSavedResultsPerPage(request); // Get start index of current results set, increment to next int startIndex = Integer.parseInt(request.getParameter("startIndex")) + resultsPerPage; Results subset = results.getSubset(startIndex, resultsPerPage); viewer.viewPage(request, response, RESULTS_PAGE, subset, Viewer.CACHE); } catch (Exception e) { viewer.viewError(request, response, Errors.ERROR_FETCHING_NEXT, e); } return true; }
This method gets the saved results from the session, checks how many results are to be displayed per page, sets the correct start index for the next results subset, and gets this subset from the results.