Additional Lesson II: Swing, Part 2

In the last lesson you learned how to build a basic Swing application, using panels, labels, buttons, text fields, and lists. You also learned how to use Swing's layout managers for enhancing the look of your user interface (UI).

In this chapter, you will learn about:

• Scroll panes

• Borders

• Setting text in the title bar

• Icons

• Keyboard support" role="lower-capitalized

• Button mnemonics

• Required fields

• Keyboard listeners

• The Swing Robot class

• Field edits and document filters

• Formatted text fields

• Tables

• Mouse listeners

• Cursors

• SwingUtilities methods invokeAndWait and invokeLater

Miscellaneous Aesthetics

In this section you'll learn how to enhance the look of the existing Courses-Panel.

JScrollPane

If you add more than a few courses using sis.ui.Sis, you'll note that the JList shows only some of them. To fix this problem, you can wrap the JList in a scroll pane. The scroll pane acts as a viewport onto the list. When the list's model contains more information than can be displayed given the current JList size, the scroll pane draws scroll bars. The scroll bars allow you to move the viewport either horizontally or vertically to reveal hidden information.

The bold code in this listing for the CoursesPanel adds a scroll pane. You can specify whether you want to show scroll bars always or only as needed using either setVerticalScrollBarPolicy or setHorizontalScrollBarPolicy. I find it more aesthetically pleasing to always show the vertical scroll bar.

image

Borders

The courses list and associated labels directly abut the edge of the panel. You can use a border to create a buffer area between the edges of the panel and any of its components.

image

You can use the BorderFactory class to create one of several different border types. Most of the borders are for decorative purposes; the empty border is the exception. You can also combine borders using createCompoundBorder. The reworked createLayout method demonstrates the use of a few different border types.

image

The use of a titled border eliminates the need for a separate JLabel to represent the “Courses:” text. The elimination of the JLabel will break testCreate in CoursesPanelTest—did you remember to make this change by testing first?

Adding a Title

No text appears in the SIS frame window's titlebar. Rectify this by updating testCreate in SisTest:

image

JFrame supplies constructors that allow you to pass in titlebar text. The code in Sis:

image

Icons

The final aesthetic element you'll add is an icon for the window. By default, you get the cup-o-Java icon, which appears as a mini-icon in the titlebar and when you minimize the window. Since the icon is part of the titlebar, it falls under the control of the frame window.

A test can simply ask for the icon from a frame by sending it the message getIconImage. The method, implemented in java.awt.Frame, returns an object of type java.awt.Image. The code in testCreate asserts that the icon retrieved from the SIS frame is the same as one explicitly loaded by name. Both the test and the sis.ui.Sis code will use a common utility method to load the image: ImageUtil.create.

image

Create the class ImageUtilTest in the sis.util package. There are several ways to write a test against the create method. The best way would be to dynamically generate an image using a collection of pixels and write it out to disk. After the create method loaded the image, you could assert that the pixels in the loaded image were the same as the original pixel collection. Unfortunately, this is an involved solution, one that is best solved using an API that eases the dynamic creation of images.

A simpler technique is to allow the test to assume that a proper image already exists on disk under a recognized filename. The test can then simply assert that the image load was successful by ensuring that the loaded image is not null. The image must appear on the classpath.1

1 I've modified the build.xml Ant script for this section to copy any file in src/images into classes/images each time a compile takes place. This allows you to quickly remove the entire contents of the classes directory without having to worry about retaining any images.

image

This may seem weak, but it will provide an effective solution for now. You must ensure that the image courses.gif continues to stay in existence throughout the lifetime of the project. Instead of using an image file associcated with the SIS project, you might consider creating an image explicitly used for testing purposes.

Java provides several different ways to load and manipulate images. You will use the most direct.

image

The class Class defines the method getResource. This method allows you to locate resources, including files, regardless of whether the application is loaded from a JAR file, individual class files, or some other source such as the Internet.2 The result of calling getResource is a URL—the unique address of the resource.

2 Technically, the image is loaded using the class loader as that of the Class object on which you call getResource. For applications that do not use a custom class loader, using the class loader of the ImageUtil class will work just fine.

Once you have a proper URL, you can pass it to the ImageIcon constructor to obtain an ImageIcon object. You can use ImageIcon objects for various purposes, such as decorating buttons or labels. Since the Frame class requires an Image object, not an ImageIcon, here you use the getImage method to produce the return value for create.

Note that the image filename passed to getResource is /images/courses.gif—a path that uses a leading slash. This indicates that the resource should be located starting from the root of each classpath entry. Thus, if you execute classes from the directory c:swing2classes, you should put courses.gif in c:swing2classesimages. If you execute classes by loading them from a JAR file, it should contain courses.gif with a relative path of images.

Here are the changes to the initialize method in the Sis class.

image

You now have an acceptably pleasing interface—at least from a visual standpoint. The revised layout is shown in Figure 1.

Figure 1. A Good Look

image

Feel

The visual appearance of the interface is important, but more important is its “feel.” The feel of an application is what a user experiences as he or she interacts with it. Examples of elements relevant to the feel of an application include:

• ability to effect all behaviors using either the keyboard or the mouse

• tabbing sequence—can the user visit all text fields in the proper order and are irrelevant items omitted from the tab sequence?

• field limits—is the user restricted from entering too many characters?

• field constraints—is the user restricted from entering inappropriate data into a field?

• button activation—are buttons deactivated when their use is inappropriate?

It is possible to address both the look and feel at the same time. In the last section, you decorated the list of courses with a scroll pane. This improved the look of the interface and at the same time provided the “feel” to allow a user to scroll the list of courses when necessary.

Keyboard Support

One of the primary rules of GUIs is that a user must be able to completely control the application using either the keyboard or the mouse. Exceptions exist. Using the keyboard to draw a polygon is ineffective, as is entering characters using the mouse. (Both are of course possible.) In such cases, the application developer often chooses to bypass the rule. But in most cases it is extremely inconsiderate to ignore the needs of a keyboard only or mouse only user.

By default, Java supplies most of the necessary support for dual keyboard and mouse control. For example, you can activate a button by either clicking on it with the mouse or tabbing to it and pressing the space bar.

Button Mnemonics

An additional way you can activate a button is using an Alt-key combination. You combine pressing the Alt key with another key. The other key is typically a letter or number that appears in the button text. You refer to this key as a mnemonic (technically, a device to help you remember something; in Java, it's simply a single-letter shortcut). An appropriate mnemonic for the Add button is the letter A.

The mnemonic is a view class element. The specifications for the mnemonic remain constant with the view's appearance. Therefore the more appropriate place to test and manage the mnemonic is in the view class.

In CoursesPanelTest:

image

In CoursesPanel:

image

After seeing your tests pass, execute sis.ui.Sis as an application. Enter a course department and number, then press Alt-A to demonstrate use of the mnemonic.

Required Fields

A valid course requires both a department and course number. However, sis.ui.Sis allows you to omit either or both, yet still press the Add button. You want to modify the application to disallow this circumstance.

One solution would be to wait until the user presses Add, then ensure that both department and course number contain a nonempty string. If not, then present a message pop-up that explains the requirement. While this solution will work, it creates a more annoying user experience. Users don't want continual interruption from message pop-ups. A better solution is to proactively disable the Add button until the user has entered information in both fields.

You can monitor both fields and track when a user enters information in them. Each time a user presses a character, you can test the field contents and enable or disable the Add button as appropriate.

Managing enabling/disabling of the Add button is an application characteristic, not a view characteristic. It involves business-related logic, as it is based on the business need for specific data. As such, the test and related code belongs not in the panel class, but elsewhere.

Another indicator that the code does not belong in the view class is that you have interaction between two components. You want the controller to notify other classes of an event (keys being pressed) and you want those other classes to tell the view what to present (a disabled or enabled button) under certain circumstances. These are two separate concerns. You don't want logic in the view trying to mediate things.

To solve the bigger problem, however, it is easier to provide tests against CoursesPanel that prove each of the two smaller occurences. A first test ensures that a listener is notified when keys are pressed. A second test ensures that CoursesPanel can enable and disable buttons.

Start with a test (in CoursesPanelTest) for enabling and disabling buttons:

image

The code in CoursesPanel is a one-line reactive method—no logic:

image

The second test shows how to attach a keystroke listener to a field:

image

A KeyAdapter is an abstract implementation of the KeyListener interface that does nothing. The first line in the test creates a concrete subclass of KeyAdapter that overrides nothing. After adding the listener (using addFieldListener) to the panel, the test ensures that the panel properly sets the listener into the text field. The code in CoursesPanel is again trivial:

image

The harder test is at the application level. A listener in the Sis object should receive messages when a user types into the department and number fields. You must prove that the listener's receipt of these messages triggers logic to enable/disable the Add button. You must also prove that various combinations of text/no text in the fields results in the appropriate state for the Add button.

A bit of programming by intention in SisTest will provide you with a test skeleton:

image

The test ensures that the button is disabled by default. After typing values into the department and number fields, it verifies that the button is enabled. The trick, of course, is how to select a field and emulate typing into it. Swing provides a few solutions. Unfortunately, each requires you to render (make visible) the actual screen. Thus the first line in the test is a call to the show method of Sis.

The solution I'll present involves use of the class java.awt.Robot. The Robot class emulates end-user interaction using the keyboard and/or mouse. Another solution requires you to create keyboard event objects and pass them to the fields using a method on java.awt.Component named dispatchEvent.

You can construct a Robot object in the SisTest setUp method. (After building this example, I noted persistent use of the CoursesPanel object, so I also refactored its extraction to setUp.)

image

The selectField method isn't that tough:

private void selectField(String name) throws Exception {
   JTextField field = panel.getField(name);
   Point point = field.getLocationOnScreen();
   robot.mouseMove(point.x, point.y);
   robot.mousePress(InputEvent.BUTTON1_MASK);
   robot.mouseRelease(InputEvent.BUTTON1_MASK);
}

After obtaining a field object, you can obtain its absolute position on the screen by sending it the message getLocationOnScreen. This returns a Point object—a coordinate in Cartesian space represented by an x and y offset.3 You can send this coordinate as an argument to Robot's mouseMove method. Subsequently, sending a mousePress and mouseRelease message to the Robot results in a virtual mouse-click at that location.

3 The upper left corner of your screen has an (x, y) coordinate of (0, 0). The value of y increases as you move down the screen. For example, you would express two pixels to the right and one pixel down as (2, 1).

The type method is equally straightforward:

private void type(int key) throws Exception {
   robot.keyPress(key);
   robot.keyRelease(key);
}

The code in Sis adds a single listener to each text field. This listener waits on keyReleased events. When it receives one, the listener calls the method -setAddButtonState. The code in setAddButtonState looks at the contents of the two fields to determine whether or not to enable the Add button.

image

Note that the last line in createKeyListeners calls setAddButtonState in order to set the Add button to its default (initial) state.

The code in testKeyListeners doesn't represent all possible scenarios. What if the user enters nothing but space characters? Is the button properly disabled if one of the two fields has data but the other does not?

You could enhance testKeyListeners with these scenarios. A second test shows a different approach, one that directly interacts with setAddButtonState. This test covers a more complete set of circumstances.

image

The test fails. A small change to isEmpty fixes things.

private boolean isEmpty(String field) {
   String value = panel.getText(field);
   return value.trim().equals("");
}

Field Edits

When you provide an effective user interface, you want to make it as difficult as possible for users to enter invalid data. You learned that you don't want to interrupt users with pop-ups when requiring fields. Similarly, you want to avoid presenting pop-ups to tell users they have entered invalid data.

A preferred solution involves verifying and even modifying data in a text field as the user enters it. As an example, a course department must contain only uppercase letters. “CMSC” is a valid department, but “Cmsc” and “cmsc” are not. To make life easier for your users, you can make the department text field automatically convert each lowercase letter to an uppercase letter as the user types it.

The evolution of Java has included several attempts at solutions for dynamically editing fields. Currently, there are at least a half dozen ways to go about it. You will learn two of the preferred techniques: using JFormattedTextField and creating custom DocumentFilter classes.

You create a custom filter by subclassing javax.swing.text.DocumentFilter. In the subclass, you override definitions for any of three methods: insertString, remove, and replace. You use these methods to restrict invalid input and/or transform invalid input into valid input.

As a user enters or pastes characters into the text field, the insertString method is indirectly called. The replace method gets invoked when a user first selects existing characters in a text field before typing or pasting new characters. The remove method is invoked when the user deletes characters from the text field. You will almost always need to define behavior for insertString and replace, but you will need to do so only occasionally for remove.

Once you have defined the behavior for the custom filter, you attach it to a text field's document. The document is the underlying data model for the text field; it is an implementation of the interface javax.swing.text.Document. You obtain the Document object associated with a JTextField by sending it the message getDocument. You can then attach the custom filter to the Document using setDocumentFilter.

Testing the Filter

How will you test the filter? You could code a test in CoursesPanel that uses the Swing robot (as described in the Required Fields section). But for the purpose of testing units, the robot is a last-resort technique that you should use only when you must. In this case, a DocumentFilter subclass is a stand-alone class that you can test directly.

In some cases, you'll find that Swing design lends itself to easy testing. For custom filters, you'll have to do a bit of legwork first to get around a few barriers.

UpcaseFilterTest appears directly below. The individual test method testInsert is straightforward and easy to read, the result of some refactoring. In testInsert, you send the message insertString directly to an UpcaseFilter instance. The second argument to insertString is the column at which to begin inserting. The third argument is the text to insert. (For now, the fourth argument is irrelevant, and I'll discuss the first argument shortly).

Inserting the text "abc" at column 0 should generate the text "ABC". Inserting "def" at position 1 (i.e., before the second column) should generate the text "ADEFBC".

image

image

The setup is considerably more involved than the test itself.

If you look at the javadoc for insertString, you'll see that it takes a reference of type DocumentFilter.FilterBypass as its first argument. A filter bypass is essentially a reference to the document that ignores any filters. After you transform data in insertString, you must call insertString on the filter bypass. Otherwise, you will create an infinite loop!

The difficulty with respect to testing is that Swing provides no direct way to obtain a filter bypass object. You need the bypass in order to test the filter.

The solution presented above is to provide a new implementation of DocumentFilter.FilterBypass. This implementation stores a concrete instance of an AbstractDocument (which implements the Document interface) known as a PlainDocument. To flesh out the bypass, you must supply implementations for the three methods insertString, remove, and replace. For now, the test only requires you to implement insertString.

The insertString method doesn't need to take a bypass object as its first parameter, since it is defined in the filter itself. Its job is to call the document's insertString method directly (i.e., without calling back into the DocumentFilter). Note that this method can throw a BadLocationException if the start position is out of range.

Once you have a DocumentFilter.FilterBypass instance, the remainder of the setup and test is easy. From the bypass object, you can obtain and store a document reference. You assert that the contents of this document were appropriately updated.

The test (UpcaseFilterTest) contains a lot of code. You might think that the robot-based test would have been easier to write. In fact, it would have. However, robots have their problems. Since they take control of the mouse and keyboard, you have to be careful not to do anything else while the tests execute. Otherwise you can cause the robot tests to fail. This alone is reason to avoid them at all costs. If you must use robot tests, find a way to isolate them and perhaps execute them at the beginning of your unit-test suite.

Also, the test code for the second filter you write will be as easy to code as the corresponding robot test code. Both filter tests would require the documentText and createBypass methods as well as most of the setUp method.

Coding the Filter

You're more than halfway done with building a filter. You've completed the hard part—writing a test for it. Coding the filter itself is trivial.

image

When the filter receives the insertString message, its job in this case is to convert the text argument to uppercase, and pass this transformed data off to the bypass.

Once you've demonstrated that your tests all still pass, you can now code the replace method. The test modifications:

image

The test shows that the replace method takes an additional argument. The third parameter represents the number of characters to be replaced, starting at the position represented by the second argument. The production code:

image

UpcaseFilter is complete. You need not concern yourself with filtering removal operations when uppercasing input.

Attaching the Filter

You have proved the functionality of UpcaseFilter as a standalone unit. To prove that the department field in CoursesPanel transforms its input into uppercase text, you need only demonstrate that the appropriate filter has been attached to the field.

Should the code in CoursesPanel attach the filters to its fields, or should code in Sis retrieve the fields and attach the filters? Does the test belong in SisTest or in CoursesPanelTest? A filter is a combination of business rule and view functionality. It enforces a business constraint (for example, “Department abbreviations are four uppercase characters”). A filter also enhances the feel of the application by making it easier for the user to enter only valid information.

Remember: Keep the view class simple. Put as much business-related logic in domain (Course) or application classes (Sis). The filter representation of the business logic is very dependent upon Swing. The filters are essentially plugins to the Swing framework. You don't want to make the domain class dependent upon such code. Thus, the only remaining choice is the application class.

The code in SisTest:

image

The code in Sis:

image

A Second Filter

You also want to constrain the number of characters in both the department and course number field. In fact, in most applications that require field entry, you will want the ability to set field limits. You can create a second custom filter, LimitFilter. The following code listing shows only the production class. The test, LimitFilterTest (see the code at http://www.LangrSoft.com/agileJava/code/) contains a lot of commonality with UpcaseFilterTest that you can factor out.

image

Note the technique of having insertString delegate to the replace method. The other significant bit of code involves throwing a BadLocationException if the replacement string is too large.

Building such a filter and attaching it to the course number field is easy enough. You construct a LimitFilter by passing it the character length. For example, the code snippet new LimitFilter(3) creates a filter that prevents more than three characters.

The problem is that you can set only a single filter on a document. You have a couple of choices. The first (bad) choice is to create a separate filter for each combination. For example, you might have filter combinations -Upcase-LimitFilter and NumericOnlyLimitFilter. A better solution involves some form of abstraction—a ChainableFilter. The ChainableFilter class subclasses DocumentFilter. It contains a sequence of individual filter classes and manages calling each in turn. The code available at http://www.LangrSoft.com/agileJava/code/ for this lesson demonstrates how you might build such a -construct.4

4 The listing does not appear here for space reasons.

JFormattedTextField

Another mechanism for managing field edits is to use the class javax.swing.JFormattedTextField, a subclass of JTextField. You can attach formatters to the field to ensure that the contents conform to your specification. Further, you can retrieve the contents of the field as appropriate object types other than text.

You want to provide an effective date field for the course. This date represents when the course is first made available in the system. Users must enter the date in the format mm/dd/yy. For example, 04/15/02 is a valid date.

The test extracts the field as a JFormattedTextField, then gets a formatter object from the JFormattedTextField. A formatter is a subclass of javax.swing.JFormattedTextField.AbstractFormatter. In verifyEffectiveDate, you expect that the formatter is a DateFormatter. The DateFormatter in turn wraps a java.text.SimpleDateFormat instance whose format pattern is MM/dd/yy.5

5 The capital letter M is used for month, while the lowercase letter m is used for minutes.

The final part of the test ensures that the field holds on to a date instance. When the user clicks Add, code in sis.ui.Sis can extract the contents of the effective date field as a java.util.Date object.

image

Code in CoursesPanel constructs the JFormattedTextField, passing a SimpleDateFormat to its constructor. The code sends the message setValue to dateField in order to supply the Date object in which to store the edited results.

image

If you execute the application with these changes, you will note that the effective date field allows you to type invalid input. When you leave the field, it reverts to a valid value. You can override this default behavior; see the API documentation for JFormattedTextField for the alternatives.

A design issue now exists. The code to create the formatted text field is in CoursesPanel and the associated test is in CoursesPanelTest. This contrasts with the goal I previously stated to manage edits at the application level!

You want to completely separate the view and application concerns. A solution involves the single responsibility principle. It will also eliminate some of the duplication and code clutter that I've allowed to fester in CoursesPanel and Sis.

A Field object is a data object whose attributes describe the information necessary to be able to create Swing text fields. A field is implementation-neutral, however, and has no knowledge of Swing. A FieldCatalog contains the collection of available fields. It can return a Field object given its name.

The CoursesPanel class needs only contain a list of field names that it must render. The CoursesPanel code can iterate through this list, asking a FieldCatalog for the corresponding Field object. It can then send the Field object to a factory, TextFieldFactory, whose job is to return a JTextField. The factory will take information from the Field object and use it to add various constraints on the JTextField, such as formats, filters, and length limits.

The code for the new classes follows. I also show the code in CoursesPanel that constructs the text fields.

image

image

image

image

image

image

image

image

image

image

image

image

Notes:

TestUtil.assertDateEquals is a new utility method whose implementation should be obvious.

• I finally moved the DateUtil class from the sis.studentinfo package to the sis.util package. This change impacts a number of existing classes.

• You must also edit Sis and CoursesPanel (and tests) to remove constants and code for constructing filters/formatters. See http://www.LangrSoft.com/agileJava/code/ for full code.

• The Field class, which is omitted from these listings, is a simple data class with virtually no logic.

• You'll want to update the Course class to contain the new attribute, effective date.

Tables

The CoursesPanel JList shows a single string for each Course object. This presentation is adequate because you only display two pieces of data: the department and course number. Adding the new effective date attribute to the summary string, however, would quickly make the list look jumbled. The JList control, in fact, works best when you have only a single piece of data to represent per row in the list.

A JTable is a very effective control that allows you to present each object as a sequence of columns. A JTable can look a lot like a spreadsheet. In fact, JTable code and documentation uses the same terms as spreadsheets: rows, columns, and cells.

The JTable class gives you a considerable amount of control over look and feel. For example, you can decide whether or not you want to allow users to edit individual cells in the table, you can allow the user to rearrange the columns, and you can control the width of each column.

For this exercise, you'll replace the JList with a JTable. The best place to start is to create the data model that will underly the JTable. Just as you inserted Course objects into a list model for the JList, you will put Course objects into a model that you attach to the JTable.

Creating the JTable model is slightly more involved. The JList was able to get a printable representation by sending the toString message to each object it contained. The JTable must treat each attribute for a given object separately. For every cell it must display, the JTable sends the message getValueAt to the model. It passes the row and column representing the current cell. The getValueAt method must return a string to display at this location.

There would be no easy way for the model to figure out what attribute you want to display for a given column. As such, you must provide a model implementation yourself. You must supply the getValueAt method in this implementation, as well as two other methods: getRowCount and getColumnCount. To enhance the look and feel of the table, you will likely implement other methods. The test below, CoursesTableModelTest, shows that the model implements the method getColumnName to return a text header for each column.

image

image

The test presents another use for the FieldCatalog—returning an appropriate column header for each field. It uses a new Field attribute with the more abstract concept of a “short name,” an abbreviated name for use in -constrained display spaces. You'll need to update Field and FieldCatalog/FieldCatalogTest to supply this new information.

The easiest way to build a table model is to subclass javax.swing.table.AbstractTableModel. You then need only supply definitions for getValueAt, getRowCount, and getColumnCount. You will want to store a collection of courses in the model. You'll need a method (add) that allows adding a new Course to the model.

You can also choose to work with javax.swing.table.DefaultTableModel. Sun provided this concrete implementation to make your job a bit simpler. However, DefaultTableModel requires that you organize your data first (in the form of either Vector objects or Object arrays) and then pass it to the model. It's almost as easy, and ultimately more effective, to create your own AbstractTableModel subclass.

image

image

Note the subtle redundancies that abound. The table must contain a list of fields you are interested in displaying. In getValueAt, you obtain the field name at the column index provided. You use this name in a pseudo–switch statement to determine which Course getter method to call. A shorter bit of code would involve a switch statement:

switch (column) {
   case 0: return course.getDepartment();
   case 1: return course.getNumber();
   case 2: return formatter.format(course.getEffectiveDate());
   default: return "";
}

While it is acceptable, I dislike the disconnect between the column number and the attribute. Changes to the columns or their order can easily result in errors. (At least your tests would catch the problem.) But see the sidebar for a discussion of the solution I present.

You will, of course, need to make a few more changes to get the JTable working. In CoursesPanelTest:

image

In CoursesPanel:

image

You can remove the class CourseDisplayAdapter and any references to the old courses list.

Make sure you take a look at the Java API documentation for JTable and the various table model classes. The JTable class contains quite a bit of customization capabilities.

Feedback

Part of creating a good user experience is ensuring that you provide plenty of feedback. Sun has already built a lot of feedback into Swing. For example, when you click the mouse button atop a JButton, the JButton repaints to appear as if it were physically depressed. This kind of information reassures the user about his or her actions.

The Sis application lacks a pertinent piece of feedback: When the user enters one of the filtered or formatted text fields, how does he or she know what they're expected to type? The user will eventually discover the constraints they are under. But he or she will waste some time as they undergo guesswork, trial, and error.

You can instead provide your users with helpful information ahead of time. Several options exist:

• Put helpful information in the label for the field. Generally, though, there is not enough screen real estate to do so.

• Provide a separate online help window that describes how the application works.

• As the user moves the mouse over fields, display a small pop-up rectangle with relevant information. This is known as hover help, or tool tips. All modern applications such as Internet Explorer provide tool tips as you hover your mouse over toolbar buttons.

• As the user moves the mouse over fields, display relevant information in a status bar at the bottom of the window.

For this exercise, you will choose the last option and create a status bar.

Unfortunately, for mouse-based testing, you must usually render (display) the user interface in order to test it. The reason is that components do not have established sizes until they are rendered. You can again use the Swing robot to help you write your tests. Note that the setUp in this test uses a couple of Swing utility methods that I will display in subsequent listings.

image

image

Conceptually, providing status information is a common need for all of your application's windows. Instead of cluttering each window with additional code, you can encapsulate the status concept in a separate class.

StatusBar

A StatusBar is a JLabel with additional functionality. You can associate an information String with each text field by sending setInfo to a Status object.

The test extracts the location of the first field, then moves the mouse to an arbitrary position outside this field. The status bar should show nothing; the first assertion proves that. The test then moves the mouse over the field, then ensures that the status bar contains the expected text. The test finally asserts that the status bar text changes to TEXT2 when the mouse is over the second field.

The SwingUtil class extracts common code used to create a simple panel and a frame for testing:

image

In StatusBar, the job of setInfo is to add a mouse listener to the text field. The listener reacts to mouse entry and mouse exit events. When a user moves the mouse atop the text, listener code displays the associated information. When a user moves the mouse away from the text field, listener code clears the status bar.

image

The test for StatusBar passes. Now you must attach the status bar to CoursesPanel. How will you test this? Your test for CoursesPanel could use the robot again. But it would be easier to ensure that each text field has been attached to the status bar.

Update the test in CoursesPanelTest. The assertion declares a new intent: A StatusBar object should be able to return the informational text for a given text field. It also suggests that the informational text should come from the field spec object, obtained from the field catalog.

image

This will not compile because you have not implemented getInfo. Note also that you will need to associate a component name with the status bar. Here are the changes to StatusBarTest and StatusBar:

image

image

You must also add a field, getter, and setter to Field. You'll need to modify FieldCatalog to populate each field with a pertinent informational string:

image

Finally, the code in CoursesPanel to make it all work for the student information system:

image

If you execute sis.ui.Sis as a stand-alone application, you should now see something like Figure 2. I rested the mouse pointer above the department field when I captured this screen image.

Figure 2. The Completed Look

image

Responsiveness

In the Sis method addCourse, insert a deliberate wait of three seconds. This wait might emulate the time required to verify and insert the Course object into the database.

image

Execute the application. Enter a department and course number and press Add. You should experience a 3-second delay. During that time, you cannot do anything else with the user interface! For an end user, this is a frustrating experience, since there is no feedback about why the application is not responding.

As an application developer, you can do two things with respect to responsiveness: First, provide feedback to the user that they should wait patiently for a short period. Second, ensure that the user is able to do other things while waiting.

Feedback comes in the form of a “wait” cursor. Windows represents a wait cursor using an hourglass. Some Unix desktops represent a wait cursor using a clock. You should provide an hourglass for any operation that does not immediately return control to the user.

image

The finally block is essential! Otherwise, any unnoticed or abnormal return from addCourse will burden the user with an hourglass as a pointer.

Providing a wait cursor is an adequate and necessary response for any prolonged wait period. But the real solution is to ensure that the user does not have to wait. The threshold is a half a second: You should spawn a slow operation off in another thread if it is going to take any longer. In the example shown here, I've separated the behind-the-scenes operation of adding the course from the code that updates the user interface. I've also disabled the Add button until the thread completes.

image

A subtle but real problem exists with this code. It is not thread-safe! Since Swing uses a separate thread known as an event dispatch thread, it is possible for a user to click the Add button before the panel is completely updated. The user might see unexpected results.

You can rectify this by executing statements to update the user interface in the event dispatch thread. The class javax.swing.SwingUtilities contains two methods, invokeLater and invokeAndWait, to allow this. Each method takes a Runnable object that defines the code to execute in the event dispatch thread. You use invokeLater when you can allow the run method to execute asynchronously (when you don't need to “hold up” activities on the user interface). In our example, you want to use invokeAndWait, which results in the run method being executed synchronously.

Here's how addCourse might look using invokeAndWait:

image

The big downside is that this change will break the SisTest method testAddCourse. The test presumes that clicking on Add will block until the course has been added to the panel. As a quick fix, you can have the test wait until elements appear in the panel's table.

image

The change requires a small addition to CoursesPanel:

int getCourseCount() {
   return coursesTableModel.getRowCount();
}

Remaining Tasks

You've invested a considerable amount of code in this simple interface. Yet it's far from complete. Here is a list of some of the things you might consider coding to complete the interface:

• Clear the text fields when the user presses Add.

• Add a constraint that prevents the user from entering duplicate courses. You would code the logic to check for duplicates in CourseCatalog.

• Add delete button to remove courses. You might allow the user to select multiple rows for deletion.

• Add an update button to make edits to existing courses.

• Install the ability to sort the data in each column.

• Add a numeric filter to limit the user to entering only digits for the course number.

• Set each column width depending upon the average or maximum width of its contents.

• Add a hook that selects the contents of each field as the user tabs or clicks into it. This allows the user to replace the field's contents by simply typing.

• Add mouseover help. As the user moves the mouse over each field, show summary information in either the status bar or in a small pop-up “tool tip.”

• Add keyboard help. Respond to the F1 key by popping up help. (Obviously this is more involved and requires an understanding of how to build a help subsystem.)

• Replace the department and/or course number fields with a drop-down list (JComboBox).

• Add interaction with the preferences subsystem (see Additional Lesson III) to allow the application to “remember” the last position of each window.

I've no doubt left a few features off this list. Building a robust, sophisticated user interface is a lot of work. The absence of any of these features will severely cripple the effectiveness of your application. However, rather than you as a developer trying to figure out what you need, you should treat each of these features as customer requirements. Your customer team needs a qualified expert to design and detail the specifications for the user interface.6

6 This person can be a developer acting in the role of UI expert for the customer team. Do not, however, underestimate the importance of this role. Most developers, even those who have read a book or two on the topic, don't have a clue how to create an effective user experience.

Final Notes

A large number of Swing books exist. Many are very thick, suggesting that there is quite a bit to learn about Swing. Indeed, there is. In these short two chapters you have only scratched the surface of Swing.

However, you have seen the basics of how to construct Swing applications. You would be able to build a decent interface with this small bit of information. No doubt you will want to learn about more complex Swing topics, such as tree widgets and drag & drop. A bit of searching should provide you with what you need to know. As always, the Java API documentation is the best place to start. Often the API documentation will lead you to a Sun tutorial on the topic.

I hope you've learned the more important lesson in these two Swing chapters: how to approach building a Swing application using TDD. Unit-testing Swing is often viewed as too difficult, and many shops choose to forgo it altogether. Don't succumb to the laziness: The frequent result of not testing Swing code is the tendency for Swing classes to bloat with easily testable application or model code.

Looking at the resultant view class, CoursesPanel, you should have noticed that it is very small and simple. Other than layout, there is little complexity in the class; it does almost nothing. TDD or not, that is always the design goal in a user-interface application: to keep the application and/or business logic out of the view.

Using TDD has pushed you toward this goal. The basic rule of TDD is to test everything that can't possibly break. One way to interpret this rule is “test everything that you can and redesign everything else so it can't possibly break.” TDD has led you to create a small view class with very simple, discrete methods that you can easily test. You've also created dumb “reactive” methods that simply delegate their work to another trusted class. These methods can't break.

The shops that choose to not test their user interface classes always regret it. Business logic creeps into the application; application and business logic creeps into the view. The view code becomes a cluttered conglomerate of various responsibilities. Since tests aren't written for the view, a considerable amount of code goes untested. The defect rate rises.

Worse, the human tendency toward laziness takes over. Since no tests for the view exist, a developer often figures that the easiest way to get out of testing is to stuff the code into the view. “Yeah, creating a new class is a pain, and so is creating a new test class, so I'll just dump all the code in this mother Swing class.” Doing so is a slippery slope that quickly degrades your application.

In these past two chapters, you've learned some additional guidelines and techniques for building Swing applications using TDD:

• Ensure that your design separates application, view, and domain logic.

• Break down design even further by considering the Single-Responsibility Principle.

• Eliminate the otherwise excessive redundancies in Swing (e.g., use common methods to create and extract fields).

• Use listeners to test abstract reactions of the view (e.g., to ensure that clicking a button results in some action being triggered).

• Use mock classes to avoid Swing rendering.

• Don't test layout.

• Use the Swing robot, but only as a last resort.

Swing was not designed with unit-level testing in mind. Figuring out how to test Swing is a problem-solving exercise. Dig through the various Swing classes to find what you need. Sometimes they'll supply the hooks to help you test; other times they won't. You may need to think outside the box to figure out how to test things!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.100.40