Chapter 7. Java Beans

The official JavaSoft definition of a bean is:

“A Java Bean is a reusable software component that can be manipulated visually in a builder tool.”

This chapter explains what you need to know about beans in order to build them. We do not cover any of the various builder environments, such as SunSoft’s Java WorkShop, Borland’s JBuilder, IBM’s VisualAge, or Symantec’s Visual Café, that are designed to help you use beans in order to produce applications or applets more efficiently.

NOTE

NOTE

Beans should eventually also be scriptable in tools such as Netscape's Visual JavaScript (http://www.netscape.com/download/). Unfortunately, these kinds of tools which use simple scripting languages to control beans are at an even earlier stage in their development than the Java builder tools currently available

Why Beans?

Programmers coming from a Windows background (specifically, Visual Basic or Delphi) will immediately know why beans are so important. Programmers coming from an environment where the tradition is to “roll your own” for everything may not understand at once. Our experience is that even more is true: Programmers who do not come from a Visual Basic or Delphi background often find it hard to believe that Visual Basic is the most successful example of reusable objects in the programming universe. For example, the market for reusable Visual Basic and Delphi components last year was more than $400,000,000—far more than the market for Java tools and Java-based products. One reason for Visual Basic’s popularity becomes clear if you consider how you build a Visual Basic application. For those who have never worked with Visual Basic, here, in a nutshell, is how you do it:

  1. You build the interface by dropping components (called controls) in Visual Basic onto a form window.

  2. When you are done “drawing” the interface, the containing window and the controls (such as command buttons, text boxes, file dialog boxes, or any one of the more than 3,000 or so other controls that are out there) automatically recognize user actions such as mouse movements and button clicks.

Only after you finish designing the interface does anything like traditional programming occur. The way this works is that Visual Basic objects generally have:

  • Properties of the object such as height or width that you can set at design time or run time. You can set them at design time without any programming, often in a rather user friendly fashion.

  • Methods to perform actions that you code with at run time.

  • Events that the controls can respond to. You write code in an event procedure associated to the event (calling the methods of the components as needed).

For example, in Chapter 2 of Volume 1, we wrote a program that would display an image on a form. It took a little over a page of code. Here’s what you would do in Visual Basic to create a program with pretty much the same functionality.

  1. Add two controls to a window: an Image control for displaying graphics and a Common Dialog control for selecting a file.

  2. Set the Filter properties of the CommonDialog control so that only files that the Image control can handle will show up. This is done in what VB calls the Properties window, as shown in Figure 7-1.

    The Properties window in VB for an image application

    Figure 7-1. The Properties window in VB for an image application

Now, we need to write the three lines of VB code that will be activated when the project first starts running. This corresponds to an event called the Form_Load, so the code goes in the Form_Load event procedure. The following code pops up the File dialog box—but only files with the right extension are shown because of how we set the Filter property. After the user selects an image file, the code then tells the Image control to display it. All the code you need for this sequence looks like this:

Private Sub Form_Load() 
   On Error Resume Next 
   CommonDialog1.ShowOpen 
   Image1.Picture = LoadPicture(CommonDialog1.FileName) 
End Sub

That’s it. These three lines of code give essentially the same functionality as the 45 or so lines of Java code. Of course, in Visual Basic, you do have to locate and then drop down the components, and you have to set properties. But it is a lot easier to learn how to do that than it is to write code.

The point is that right now Java is still a tool used only by top-notch, object-oriented programmers. And, realistically, even such programmers are unlikely to be as productive as a good Visual Basic programmer is for a small- to medium-sized GUI-intensive application. Note that we do not want to imply that Visual Basic is a good solution for every problem. It is clearly optimized for a particular kinds of problems—UI-intensive Windows programs. In contrast, Java will always have a much wider range of uses. And the good news is that with the advent of JavaBeans, some of the benefits of Visual Basic-style application building are on the horizon for Java. We predict that in the near future:

  1. Beans with the same functionality as the most common Visual Basic controls will become readily available.

  2. Java Builder tools will become as easy to use as the Visual Basic environment is today.

NOTE

NOTE

Of course, once you have built a bean, you have the nice bonus that you will be able to use it everywhere. You can even, through the magic of the JavaSoft “Beans to ActiveX Bridge," use them in Visual Basic and Delphi (http://www.javasoft.com/beans/bridge).

The Bean-Writing Process

Most of the rest of this chapter shows you the techniques that you will use to write beans. Before we go into details, we give an overview of the process. First, we want to stress that writing a bean is not technically difficult—there are only a few new classes and interfaces for you to master. In fact, no new techniques are involved. (For example, to make a bean persistent, simply make sure it implements the Serializable interface.)

In particular, the simplest kind of bean is really nothing more than a Java class that follows some fairly strict naming conventions for its event listeners and its methods. Example 7-1 shows the code for an ImageViewer Bean that could give a Java-based builder environment the same functionality as the Visual Basic Image control that we mentioned in the previous section. (Except, of course, this bean can handle only GIF or JPEGs because of limitations in the Java Image class; compare the class to the Visual Basic Image control.)

Example 7-1. ImageViewerBean.java

import java.awt.*; 

public class ImageViewerBean extends Component 
{  public void setFileName(String f) 
   {  fileName = f; 
      image = Toolkit.getDefaultToolkit().getImage(fileName); 
      setSize(getPreferredSize()); 
      repaint(); 
   } 

   public String getFileName() 
   {  return fileName; 
   } 

   public void paint(Graphics g) 
   {  if (image == null) 
      {  Dimension d = getSize(); 
         g.drawRect(0, 0, d.width, d.height); 
      } 
      else 
         g.drawImage(image, 0, 0, this); 
   } 

   public Dimension getPreferredSize() 
   {  if (image == null) 
         return new Dimension(MINSIZE, MINSIZE); 
         return new Dimension(image.getWidth(null), 
         image.getHeight(null)); 
   } 

   private static final int MINSIZE = 50; 
   private Image image = null; 
   private String fileName = ""; 
}

When you look at this code, notice that it really doesn’t look any different from any other well-designed Java class. For example, all mutator methods begin with set, all accessor methods begin with get. (As you will soon see, using a standard naming convention is one way builder tools can get information about the properties and events your bean supports.)

In general, the properties of your bean will usually be the part of its internal state that can be programmed. In particular, just as with any well-designed class, you should only expose private data fields through accessor and mutator methods. When you do this using the standard naming convention that we will describe shortly, then these methods become the properties of your bean. Events that your bean supports are simply any event handling code that follows the Java 1.1 event model. If these are custom events, you’ll need to follow the requirements for adding custom event listeners to a class (see Chapter 8 of Volume 1 or the TimerBean example below for examples). Finally, the methods of your bean are any public methods that are not associated to a property or an event.

VB NOTE

VB NOTE

Unfortunately, the JavaSoft people decided not to introduce a Property or Event keyword like VB5 has. You simply use setX and getX for a property named X and trust to Java to realize that they are properties. (See the section on "Design Patterns" a little later on in this chapter.)

The real problems come from what you need to do to make beans reusable by a large class of users. In particular, if you are interested in developing beans for commercial use (either for in-house use or for sale to other programmers), you need to keep two (possibly conflicting) points in mind.

  • FirstBeans may need to be usable by less than expert programmers. Such people will access the functionality of your bean either with a scripting language or with a visual design tool that uses essentially no programming language at all. (The connect-the-dots approach to application development seems to be growing more popular.)

    If the users of your beans want to script them by using JavaScript or VBScript on a Web page, your beans must be full featured enough to be worthwhile, while not being too complex for a naïve user to understand. For example, based on the Visual Basic component market, your naïve users will tend to think of:

    • Properties as things that describe what a bean is

    • Methods as describing what your bean can do

    • Events as being what actions the bean is aware of and can respond to

  • SecondA full-featured bean that can be used by either a naïve user or a professional developer often turns out to be quite tedious to code—useful components must deal with the complexity of the real world. Of course, that is why people can charge hundreds of dollars for a professional component, such as a full-featured chart control. For example, the prototypical VB chart control has

    • 60 properties

    • 47 events

    • 14 methods

    • 178 pages of documentation

      Obviously, a chart control is about as full featured a control as one can imagine, but even a bean as simple as an Integer text field turns out to be not quite so simple to code as one might first imagine.

      Consider, for example, what should happen as the user types digits into the text field. Should the new numbers be immediately reported to the listeners? On the one hand, this approach is attractive because it gives instant feedback to the user of your bean. But many applications restrict the range for valid entries, and partial entries that can go into an eventual valid entry might not themselves be valid. This restriction is particularly important for beans because the bean specification allows a listener for a property change in your bean to veto an invalid input, as you will shortly see.

      For example, suppose a listener vetoed all odd numbers. If a user tried to enter 256, the input 25 would be vetoed, and the user could never get past the veto. So, in this case, incremental update is a bad idea because the incremental value would be vetoed even though the eventual entry would not have been. In cases like this, your bean should report the value to listeners only when the user has finished entering the number, that is, when the user presses ENTER or if the text field loses focus.

      If you are writing a program for a single application, then you will simply choose one behavior or the other. But if you are building a bean, you aren’t programming for yourself but for a wide audience of users with different needs. Therefore, it would be best to add a property called something like incrementalUpdate and implement both behaviors, depending on the value of that property. Add a few more options like this, and even a simple IntTextBean will end up with a dozen properties.

Fortunately, you need to master only a small number of concepts to write beans with a rich set of behavior. The example beans in this chapter, while not trivial, are kept simple enough to illustrate the necessary concepts.

TIP

You will find full-featured beans available for sale from many programming product stores, bundled with Java development environments, or for free at sites such as www.gamelan.com. Also, all the AWT components in Java 1.1 are beans, but, more importantly, all the components in the JFC ("swing set") are beans. Using the swing set components as a base class for your own beans gives you a great starting point. These components are quite a bit slicker and more sophisticated than the simple AWT controls that are supplied with Java 1.1. As we write this, the swing set is in beta, but it is freely available after you register (also free) at the Java Developer connection (developer.javasoft.com).

The BDK and the BeanBox

Before we get into the mechanics of writing beans, we want you to see how you might use or test them. (The ImageViewerBean is a perfectly usable bean, but outside a builder environment it can’t show off any of its special features. In particular, the only way to use it in an ordinary Java program would be to write a class that instantiates it and calls the setFileName method.) So, the question is how can we show off the power of this simple bean? This question, in turn, leads to the question of what bean builder tool to use. Our current opinion is that no bean environment that we are familiar with seems mature enough to recommend unconditionally. Instead, we use the least common denominator environment, the so-called BeanBox, that is part of JavaSoft’s Bean Developer Kit (or BDK). The BeanBox is not very full featured and is certainly flaky at times, but it is available on many platforms and it lets you test simple beans. Moreover, if you get your bean working in the BeanBox, then it should work in other development environments without any changes.

NOTE

NOTE

At this time, the BDK installation is separate from the JDK installation. You may need to obtain it from the Java Web site (java.sun.com). Be sure that you have installed it before going any further.

Using the BeanBox

By default, the BDK is installed into a directory bdk. The BeanBox is automatically installed into a subdirectory called BeanBox. This directory has a batch file (run.bat ) to start up the BeanBox under Windows and a shell script (run.sh ) for Unix systems. When you start up the BeanBox, you’ll see a message box that looks like Figure 7-2. (As you will soon see, JAR files are how beans are packaged for use.)

The Analyzing Jars message box

Figure 7-2. The Analyzing Jars message box

The actual BeanBox consists of three independently resizable and movable windows, as shown in Figure 7-3. The Toolbox lists all the beans that the BeanBox knows about. The default installation of the BDK comes with 16 beans. (See below for how to add new beans to the BeanBox.) As you can see in Figure 7-3, some beans have associated icons that show up in the left side of the Toolbox as well. The window marked BeanBox is where you will drop the beans and where you will (try to) connect them so that events in one trigger actions in another. The window marked Properties - BeanBox is where you control the properties of your beans. This is a vital part of component-based development tools since setting properties at design time is how you set the initial state of a component.

The parts of the BeanBox

Figure 7-3. The parts of the BeanBox

For example, you can reset the name property of the Panel used for the BeanBox by simply typing in the text box in the window marked Properties-BeanBox. (This type of window for setting the properties of a bean in the BeanBox is often called a Property Sheet.) Changing the name property is simple—you just edit a string in a text field. But properties don’t have to be strings; they can be values of any Java type. To make it possible for users to set values for properties of any type, builder tools access specialized property editors. (Property editors either come with the builder or are supplied by the bean developer. You’ll see how to write your own property editors later in this chapter.)

To see a simple property editor at work, try to reset the background color of the BeanBox window as follows:

  1. Click in the box marked background in the Property Sheet. This brings up a property editor for editing objects of type Color, as shown in Figure 7-4.

    Using a property editor to set properties

    Figure 7-4. Using a property editor to set properties

  2. Choose a new color either by entering new hex digits in the first box or by making a selection via the choice box in the editor.

Notice that you’ll immediately see the change to the background color.

VB NOTE

VB NOTE

What JavaSoft calls a Property Sheet is analogous to the Properties window; what VB calls a Property Sheet is analogous to what JavaSoft calls a customizer (see the section on them below).

Using a Bean in the BeanBox

Using a bean that the BeanBox knows about is not hard:

  1. Select the bean in the Toolbox. (The cursor will become a crosshair).

  2. Click on the spot in the BeanBox window where you want the bean to go.

The user interface is not very clever. It neither gives you a clue as to which bean you selected nor lets you cancel the operation once you start it. Once you have a bean in the BeanBox window, click in it to select it. A hatched border indicates that the bean is selected (see Figure 7-5—the bottom button is the one selected).

A selected bean versus an unselected bean

Figure 7-5. A selected bean versus an unselected bean

You can move selected beans around the BeanBox window by:

  1. Moving the mouse cursor to the boundary. (You may have to try various spots—you’ll know you are at the right place when the cursor changes to a four-headed arrow, as shown in Figure 7-6.)

    A bean ready for moving

    Figure 7-6. A bean ready for moving

  2. Dragging the bean to the new location.

Similarly, you can resize most beans by moving the cursor to one of the corners (it will change to a two-sided arrow) and dragging the boundary of the bean to be the size that you want.

TIP

To remove a bean that you inadvertently placed on the BeanBox window:

  1. Select the bean.

  2. Choose Edit|Cut.

Building a Simple Application in the BeanBox

We explain in the section on Introspection near the end of this chapter how builder tools such as the BeanBox can know what properties a bean supports or what events it triggers. For now though, we simply want to show you that it all works. For this, we will follow tradition and hook up Start and Stop buttons to the juggling, tooth-shaped character shown in the Juggler bean. Here are the steps to follow (see Figure 7-7 for what the results might look like):

The BeanBox window for our Stop/Start Juggler application

Figure 7-7. The BeanBox window for our Stop/Start Juggler application

  1. Add two “Explicit“ beans to the BeanBox window.

  2. Set the Label property of one to Start and the other to Stop.

  3. Add the Juggler bean to the BeanBox. (Notice that it immediately starts juggling.)

Since the Juggler is already annoyingly juggling, let’s hook up the Stop button first.

  1. Select the Stop button.

  2. Choose Edit|Events|button push (see Figure 7-8). Choose the actionPerformed event from this submenu.

    The possible events for the Stop Button window for our Stop/Start Juggler application

    Figure 7-8. The possible events for the Stop Button window for our Stop/Start Juggler application

Now, when you move the mouse cursor on the BeanBox window, you’ll see a red line growing from the Stop button. (See Figure 7-9.)

The marker for an event hookup

Figure 7-9. The marker for an event hookup

Now, choose the Juggler bean. You’ll see a window like that shown in Figure 7-10 where you can select the method you want to hook up to the actionPerformed event.

The target method dialog

Figure 7-10. The target method dialog

We wanted to stop the juggling, so just choose stopJuggling in the dialog box shown in Figure 7-10. After you click on OK, the BeanBox will generate the needed adapter code. (Look inside the tmpsunweanbox directory below where the BeanBox is stored.)

Here is what the code looks like:

// Automatically generated event hookup file. 

package tmp.sunw.beanbox; 
import sunw.demo.juggler.Juggler; 
import java.awt.event.ActionListener; 
import java.awt.event.ActionEvent; 

public class ___Hookup_146ca316c4 implements 
java.awt.event.ActionListener, java.io.Serializable {

   public void setTarget(sunw.demo.juggler.Juggler t) {
      target = t; 
   } 

   public void actionPerformed(java.awt.event.ActionEvent 
         arg0) {
      target.stopJuggling(arg0); 
   } 

   private sunw.demo.juggler.Juggler target; 
}

Notice that the adapter class is given an absurd name, but the key is that the actionPerformed method calls the stopJuggling method of the Juggler class. Once you have called this class, clicking the Stop button really does stop the Juggler.

Saving and Restoring the State of the BeanBox

As you might expect, the BeanBox uses object serialization (Chapter 1) to save the state of your BeanBox.

NOTE

NOTE

It is essential that a builder tool be able to save the state of a bean (such as the current property settings), so design your beans so that their current state is serializable (see Chapter 1).

To save the state of an application that you are testing in the BeanBox, choose File|Save and fill in the File dialog box as desired. To restore the state of the BeanBox, make sure the BeanBox is clear of any beans (choose File|Clear). then choose File|Open to reload the previous application.

Building an Applet from the BeanBox

If you finish hooking up the Start button to the Juggler as well, you might want to see this application in a Browser, like HotJava or Internet Explorer 4, that has the Java 1.1 event model enabled. This is how it is supposed to work:

  1. Choose File|Make Applet.

  2. In the Dialog Box (Figure 7-11) that pops up, you can give the JAR file a new name or simply accept the default choice.

    The applet creation dialog

    Figure 7-11. The applet creation dialog

That’s it. The BeanBox will generate the needed HTML code (myApplet.HTML is the default name). You can load the HTML file in your favorite Java 1.1 event-enabled browser or the applet viewer (see Figure 7-12).

The connected beans as an applet in the applet viewer

Figure 7-12. The connected beans as an applet in the applet viewer

Adding Beans to the Toolbox

The first step in making any bean usable (whether in the BeanBox or any builder tool) is to package all class files that are used by the bean code into a JAR file. (See Chapter 10 of Volume 1 for more on JAR files). Unlike the JAR files for an applet that you saw previously, a JAR file for a bean needs a manifest file that specifies which class files in the archive are beans and should be included in the Toolbox. For example, here is the manifest file ImageViewerBean.mf for the ImageViewerBean:

Name: ImageViewerBean.class 
Java-Bean: True

If your bean contains multiple class files, you do not need to mention them in the manifest unless they are also beans that you want to have displayed in the Toolbox.

To make the JAR file, follow these steps:

  1. Edit the manifest file.

  2. Gather all needed class files in a directory.

  3. Run the jar tool as follows:

    jar cfm JarFile ManifestFile *.class

    For example,

    jar cfm ImageViewerBean.jar ImageViewerBean.mf *.class

You can also add other items, such as GIF files, to the JAR file.

TIP

If you prefer to build a make file to create the needed jar file, look at the demo directory below the bdk directory for some sample make (.mk ) files.

Finally, to make the BeanBox’s Toolbox aware of your JAR file that contains the bean, copy the JAR file to the jars subdirectory of the BDK or choose File|Load Jar in the BeanBox window.

Building an Image Viewer Application via Beans

We now have shown you enough BeanBox techniques so that you can use the two Java beans on the CD that give you the equivalent of the Visual Basic ImageViewer program. To see these beans at work, add the ImageViewerBean and the FileNameBean JAR files to the BeanBox by choosing File|LoadJar, or by copying the JAR file to the jars subdirectory of the bdk directory.

NOTE

NOTE

You have already seen the code for the ImageViewerBean. The code for the FileNameBean is a lot more sophisticated. We'll analyze it in depth later in this chapter. For now, all you have to know is that clicking on the ellipsis will open a standard File Open dialog box where you can enter the name of the file.

If you add the ImageViewerBean, then, as you can see from Figure 7-13, you can choose a file name for a GIF or JPEG file. Once you do so, the ImageViewerBean automatically displays the bean.

The ImageViewerBean at work

Figure 7-13. The ImageViewerBean at work

To imitate the functionality of the Visual Basic program, we don’t want to hook up events, as we did for the juggler. Instead, we link together the respective fileName properties of the beans. The idea is that a change in the fileName property of the FileNameBean is immediately reflected in the current value of the fileName property of ImageViewerBean. (Of course, this actually happens through a propertyChange event; we discuss these kinds of events a little later in this chapter.) This name change, in turn, results in the ImageViewerBean immediately displaying the right image—exactly as the VB program did.

NOTE

NOTE

We describe the way it is supposed to work in the BeanBox. Be sure to use the most updated version of the BeanBox. Some older versions do not update the image, some versions do not update the Property Sheet, and, unfortunately, some do neither.

To hook up the properties in the respective beans:

  1. Select the FileNameBean.

  2. Choose Edit|Bind Property.

  3. In the resulting dialog box that pops up (Figure 7-14), choose the filename property.

    The Bind PropertyNameDialog box for the FileNameBean

    Figure 7-14. The Bind PropertyNameDialog box for the FileNameBean

  4. Click on OK.

Now, the magic red line should appear, and you stretch it to reach the ImageViewerBean.

NOTE

NOTE

You can't see the ImageViewerBean when you are trying to bind the properties of the FileNameBean to it. Rest assured, though, it is still there—you can see the red line stop increasing in size when it hits the ImageViewerBean —even if the BeanBox is behaving badly by not showing it to you. We could have overcome this problem by setting the background color of the bean to, say, pink, but this is an issue only for the BeanBox. Other bean-building environments would show the outlines of the beans.

Click when the magic line reaches to the ImageViewerBean. The BeanBox will then pop up another dialog box (see Figure 7-15).

The PropertyNameDialog box for the ImageViewerBean

Figure 7-15. The PropertyNameDialog box for the ImageViewerBean

At this point, the two fileName properties are hooked up, so you can use the FileNameBean to update the image by clicking on the ellipsis and choosing the name of a GIF or JPEG file (see Figure 7-16).

The result of hooking up the fileName properties

Figure 7-16. The result of hooking up the fileName properties

“Design Patterns” for Bean Properties and Events

First, we want to stress there is no cosmic base beans class that you extend to build your beans. Visual beans obviously have to extend from Component or a subclass of Component, but nonvisual beans can simply extend implicitly from Object. Remember, a bean is simply any class that can be manipulated in a visual design tool. The design tool does not look at the base class to determine the bean nature of a class, but it analyzes the names of its methods. To enable this analysis, the method names for beans must follow a certain pattern.

NOTE

NOTE

There is a java.beans.Beans class, but all methods in it are static. Extending it would, therefore, be rather pointless, even though you will see it done occasionally, supposedly for greater "clarity." Clearly, since abean can't both extend Beans and Component, this approach can't work for visual beans. In fact, the Beans class contains methods that are designed to be called by builder tools, for example, to check whether the bean is being used at design time or run time.

Other languages for visual design environments, such as Visual Basic and Delphi, have special keywords such as “Property” and “Event “to express these concepts directly. The designers of Java decided not to add keywords to the language in order to support visual programming. Therefore, they needed an alternative so that a builder tool could analyze a bean to learn its properties or events. The first method they suggest that builder tools should use is based on the bean writer using a standard naming pattern for properties and events. If you use a standard naming pattern for your properties and events, then the builder tool can use the powerful reflection mechanism added to Java 1.1 (see Chapter 5 of Volume 1) to understand what properties and events the bean is supposed to expose.

NOTE

NOTE

Although the JavaBeans documentation calls this standard naming practice "Design patterns," these are really only naming conventions and have nothing to do with the term as used in OOP theory books.

The naming convention to use for properties so that a builder tool can use reflection to discover them automatically is simple:

  • Any pair of methods

    X getPropertyName() 
    void setPropertyName(X x)

    corresponds to a read/write property of type X .

For example, in our ImageViewerBean, there is only one read/write property (for the file name to be viewed), with the following methods:

public void setFileName(String f) 
public String getFileName()

Note that if you have a get method but not an associated set method, you define a read-only property.

Be careful with the capitalization pattern you use for your method names. JavaSoft decided that the name of the property in our example would be f ileName, with a lowercase f, even though the get and set methods contain an uppercase F (get F ileName, set F ileName ). The bean analyzer performs a process called decapitalization to derive the property name. (That is, the first character after get or set is converted to lower case.) The JavaBeans designers felt that this process would result in method and property names that are more natural to Java programmers.

NOTE

NOTE

The get and set methods you create can do more than simply get and set a private data field. Like any Java method, they can carry out arbitrary actions. For example, notice that the setFileName method of the ImageViewerBean class not only sets the value of the fileName data field, but it also opens the file, loads the image, and displays the image.

VB NOTE

VB NOTE

In VB, properties also come from get and set methods. (Delphi uses read and write.) But, as we already mentioned, in both these languages there is a Property keyword, which allows the the compiler to not have to second-guess the programmer's intentions by analyzing method names. And using a keyword in those languages has another advantage: Using a property name on the left-hand side of an assignment automatically calls the set method. Using a property name in an expression automatically calls the get method. For example, in VB you can write

imageBean.fileName = "corejava.gif"

instead of

imageBean.setFileName("corejava.gif");

There is one exception to the get/set naming pattern. Properties that have Boolean values should use a naming convention as in the following examples:

public bool isPropertyName() 
public void setPropertyName(boolean b)

For example, an animation might have a property running, with two methods

public boolean isRunning() 
public void setRunning(boolean b)

The setRunning method would start and stop the animation. The isRunning method would report its current status. (The juggler bean does not have such a property, however.)

For events, the naming conventions are even simpler. A bean builder environment will infer that your bean generates events when you supply methods to add and remove event listeners. For example, suppose your bean generates events of type EventName Event. (All events must end in Event.) Then, the listener interface must be called EventName Listener, and the methods to add and remove a listener must be called

public void addEventNameListener(EventNameListener e) 
public void removeEventNameListener(EventNameListener e)

If you look at the code for the ImageViewerBean, you’ll see that it has no events to expose. Later, we will see a timer bean that generates TimerEvent objects and has the following methods to manage event listeners:

public void addTimerListener(TimerListener e) 
public void removeTimerListener(TimerListener e)

Writing Bean Properties

A sophisticated bean will have lots of different kinds of properties that it should expose in a builder tool for a user to set at design time or get at run time. It can also trigger both standard and custom events. Properties can be as simple as the file name property that you saw in the ImageViewerBean and the FileNameBean or as sophisticated as a color value or even an array of data points—we encounter both of these cases later in this chapter. Obviously, the more sophisticated a property, the more important it is that you give your users a friendly way to set it. Later in this chapter, we show you beans that exhibit all the various ways of letting users set properties. These examples should provide reasonable models on which you can make property settings for your own beans user friendly. Before we do that, though, you should first be familiar with the kinds of properties beans can support. The next few sections give examples of custom beans that show off the possible kinds of properties.

Getting your properties of your beans right is probably the most complex part of building a bean because the model is quite rich. The beans specification (http://java.sun.com/beans/) allows four types of properties, which we illustrate by various examples.

Simple Properties

A simple property is one that takes a single value such as a string or a number. The fileName property of the ImageViewer is an example of a simple property. Simple properties are easy to program: just use the set/get naming convention we indicated earlier. For example, if you look at the code in Example 7-1, you can see that all it took to implement a simple string property is:

public void setFileName(String f) 
{  fileName = f; 
   image = Toolkit.getDefaultToolkit().getImage(fileName); 
   setSize(getPreferredSize()); 
   repaint(); 
} 

public String getFileName() 
{  return fileName; 
}

Notice that, as far as the JavaBeans specification is concerned, we also have a read-only property of this bean because we have a method with this signature inside the class:

public Dimension getPreferredSize()

without a corresponding setPreferredSize method. You would not normally be able to see read-only properties at design time in a Property Sheet.

TIP

In the version of the BeanBox we are working with, you have to initialize private data members explicitly to non-null values, or the BeanBox simply won't display them, leaving you to wonder whether you misspelled something. For example,

public String getProp() { return prop; } 
public void setProp(String p) { prop = p; ) 
. . . 
private String prop = ""; 
   // if not set, BeanBox won't display prop

Problems such as this one make programming beans decidedly unpleasant. If you make any mistake at all, a part of your bean will silently fail, and you have no idea what the problem is or whether you just ran up against a limitation of the BeanBox. Of course, if you use a bean development environment, more of the routine code is generated automatically and there is less room for error.

Indexed Properties

An indexed property is one that gets or sets an array. A chart bean (see below) would certainly use an indexed property for the data points. With an indexed property, you supply two pairs of get and set methods: one for the array and one for individual entries. They must follow the pattern

X[] getPropertyName() 
void setPropertyName(X[] x) 
X getPropertyName(int i) 
void setPropertyName(int i, X x)

Here’s an example of the indexed property we use in the chart bean that you will see later in this chapter.

public double[] getValues() { return values; } 
public void setValues(double[] v) { values = v; } 
public double getValues(int i) { return values[i]; } 
public void setValues(int i, double v) { values[i] = v; } 
. . . 
private double[] values;

The get/set functions that set individual array entries can assume that i is within the legal range. Builder tools will not call them with an index that is less than 0 or larger than the length of the array. In particular, the

setPropertyName(int i, X[] x)

method cannot be used to grow the array. To grow the array, you must manually build a new array and then pass it to this method:

setPropertyName(X[] x)

As a practical matter, however, the get and set methods can still be called programmatically, not just by the builder environment. Therefore, it is best to add some error checking, after all. For example, here is the code that we really use in the ChartBean class:

public double getValues(int i) 
{  if (0 <= i && i < values.length) return values[i]; 
   return 0; 
} 

public void setValues(int i, double value) 
{  if (0 <= i && i < values.length) values[i] = value; 
}

NOTE

NOTE

As we write this, the BeanBox's Property Sheet does not support indexed properties. That is, they don't show up on the Property Sheet, and you can't set them in the builder. You will see later in this chapter how to overcome this limitation by supplying a property editor for arrays.

Bound Properties

Bound properties tell interested listeners that their value has changed. For example, the fileName property in the FileNameBean is a bound property. When the file name changes, then the ImageViewerBean is automatically notified and it loads the new file. To implement a bound property, you must implement two mechanisms.

  1. Whenever the value of the property changes, you must fire a PropertyChange event to all registered listeners. This change can occur when the set method is called or when the program user carries out an action, such as editing text or selecting a file.

  2. To enable interested listeners to register themselves, the bean has to implement the following two methods:

    void addPropertyChangeListener(PropertyChangeListener l) 
    void removePropertyChangeListener(PropertyChangeListener l)

The java.beans package has a convenience class, called PropertyChangeSupport, that manages the listeners for you. To use this convenience class, your bean must have a data field of this class that looks like this:

private PropertyChangeSupport pcs = new 
   PropertyChangeSupport(this);

You delegate the task of adding and removing property change listeners to that object.

public void addPropertyChangeListener(PropertyChangeListener l) 
{  pcs.addPropertyChangeListener(l); 
} 

public void removePropertyChangeListener(PropertyChangeListener 
   l) 
{  pcs.removePropertyChangeListener(l); 
}

Whenever the value of the property changes, use the firePropertyChange method of the PropertyChangeSupport object to deliver an event to all the registered listeners. That method has three parameters: the name of the property, the old value, and the new value. For example,

pcs.firePropertyChange("fileName", oldValue, newValue);

The values must be objects. If the property type is not an object, then you must use an object wrapper. For example,

pcs.firePropertyChange("running", new Boolean(false), new 
   Boolean(true));

Other beans that want to be notified when the property value changes must implement the PropertyChangeListener interface. That interface contains only one method:

void propertyChange(PropertyChangeEvent evt)

The code in the propertyChange method is triggered whenever the property value changes, provided, of course, that you have added the recipient to the property change listeners of the bean that generates the event. The PropertyChangeEvent object encapsulates the old and new value of the property, obtainable via

Object oldVal = evt.getOldValue(); 
Object newVal = evt.getNewValue();

If the property type is not a class type, then the returned objects are the usual wrapper types. For example, if a boolean property is changed, then an Integer is returned and you need to retrieve the boolean value with the booleanValue method.

Thus, a listening object must follow this model:

class Listener 
{  public Listener() 
   {  bean.addPropertyChangeListener(this); 
   } 
   void propertyChange(PropertyChangeEvent evt) 
   {  Object newVal = evt.getNewValue(); 
      . . . 
   } 
   . . . 
}

Now, you may be wondering how the ImageViewerBean got notified when the file name in the FileNameBean changed. After all, there is no listener method in the ImageViewerBean source code. In this case, the BeanBox registered itself as a property change listener to the FileNameBean, and it called the setFileName method of the ImageViewerBean in its propertyChange listener method.

Here’s the full code for the FileNameBean :

Example 7-2. FileNameBean.java

import java.awt.*; 
import java.awt.event.*; 
import java.beans.*; 

public class FileNameBean extends Panel 
   implements ActionListener 
{  public FileNameBean() 
   {  setLayout(new GridBagLayout()); 
      GridBagConstraints gbc = new GridBagConstraints(); 
      gbc.weightx = 100; 
      gbc.weighty = 100; 
      gbc.anchor = GridBagConstraints.WEST; 
      gbc.fill = GridBagConstraints.HORIZONTAL; 
      add(nameField, gbc, 0, 0, 1, 1); 
      dialogButton.addActionListener(this); 
      nameField.setEditable(false); 
      gbc.weightx = 0; 
      gbc.anchor = GridBagConstraints.EAST; 
      gbc.fill = GridBagConstraints.NONE; 
      add(dialogButton, gbc, 1, 0, 1, 1); 
   } 

   public void add(Component c, GridBagConstraints gbc, 
      int x, int y, int w, int h) 
   {  gbc.gridx = x; 
      gbc.gridy = y; 
      gbc.gridwidth = w; 
      gbc.gridheight = h; 
      add(c, gbc); 
   } 

   public void actionPerformed(ActionEvent evt) 
   {  if (evt.getSource() == dialogButton) 
         showFileDialog(); 
   } 

   public void showFileDialog() 
   {  if (fileDialog == null) 
      {  Container c = getParent(); 
         while (c != null && !(c instanceof Frame)) 
         {  c = c.getParent(); 
         } 

         if (c != null) 
            fileDialog = new FileDialog((Frame)c, title); 
      } 
      if (fileDialog == null) return; 
      fileDialog.setFile(defaultExtension); 
      fileDialog.setDirectory(lastDir); 
      fileDialog.show(); 
      String f = fileDialog.getFile(); 
      lastDir = fileDialog.getDirectory(); 
      if (f != null) 
      {  setFileName(lastDir + f); 
      } 
   } 

   public void setFileName(String newValue) 
   {  String oldValue = nameField.getText(); 
      pcs.firePropertyChange("fileName", oldValue, newValue); 
      nameField.setText(newValue); 
   } 

   public String getFileName() { return nameField.getText(); } 

   public Dimension getMinimumSize() 
   {  return new Dimension(XMINSIZE, YMINSIZE); 
   } 

   public void addPropertyChangeListener 
      (PropertyChangeListener l) 
   {  pcs.addPropertyChangeListener(l); 
   } 

   public void removePropertyChangeListener 
      (PropertyChangeListener l) 
   {  pcs.removePropertyChangeListener(l); 
   } 

   public String getDefaultExtension() 
   {  return defaultExtension; 
   } 

   public void setDefaultExtension(String s) 
   {  defaultExtension = s; 
   } 

   public String getTitle() { return title; } 
   public void setTitle(String s) { title = s; } 

   private static final int XMINSIZE = 100; 
   private static final int YMINSIZE = 20; 
   private Button dialogButton = new Button("..."); 
   private TextField nameField = new TextField(""); 
   private FileDialog fileDialog; 
   private int mode = FileDialog.LOAD; 
   private String defaultExtension = "*.*"; 
   private String title = ""; 
   private String lastDir = ""; 
   private PropertyChangeSupport pcs 
      = new PropertyChangeSupport(this); 
}

Constrained Properties

A constrained property is the most interesting of the properties (and, therefore, the most painful to implement). The idea is that these are properties of your bean such that certain state changes can be “vetoed” by the various users of your bean. For example, consider a text field to enter a number, implemented as a bean IntTextBean. The consumer of that number may have restrictions on the value; for example, perhaps the number should be between 0 and 255. Such a restriction would be easy to implement: add a minValue and maxValue to the IntTextBean. However, the restriction may be more complex than that. Perhaps the number should be even. Or it may depend on another number that also changes. In that case, no simple property of the IntTextBean is able to distinguish good user inputs from bad ones. Instead, the bean must notify its consumers of the new value and give the consumers the chance to veto the change. A property that can be vetoed is called a constrained property.

We will put this concept to use with a range bean (see Figure 7-17). This bean contains two IntTextBean fields to specify the lower and the upper bound of a range. (You might use such a bean in a print dialog where the user can specify the range of pages to be printed.) If the user enters a to value that is less than the current from value (or a from value that is greater than the current to value), then the RangeBean vetoes the change.

The range bean

Figure 7-17. The range bean

In this example, the IntTextBean is the producer of the vetoable event, and the RangeBean is the consumer that (occasionally) issues a veto.

To build a constrained (vetoable) property, your bean must have the following two methods to let it register VetoableChangeListener objects:

void addVetoableChangeListener(VetoableChangeListener 
   listener); 
void removeVetoableChangeListener(VetoableChangeListener 
   listener);

Just as there is a convenience class to manage property change listeners, there is a convenience class, called VetoableChangeSupport, that manages vetoable change listeners. Your bean should contain an object of this class.

private VetoableChangeSupport vcs 
   = new VetoableChangeSupport(this);

Adding and removing listeners should be delegated to this object. For example:

public void addVetoableChangeListener(VetoableChangeListener l) 
{  vcs.addVetoableChangeListener(l); 
} 
public void removeVetoableChangeListener(VetoableChangeListener 
   l) 
{  vcs.removeVetoableChangeListener(l); 
}

Constrained properties should also be bound. That is, you need to have methods to add PropertyChangeListener objects in addition to VetoableChangeListeners. You saw how to implement those methods in the preceding section.

An object that is able to veto changes will then need to implement not only the PropertyChangeListener interface but the VetoableChangeListener interface as well. That interface contains one method:

void vetoableChange(PropertyChangeEvent evt) 
   throws PropertyVetoException

This method receives a PropertyChangeEvent object, from which it can retrieve the current value and the proposed new value with the getOldValue and getNewValue methods.

Notice that the vetoableChange method throws an exception. A listener indicates its disapproval with the proposed change by throwing a PropertyVetoException. This exception forces the bean that produced the event to abandon the new value.

To see how to implement the VetoableChangeInterface, look at the code for the RangeBean class. It implements the needed method with the following code:

public void vetoableChange(PropertyChangeEvent evt) 
   throws PropertyVetoException 
{  int v = ((Integer)evt.getNewValue()).intValue(); 
   if (evt.getSource() == from && v > to.getValue()) 
      throw new PropertyVetoException("from > to", evt); 
   if (evt.getSource() == to && v < from.getValue()) 
      throw new PropertyVetoException("to < from", evt); 
}

To update a constrained property value:

  1. Notify all vetoable listeners that a change is about to occur. (Use the fireVetoableChange method of the VetoableChangeSupport class.)

  2. If none of the vetoable listeners has thrown a PropertyVetoException, then update the value of the property.

  3. Notify all property listeners that a change has occurred.

Here is a typical example from the IntTextBean code:

public void setValue(int v) throws PropertyVetoException 
{  Integer oldValue = new Integer(getValue()); 
   Integer newValue = new Integer(v); 
   vcs.fireVetoableChange("value", oldValue, newValue); 
   // survived, therefore no veto 
   value = v; 
   setText("" + v); 
   pcs.firePropertyChange("value", oldValue, newValue); 
}

It is important that you don’t change the property value until all the registered vetoable change listeners have agreed with the proposed change. Conversely, a vetoable change listener should never assume that a change that it agrees with is actually happening until it receives a second notification through the propertyChange method.

Example 7-3 shows the code for the integer text bean that allows its values to be vetoed—it is based on the IntTextField class from Volume 1. Example 7-4 shows the code for the RangeBean. A range bean contains two IntTextBean objects and vetoes the change if it would result in an invalid range. Try typing in a lower bound that is greater than the upper bound, and you will not succeed. Then, try the following: In the property inspector, set the from property value to a value that is greater than the to property value. You will see a veto dialog box, as shown in Figure 7-18. This dialog is displayed by the BeanBox. Here is what happens:

A property change veto dialog box

Figure 7-18. A property change veto dialog box

  1. You type a value into the edit field for the from property.

  2. The BeanBox calls the setFrom method of the RangeBean class.

  3. The setFrom method calls the setValue method of the IntTextBean class.

  4. The setValue method notifies its vetoable listeners, in our case, the RangeBean.

  5. The vetoableChange method of the RangeBean class throws a PropertyVetoException.

  6. The setValue method of the IntTextBean class does not catch the exception, and the value is not updated.

  7. The BeanBox captures the exception and displays the veto dialog box.

Example 7-3. IntTextBean.java

import java.awt.*; 
import java.awt.event.*; 
import java.beans.*; 
import java.io.*; 

public class IntTextBean extends TextField 
   implements Serializable 
{  public IntTextBean() 
   {  setText("0"); 

      addKeyListener(new KeyAdapter() 
      {  public void keyTyped(KeyEvent evt) 
         {  char ch = evt.getKeyChar(); 
            if (!('0' <= ch && ch <= '9' 
                  || ch == '-' 
                  || Character.isISOControl(ch))) 
               evt.consume(); 
            else 
               lastCaretPosition = getCaretPosition(); 
         } 
         public void keyPressed(KeyEvent evt) 
         {  if (evt.getKeyCode() == KeyEvent.VK_ENTER) 
            {  editComplete(); 
            } 
         } 
      }); 

      addFocusListener(new FocusAdapter() 
      {  public void focusLost(FocusEvent evt) 
         {  if (!evt.isTemporary()) 
            {  editComplete(); 
            } 
         } 
      }); 

      addTextListener(new TextListener() 
      {  public void textValueChanged(TextEvent evt) 
         {  checkFieldValue(); 
         } 
      }); 
   } 

   public void editComplete() 
   {  Integer oldValue = new Integer(value); 
      Integer newValue = new Integer(getFieldValue()); 
      try 
      {  vcs.fireVetoableChange("value", oldValue, newValue); 
         // survived, therefore no veto 
         value = getFieldValue(); 
         pcs.firePropertyChange("value", oldValue, newValue); 
      } 
      catch(PropertyVetoException e) 
      {  // someone didn't like it 
         // back to the drawing board... 
         requestFocus(); 
      } 
   } 

   public boolean checkFieldValue() 
   {  String lastValue = getText(); 
      try 
      {  Integer.parseInt(getText().trim() + "0"); 
         return true; 
      } 
      catch(NumberFormatException e) 
      {  setText(lastValue); 
         setCaretPosition(lastCaretPosition); 
         return false; 
      } 
   } 

   public int getFieldValue() 
   {  if (checkFieldValue()) 
         return Integer.parseInt(getText().trim()); 
      else 
         return 0; 
   } 

   public int getValue() 
   {  return value; 
   } 

   public void setValue(int v) throws PropertyVetoException 
   {  Integer oldValue = new Integer(getValue()); 
      Integer newValue = new Integer(v); 
      vcs.fireVetoableChange("value", oldValue, newValue); 
      // survived, therefore no veto 
      value = v; 
      setText("" + v); 
      pcs.firePropertyChange("value", oldValue, newValue); 
   } 
   public void addPropertyChangeListener 
      (PropertyChangeListener l) 
   {  pcs.addPropertyChangeListener(l); 
   } 

   public void removePropertyChangeListener 
      (PropertyChangeListener l) 
   {  pcs.removePropertyChangeListener(l); 
   } 

   public void addVetoableChangeListener 
      (VetoableChangeListener l) 
   {  vcs.addVetoableChangeListener(l); 
   } 

   public void removeVetoableChangeListener 
      (VetoableChangeListener l) 
   {  vcs.removeVetoableChangeListener(l); 
   } 

   public Dimension getMinimumSize() 
   {  return new Dimension(XMINSIZE, YMINSIZE); 
   } 

   private static final int XMINSIZE = 50; 
   private static final int YMINSIZE = 20; 
   private PropertyChangeSupport pcs 
      = new PropertyChangeSupport(this); 

   private VetoableChangeSupport vcs 
      = new VetoableChangeSupport(this); 

   private int value = 0; 

   int lastCaretPosition; 
}

Example 7-4. RangeBean.java

import java.awt.*; 
import java.beans.*; 
import java.io.*; 

public class RangeBean extends Panel 
   implements VetoableChangeListener, Serializable 
{  public RangeBean() 
   {  add(new Label("From")); 
      add(from); 
      add(new Label("To")); 
      add(to); 

      from.addVetoableChangeListener(this); 
      to.addVetoableChangeListener(this); 
   } 

   public void vetoableChange(PropertyChangeEvent evt) 
      throws PropertyVetoException 
   {  int v = ((Integer)evt.getNewValue()).intValue(); 
      if (evt.getSource() == from && v > to.getValue()) 
         throw new PropertyVetoException("from > to", evt); 
      if (evt.getSource() == to && v < from.getValue()) 
         throw new PropertyVetoException("to < from", evt); 
   } 

   public int getFrom() { return from.getValue(); } 
   public int getTo() { return to.getValue(); } 

   public void setFrom(int v) throws PropertyVetoException 
   {  from.setValue(v); 
   } 

   public void setTo(int v) throws PropertyVetoException 
   {  to.setValue(v); 
   } 

   private IntTextBean from = new IntTextBean(); 
   private IntTextBean to = new IntTextBean(); 
}

Finally, when you build a constrained property into your bean, keep in mind that you have no control over how property change events are sent to the interested beans. This means that one bean can accept the change, think it’s great, and decide to use the new value. But, when the AWT gets around to sending the same PropertyChange event to another bean component, that second bean component can veto it. Unless you make your constrained properties bound, you will have no way to notify the happy user of the new value that it can’t use it anymore. For this reason, we strongly suggest that all your constrained properties be bound.

Adding Custom Bean Events

When you add a bound or constrained property to a bean, you also enable the bean to fire events whenever the value of that property changes. However, there are other events that a bean can send out, for example,

  • When the program user has clicked on a control within the bean

  • When new information is available

  • Or simply, when some amount of time has elapsed

Unlike the PropertyChangeEvent events, these events belong to custom classes and need to be captured by custom listeners.

Here is how to write a bean that generates custom events. (Please consult Chapter 8 of Volume 1 for more details on Java’s event-handling mechanism.) Be sure to follow the first two steps precisely, or the introspection mechanism will not recognize that you are trying to define a custom event.

  1. Write a class Custom Event that extends EventObject. (The event name must end in Event in order for a builder to use the naming patterns to find it.)

  2. Write an interface Custom Listener with a single notification method. That notification method can have any name, but it must have a single parameter of type Custom Event and return type void.

  3. Supply the following two methods in the bean:

    public void addCustomListener(CustomListener e) 
    public void removeCustomListener(CustomListener e)

PITFALL

If your eventclass doesn't extend EventObject, chances are that your code will compile just fine because none of the methods of the EventObject class are actually needed, and none of the various other methods ever try to cast the event objects to the EventObject class. However, your bean will mysteriously fail—the introspection mechanism will not recognize the events.

To implement the methods needed for adding, removing, and delivering custom events, you can no longer rely on convenience classes that automatically manage the event listeners. Instead, you need to collect all the event listeners, for example, in a Vector. Moreover, when delivering the event, you also need to call the notification method for all the collected listeners. This call can lead to a synchronization problem—it is possible that one thread tries to add or remove a listener at the same time that another thread is handling an event delivery to it. For that reason, synchronize access to the collection of listeners. The following code shows how you can provide the needed synchronization:

public synchronized void addCustomListener 
   (CustomListener l) 
{  listeners.addElement(l); 
} 

public synchronized void removeCustomListener 
   (CustomListener l) 
{  listeners.removeElement(l); 
} 

public void fireCustomEvent(CustomEvent evt) 
{  Vector currentListeners = null; 
   synchronized(this) 
   {  currentListeners = (Vector)listeners.clone(); 
   } 
   for (int i = 0; i < currentListeners.size(); i++) 
   {CustomListener listener 
         = (CustomListener)currentListeners.elementAt(i); 
      listener.notifyMethod(evt); 
   } 
} 
. . . 
private Vector timerListeners = new Vector();

The code for the synchronization is a bit tricky. Why don’t we just make fire Custom Event into a synchronized method as well? Then, we could be sure that no listeners were added or removed as the events were fired. However, that opens us up to the potential for deadlocks because, in some cases, the notifyMethod might call add Custom Listener or remove Custom Listener. Since it is not called in a separate thread, that call would give rise to a deadlock. Therefore, we first clone the vector. Only the block of code that performs the cloning is synchronized. Of course, now only the listeners that have been registered at the outset of the delivery process are notified. And if one listener is removed before the delivery process is completed, that listener is still called. This in turn means that there is no absolute guarantee that event deliveries will cease immediately when a listener removes itself from an event source.

Now, let’s apply this technique to implementing a TimerBean. This bean should send TimerEvent objects to its listeners. (This is a modification of the Timer class from Chapter 2.) Timer events are generated at regular intervals (measured in milliseconds), set by the interval property, provided that the running property is set to true. Examples 7-57-7 show the code for the following:

  • The TimerEvent class, the custom event that is generated by this bean

  • The TimerListener class with a notification method that we called timeElapsed

  • The TimerBean with methods addTimerListener and removeTimerListener

Here is how you can test the bean. Drop a timer bean into the BeanBox. (This is an invisible bean with no paint method—it is just displayed as a string “TimerBean” by the BeanBox.) Then, drop an EventMonitor bean into the BeanBox. Select the TimerBean and choose Edit|Events from the BeanBox menu. Note that there is a submenu timer with a child menu timeElapsed. These menus show that the introspection method has correctly identified the custom event. Select the timeElapsed method. Then, a red line appears. Connect it to the EventMonitor. A dialog asks you which method should be called when the event notification occurs. Choose the only method available, initiateEventSourceMonitoring. Now, select the TimerBean once again. In the property sheet, set the running property to true and watch the text area in the EventMonitor bean. Once every second, a notification message is displayed (see Figure 7-19).

The BeanBox monitors a custom event

Figure 7-19. The BeanBox monitors a custom event

Here’s the complete code for the TimerBean :

Example 7-5. TimerBean.java

import java.awt.*; 
import java.util.*; 
import java.io.*; 

public class TimerBean implements Runnable, Serializable 
{  public int getInterval() { return interval; } 
   public void setInterval(int i) { interval = i; } 

   public boolean isRunning() { return runner != null; } 
   public void setRunning(boolean b) 
   {  if (b && runner == null) 
      {  runner = new Thread(this); 
         runner.start(); 
      } 
      else if (!b && runner != null) 
      {  runner.stop(); 
         runner = null; 
      } 
   } 

   public synchronized void addTimerListener 
      (TimerListener l) 
   {  timerListeners.addElement(l); 
   } 

   public synchronized void removeTimerListener 
      (TimerListener l) 
   {  timerListeners.removeElement(l); 
   } 

   public void fireTimerEvent(TimerEvent evt) 
   {  Vector currentListeners = null; 
      synchronized(this) 
      {  currentListeners = (Vector)timerListeners.clone(); 
      } 
      for (int i = 0; i < currentListeners.size(); i++) 
      {  TimerListener listener 
            = (TimerListener)currentListeners.elementAt(i); 
         listener.timeElapsed(evt); 
      } 
   } 

   public void run() 
   {  if (interval <= 0) return; 
      while (true) 
      {  try { Thread.sleep(interval); } 
         catch(InterruptedException e) {} 
         fireTimerEvent(new TimerEvent(this)); 
      } 
   } 

   private int interval = 1000; 
   private Vector timerListeners = new Vector(); 
   private Thread runner; 
}

Example 7-6. TimerListener.java

import java.util.*; 

public interface TimerListener extends EventListener 
{  public void timeElapsed(TimerEvent evt); 
}

Example 7-7. TimerEvent.java

import java.util.*; 

public class TimerEvent extends EventObject 
{  public TimerEvent(Object source) 
   {  super(source); 
      now = new Date(); 
   } 

   public Date getDate() { return now; } 

   private Date now; 
}

Property Editors

If you add an integer or string property to a bean, then that property is automatically displayed in the bean’s Property Sheet. But what happens if you add a property whose values cannot easily be edited in a text field, for example, a Day or a Color ? Then, you need to provide a separate component which the user can use to specify the property value. Such components are called property editors. For example, a property editor for a Day object might be a calendar that lets the user scroll through the months and pick a date. A property editor for a Color object would let the user select the red, green, and blue components of the color.

Actually, the BeanBox already has a property editor for colors—you saw it in Figure 7-4. And, of course, there are property editors for basic types such as String (a text field) and boolean (a choice list with values true and false ). These property editors are registered with the property editor manager. You can add property editors to the manager with the static registerEditor method of the PropertyEditorManager class. You supply the class to which the editor applies and the class of the editor.

PropertyEditorManager.registerEditor(Day.class, 
   CalendarSelector.class);

You can use the findEditor method in the PropertyEditorManager class to check whether a property editor exists for a given type in your builder tool. That method does the following:

  1. It looks first to see which property editors are already registered with it. (These will be the editors supplied by the builder tool and the editors that you supplied by calling registerEditor.)

  2. Then, it looks for a class whose name consists of the name of the type plus the word Editor.

  3. If neither lookup succeeds, then it returns null.

For example, a CalendarSelector class would be used to edit a Day property.

The BeanBox also uses the findEditor method to locate an editor for each type it displays in a Property Sheet. But before looking for a generic editor, it checks whether you requested a specific editor in the bean info of your bean. The bean info is a collection of miscellaneous information about your bean. For example, if you have a property whose type is int or String, but whose legal values are restricted in some way, you may not want to use the general-purpose property editor that is supplied by the BeanBox. Instead, you can supply a specific editor for a particular property by naming it in the bean info.

The process for supplying a specific propery editor is slightly involved. First, you create a bean info class to accompany your bean. The name of the class must be the name of the bean, followed by the word BeanInfo. That class must implement the BeanInfo interface, an interface with eight methods. It is simplest to extend the SimpleBeanInfo class instead. This convenience class has do-nothing implementations for the eight methods. For example,

// bean info class for ChartBean 
class ChartBeanBeanInfo 
   extends SimpleBeanInfo 
{ . . . }

To request a specific editor for a particular bean, you override the getPropertyDescriptors method. That method returns an array of PropertyDescriptor objects. You create one object for each property that should be displayed on a property editor, even those for which you just want the default editor.

You construct a PropertyDescriptor by supplying the name of the property and the class of the bean that contains it.

PropertyDescriptor pd1 
   = new PropertyDescriptor("title", ChartBean.class);

To request a specific editor for the property, you call the setPropertyEditorClass method of the PropertyDescriptor class.

PropertyDescriptor pd2 
   = new PropertyDescriptor("titlePosition", ChartBean.class); 
pd2.setPropertyEditorClass(TitlePositionEditor.class);

Next, you build an array of descriptors for properties of your bean. For example, the chart bean that we discuss in this section has four properties:

  • A String property, title

  • An int property, titlePosition

  • A double[] property, values

  • A boolean property, inverse

Figure 7-20 shows the chart bean. You can see the title on the top. Its position can be set to left, center, or right. The values property specifies the graph values. If the inverse property is true, then the background is colored and the bars of the chart are white. Example 7-8 lists the code for the chart bean; the bean is simply a modification of the chart applet in Volume 1, Chapter 10.

The chart bean

Figure 7-20. The chart bean

The code in Example 7-8 shows the ChartBeanBeanInfo class that specifies the property editors for these properties. It achieves the following:

  1. In the static block, the DoubleArrayEditor is registered as an editor for any double[] array, both in this bean and in other beans.

  2. The getPropertyDescriptors method returns a descriptor for each property. The title and values properties are used with the default editors, that is, the string editor that comes with the BeanBox and the DoubleArrayEditor that was registered in the static block.

  3. The titlePosition and inverse properties use special editors of type TitlePositionEditor and InverseEditor, respectively.

Figure 7-21 shows the resulting property sheet. You’ll see in the following sections how to implement these kinds of editors.

The property sheet for the chart bean

Figure 7-21. The property sheet for the chart bean

Example 7-8. ChartBean.java

import java.awt.*; 
import java.util.*; 
import java.beans.*; 
import java.io.*; 

public class ChartBean extends Component 
   implements Serializable 
{  public void paint(Graphics g) 
   {  if (values == null || values.length == 0) return; 
      int i; 
      double minValue = 0; 
      double maxValue = 0; 
      for (i = 0; i < values.length; i++) 
      {  if (minValue > getValues(i)) minValue = getValues(i); 
         if (maxValue < getValues(i)) maxValue = getValues(i); 
      } 
      if (maxValue == minValue) return; 

      Dimension d = getSize(); 
      int clientWidth = d.width; 
      int clientHeight = d.height; 
      int barWidth = clientWidth / values.length; 

      g.setColor(inverse ? color : Color.white); 
      g.fillRect(0, 0, clientWidth, clientHeight); 
      g.setColor(Color.black); 

      Font titleFont = new Font("SansSerif", Font.BOLD, 20); 
      FontMetrics titleFontMetrics 
         = g.getFontMetrics(titleFont); 

      int titleWidth = titleFontMetrics.stringWidth(title); 
      int y = titleFontMetrics.getAscent(); 
      int x; 
      if (titlePosition == LEFT) 
         x = 0; 
      else if (titlePosition == CENTER) 
         x = (clientWidth - titleWidth) / 2; 
      else 
         x = clientWidth - titleWidth; 

      g.setFont(titleFont); 
      g.drawString(title, x, y); 

      int top = titleFontMetrics.getHeight(); 
      double scale = (clientHeight - top) 
         / (maxValue - minValue); 
      y = clientHeight; 

      for (i = 0; i < values.length; i++) 
      {  int x1 = i * barWidth + 1; 
         int y1 = top; 
         int height = (int)(getValues(i) * scale); 
         if (getValues(i) >= 0) 
            y1 += (int)((maxValue - getValues(i)) * scale); 
         else 
         {  y1 += (int)(maxValue * scale); 
            height = -height; 
         } 

         g.setColor(inverse ? Color.white : color); 
         g.fillRect(x1, y1, barWidth - 2, height); 
         g.setColor(Color.black); 
         g.drawRect(x1, y1, barWidth - 2, height); 
      } 
   } 
   public void setTitle(String t) { title = t; } 
   public String getTitle() { return title; } 


   public double[] getValues() { return values; } 

   public void setValues(double[] v) { values = v; } 

   public double getValues(int i) 
   {  if (0 <= i && i < values.length) return values[i]; 
      return 0; 
   } 

   public void setValues(int i, double value) 
   {  if (0 <= i && i < values.length) values[i] = value; 
   } 

   public boolean isInverse() 
   {  return inverse; 
   } 

   public void setTitlePosition(int p) { titlePosition = p; } 

   public int getTitlePosition() 
   {  return titlePosition; 
   } 

   public void setInverse(boolean b) { inverse = b; } 

   public Dimension getMinimumSize() 
   {  return new Dimension(MINSIZE, MINSIZE); 
   } 

   public void setGraphColor(Color c) { color = c; } 
   public Color getGraphColor() { return color; } 

   private static final int LEFT = 0; 
   private static final int CENTER = 1; 
   private static final int RIGHT = 2; 
   private static final int MINSIZE = 50; 
   private double[] values = { 1, 2, 3 }; 
   private String title = "Title"; 
   private int titlePosition = CENTER; 
   private boolean inverse; 
   private Color color = Color.red; 
}

Example 7-9. ChartBeanBeanInfo.java

import java.beans.*; 

public class ChartBeanBeanInfo extends SimpleBeanInfo 
{  public PropertyDescriptor[] getPropertyDescriptors() 
   {  try 
      {  PropertyDescriptor titlePositionDescriptor 
            = new PropertyDescriptor("titlePosition", 
               ChartBean.class); 
         titlePositionDescriptor.setPropertyEditorClass 
            (TitlePositionEditor.class); 
         PropertyDescriptor inverseDescriptor 
            = new PropertyDescriptor("inverse", 
               ChartBean.class); 
         inverseDescriptor.setPropertyEditorClass 
            (InverseEditor.class); 

         return new PropertyDescriptor[] 
         {  new PropertyDescriptor("title", 
               ChartBean.class), 
            titlePositionDescriptor, 
            new PropertyDescriptor("values", 
               ChartBean.class), 
            new PropertyDescriptor("graphColor", 
               ChartBean.class), 
            inverseDescriptor 
         }; 
      } 
      catch(IntrospectionException e) 
      {  System.out.println("Error: " + e); 
            return null; 
      } 
   } 

   static 
   {  PropertyEditorManager.registerEditor(double[].class, 
         DoubleArrayEditor.class); 
   } 
}

Writing a Property Editor

Before we begin showing you how to write a property editor, we want to point out that while each property editor works with a value of one specific type, it can nonetheless be quite elaborate. For example, a color property editor (which edits an object of type Color ) could use sliders or a palette (or both) to allow the user to edit the color in a more congenial way.

Next, any property editor you write must implement the PropertyEditor interface, an interface with 12 methods. As with the BeanInfo interface, you will not want to do this directly. Instead, it is far more convenient to extend the convenience PropertyEditorSupport class that is supplied with Java. This support class comes with methods to add and remove property change listeners, and with default versions of all other methods of the PropertyEditor interface. For example, our editor for editing the title position of a chart in our chart bean starts out like this:

// property editor class for title position 
class TitlePositionEditor 
   extends PropertyEditorSupport 
{ . . . }

Note that if a property editor class has a constructor, it must also supply a constructor without arguments.

Finally, before we get into the mechanics of actually writing a property editor, we want to point out that the editor is under the control of the builder, not the bean. The builder adheres to the following procedure to display the current value of the property:

  1. It instantiates property editors for each property of the bean.

  2. It asks the bean to tell it the current value of the property.

  3. It then asks the property editor to display the value.

The property editor can either use text-based or graphically based methods to actually display the value. We discuss these methods next.

Simple Property Editors

Simple property editors work with text strings. You override the setAsText and getAsText methods. For example, our chart bean has a property that lets you set where the title should be displayed: Left, Center, or Right. These choices are implemented as integer constants.

private static final int LEFT = 0; 
private static final int CENTER = 1; 
private static final int RIGHT = 2;

But of course, we don’t want them to appear as numbers 0, 1, 2 in the text field—unless we are competing for the User Interface Hall of Horrors. Instead, we define a property editor whose getAsText method returns the value as a string. The method calls the getValue method of the PropertyEditor to find the value of the property. Since this is a generic method, the value is returned as an Object. If the property type is a basic type, we need to return a wrapper object. In our case, the property type is int and the call to getValue returns an Integer.

class TitlePositionEditor 
   extends PropertyEditorSupport 
{  public String getAsText() 
   {  int p = ((Integer)getValue()).intValue(); 
      if (0 <= i && i < options.length) return options[i]; 
      return ""; 
   } 
   . . . 
   private String[] options = { "Left", "Center", "Right" }; 
}

Now, the text field displays one of these fields. When the user edits the text field, this triggers a call to the setAsText method to update the property value by invoking the setValue method. It, too, is a generic method whose parameter is of type Object. To set the value of a numeric type, we need to pass a wrapper object.

public void setAsText(String s) 
{  for (int i = 0; i < options.length; i++) 
   {  if (options[i].equals(s)) 
      {  setValue(new Integer(i)); 
         return; 
      } 
   } 
}

Actually, this property editor is not a good choice for the titlePosition property, unless, of course, we are also competing for the User Interface Hall of Shame. The user may not know what the legal choices are. It would be better to display them in a Choice box (see Figure 7-22). The PropertyEditorSupport class gives a simple method to use a Choice box in a property editor. We simply write a getTags method that returns an array of strings.

The TitlePositionEditor at work

Figure 7-22. The TitlePositionEditor at work

public String[] getTags() { return options; }

The default getTags method returns null. By returning a non-null value, we indicate a choice field instead of a text field.

We still need to supply the getAsText and setAsText methods. The getTags method simply specifies the values to be displayed in the Choice field. The getAsText /setAsText methods translate between the strings and the data type of the property (which may be a string, an int, or a completely different type).

Example 7-10 lists the complete code for the property editor (see Example 7-8 for the code for the actual bean).

Example 7-10. TitlePositionEditor.java

import java.beans.*; 

public class TitlePositionEditor 
   extends PropertyEditorSupport 
{  public String getAsText() 
   {  int value = ((Integer)getValue()).intValue(); 
      return options[value]; 
   } 

   public void setAsText(String s) 
   {  for (int i = 0; i < options.length; i++) 
      {  if (options[i].equals(s)) 
         {  setValue(new Integer(i)); 
            return; 
         } 
      } 
   } 

   public String[] getTags() { return options; } 

   private String[] options = { "Left", "Center", "Right" }; 
}

GUI-Based Property Editors

More sophisticated property types can’t be edited as text. Instead, they are represented in two ways. The property sheet contains a small area (which otherwise would hold a text box or choice field) onto which the property editor will draw a graphical representation of the current value. When the user clicks on that area, a custom editor dialog box pops up (see Figure 7-23). The dialog box contains a component to edit the property values, supplied by the property editor, and a button labeled Done at the bottom, supplied by the BeanBox.

A custom editor dialog

Figure 7-23. A custom editor dialog

To build a GUI-based property editor:

  1. Tell the builder tool that you will paint the value and not use a string.

  2. “Paint” the value the user enters onto the GUI.

  3. Tell the builder tool that you will be using a GUI-based property editor.

  4. Build the GUI.

  5. Write the code to validate what the user tries to enter as the value.

For the first step, you override the getAsText method in the PropertyEditor interface to return null and the isPaintable method to return true.

public String getAsText() 
{  return null; 
} 
public boolean isPaintable() 
{  return true; 
}

Then, you implement the paintValue procedure. It receives a Graphics handle and the coordinates of the rectangle inside which you can paint. Note that this rectangle is typically small, so you can’t have a very elaborate representation. To graphically represent the inverse property, we draw the string "Inverse" in white letters with a black background or the string "Normal" in black letters with a white background:

public void paintValue(Graphics g, Rectangle box) 
{  boolean isInverse = ((Boolean)getValue()).booleanValue(); 
   String s = isInverse ? "Inverse" : "Normal"; 
   g.setColor(isInverse ? Color.black : Color.white); 
   g.fillRect(box.x, box.y, box.width, box.height); 
   g.setColor(isInverse ? Color.white : Color.black); 
   FontMetrics fm = g.getFontMetrics(); 
   int w = fm.stringWidth(s); 
   int x = box.x; 
   if (w < box.width) x += (box.width - w) / 2; 
   int y = box.y + (box.height - fm.getHeight()) / 2 
      + fm.getAscent(); 
   g.drawString(s, x, y); 
}

Of course, this graphical representation is not editable. The user must click on it to pop up a custom editor.

You indicate that you will have a custom editor by overriding the supportsCustomEditor in the PropertyEditor interface to return true :

public boolean supportsCustomEditor() 
{  return true; 
}

Now, you write the AWT code that builds up the component that will hold the custom editor. You will need to build a separate custom editor class for every property. For example, associated to our InverseEditor class is an InverseEditorPanel class (see Example 7-9) that describes a GUI with two radio buttons to toggle between normal and inverse mode. That code is straightforward AWT code. However, the GUI actions must update the property values. We did this as follows:

  1. Have the custom editor constructor receive a reference to the property editor object and store it in a variable editor.

  2. To read the property value, we have the custom editor call editor.getValue().

  3. To set the object value, we have the custom editor call editor.setValue(newValue) followed by editor.firePropertyChange().

Next, the getCustomEditor method of the PropertyEditor interface constructs and returns an object of the custom editor class.

public Component getCustomEditor() 
{  return new InverseEditorPanel(this); 
}

Example 7-11 shows the complete code for the InverseEditor.

The other custom editor that we built for the chart bean class lets you edit a double[] array. Recall that the BeanBox cannot edit array properties at all. We developed this custom editor to fill this obvious gap. That custom editor is a little involved. Figure 7-24 shows the custom editor in action. All array values are shown in the list box, prefixed by their array index. Clicking on an array value places it into the text field above it, and you can edit it. You can also resize the array. The code for the DoubleArrayPanel class that implements the GUI is listed in Example 7-14.

The custom editor dialog for editing an array

Figure 7-24. The custom editor dialog for editing an array

The code for the property editor class (shown in Example 7-13) is almost identical to that of the InverseEditor, except that we simply paint a string consisting of the first few array values, followed by . . ., in the paintValue method. And, of course, we return a different custom editor in the getCustomEditor method. These examples complete the code for the chart bean.

NOTE

NOTE

Unfortunately, we have to paint the array values. It would be more convenient to return a string with the getAsText method. However, then the BeanBox assumes that you can edit that text, and it won't pop up a custom editor if you click on it, even if the getCustomEditor method is defined.

Example 7-11. InverseEditor.java

import java.awt.*; 
import java.beans.*; 

public class InverseEditor extends PropertyEditorSupport 
{  public Component getCustomEditor() 
   {  return new InverseEditorPanel(this); 
   } 

   public boolean supportsCustomEditor() 
   {  return true; 
   } 

   public boolean isPaintable() 
   {  return true; 
   } 

   public void paintValue(Graphics g, Rectangle box) 
   {  boolean isInverse = ((Boolean)getValue()).booleanValue(); 
      String s = isInverse ? "Inverse" : "Normal"; 
      g.setColor(isInverse ? Color.black : Color.white); 
      g.fillRect(box.x, box.y, box.width, box.height); 
      g.setColor(isInverse ? Color.white : Color.black); 
      FontMetrics fm = g.getFontMetrics(); 
      int w = fm.stringWidth(s); 
      int x = box.x; 
      if (w < box.width) x += (box.width - w) / 2; 
      int y = box.y + (box.height - fm.getHeight()) / 2 
         + fm.getAscent(); 
      g.drawString(s, x, y); 
   } 

   public String getAsText() 
   {  return null; 
   } 
}

Example 7-12. InverseEditorPanel.java

import java.awt.*; 
import java.awt.event.*; 
import java.text.*; 
import java.lang.reflect.*; 
import java.beans.*; 

public class InverseEditorPanel extends Panel 
   implements ItemListener 
{  public InverseEditorPanel(PropertyEditorSupport ed) 
   {  editor = ed; 
      CheckboxGroup g = new CheckboxGroup(); 
      boolean isInverse 
         = ((Boolean)editor.getValue()).booleanValue(); 
      normal = new Checkbox("Normal", g, !isInverse); 
      inverse = new Checkbox("Inverse", g, isInverse); 

      normal.addItemListener(this); 
      inverse.addItemListener(this); 
      add(normal); 
      add(inverse); 
   } 

   public void itemStateChanged(ItemEvent evt) 
   {  if (evt.getStateChange() == ItemEvent.SELECTED) 
      {  editor.setValue(new Boolean(inverse.getState())); 
      editor.firePropertyChange(); 
      } 
   } 

   private Checkbox normal; 
   private Checkbox inverse; 
   PropertyEditorSupport editor; 
}

Example 7-13. DoubleArrayEditor.java

import java.awt.*; 
import java.beans.*; 

public class DoubleArrayEditor extends PropertyEditorSupport 
{  public Component getCustomEditor() 
   {  return new DoubleArrayEditorPanel(this); 
   } 

   public boolean supportsCustomEditor() 
   {  return true; 
   } 

   public boolean isPaintable() 
   {  return true; 
   } 

   public void paintValue(Graphics g, Rectangle box) 
   {  double[] values = (double[]) getValue(); 
      String s = ""; 
      for (int i = 0; i < 3; i++) 
      {  if (values.length > i) s = s + values[i]; 
         if (values.length > i + 1) s = s + ", "; 
      } 
      if (values.length > 3) s += "..."; 

      g.setColor(Color.white); 
      g.fillRect(box.x, box.y, box.width, box.height); 
      g.setColor(Color.black); 
      FontMetrics fm = g.getFontMetrics(); 
      int w = fm.stringWidth(s); 
      int x = box.x; 
      if (w < box.width) x += (box.width - w) / 2; 
      int y = box.y + (box.height - fm.getHeight()) / 2 
         + fm.getAscent(); 
      g.drawString(s, x, y); 
   } 

   public String getAsText() 
   {  return null; 
   } 
}

Example 7-14. DoubleArrayEditorPanel.java

import java.awt.*; 
import java.awt.event.*; 
import java.text.*; 
import java.lang.reflect.*; 
import java.beans.*; 

public class DoubleArrayEditorPanel extends Panel 
   implements ItemListener 
{  public DoubleArrayEditorPanel(PropertyEditorSupport ed) 
   {  editor = ed; 
      setArray((double[])ed.getValue()); 
      setLayout(new GridBagLayout()); 
      GridBagConstraints gbc = new GridBagConstraints(); 
      gbc.weightx = 0; 
      gbc.weighty = 0; 
      gbc.fill = GridBagConstraints.NONE; 
      gbc.anchor = GridBagConstraints.EAST; 
      add(new Label("Size"), gbc, 0, 0, 1, 1); 
      add(new Label("Elements"), gbc, 0, 1, 1, 1); 
      gbc.weightx = 100; 
      gbc.anchor = GridBagConstraints.WEST; 
      add(sizeField, gbc, 1, 0, 1, 1); 
      gbc.fill = GridBagConstraints.HORIZONTAL; 
      add(valueField, gbc, 1, 1, 1, 1); 
      gbc.weighty = 100; 
      gbc.fill = GridBagConstraints.BOTH; 
      add(elementList, gbc, 1, 2, 1, 1); 
      gbc.fill = GridBagConstraints.NONE; 

      elementList.addItemListener(this); 
      sizeField.addKeyListener(new KeyAdapter() 
         {  public void keyPressed(KeyEvent evt) 
            {  if (evt.getKeyCode() == KeyEvent.VK_ENTER) 
               {  resizeArray(); 
               } 
            } 
         }); 
      sizeField.addFocusListener(new FocusAdapter() 
         {  public void focusLost(FocusEvent evt) 
            {  if (!evt.isTemporary()) 
               {  resizeArray(); 
               } 
            } 
         }); 
      valueField.addKeyListener(new KeyAdapter() 
         {  public void keyPressed(KeyEvent evt) 
            {  if (evt.getKeyCode() == KeyEvent.VK_ENTER) 
               {  changeValue(); 
               } 
            } 
         }); 
      valueField.addFocusListener(new FocusAdapter() 
         {  public void focusLost(FocusEvent evt) 
            {  if (!evt.isTemporary()) 
               {  changeValue(); 
               } 
            } 
         }); 
   } 

   public void add(Component c, GridBagConstraints gbc, 
      int x, int y, int w, int h) 
   {  gbc.gridx = x; 
      gbc.gridy = y; 
      gbc.gridwidth = w; 
      gbc.gridheight = h; 
      add(c, gbc); 
   } 

   public void resizeArray() 
   {  fmt.setParseIntegerOnly(true); 
      int s = 0; 
      try 
      {  s = fmt.parse(sizeField.getText()).intValue(); 
         if (s < 0) 
            throw new ParseException("Out of bounds", 0); 
      } 
      catch(ParseException e) 
      {  sizeField.requestFocus(); 
         return; 
      } 
      if (s == theArray.length) return; 
      setArray((double[])arrayGrow(theArray, s)); 
      editor.setValue(theArray); 
      editor.firePropertyChange(); 
   } 

   public void changeValue() 
   {  double v = 0; 
      fmt.setParseIntegerOnly(false); 
      try 
      {  v = fmt.parse(valueField.getText()).doubleValue(); 
      } 
      catch(ParseException e) 
      {  valueField.requestFocus(); 
         return; 
      } 
      setArray(currentIndex, v); 
      editor.firePropertyChange(); 
   } 

   public void itemStateChanged(ItemEvent evt) 
   {  if (evt.getStateChange() == ItemEvent.SELECTED) 
      {  int i = elementList.getSelectedIndex(); 
         valueField.setText("" + theArray[i]); 
         currentIndex = i; 
      } 
   } 

   static Object arrayGrow(Object a, int newLength) 
   {  Class cl = a.getClass(); 
      if (!cl.isArray()) return null; 
      Class componentType = a.getClass().getComponentType(); 
      int length = Array.getLength(a); 

      Object newArray = Array.newInstance(componentType, 
         newLength); 
      System.arraycopy(a, 0, newArray, 0, 
         Math.min(length, newLength)); 
      return newArray; 
   } 

   public double[] getArray() 
   {  return (double[])theArray.clone(); 
   } 

   public void setArray(double[] v) 
   {  if (v == null) theArray = new double[0]; 
      else theArray = v; 
      sizeField.setText("" + theArray.length); 
      elementList.removeAll(); 
      for (int i = 0; i < theArray.length; i++) 
         elementList.add("[" + i + "] " + theArray[i]); 
      if (theArray.length > 0) 
      {  valueField.setText("" + theArray[0]); 
         elementList.select(0); 
         currentIndex = 0; 
      } 
      else 
         valueField.setText(""); 
   } 
   public double getArray(int i) 
   {  if (0 <= i && i < theArray.length) return theArray[i]; 
      return 0; 
   } 

   public void setArray(int i, double value) 
   {  if (0 <= i && i < theArray.length) 
      {  theArray[i] = value; 
         elementList.replaceItem("[" + i + "] " + value, i); 
         int previous = elementList.getSelectedIndex(); 
         elementList.select(i); 
         valueField.setText("" + value); 
         elementList.select(previous); 
      } 
   } 

   private PropertyEditorSupport editor; 
   private double[] theArray; 
   private int currentIndex = 0; 
   private NumberFormat fmt = NumberFormat.getNumberInstance(); 
   private TextField sizeField = new TextField(4); 
   private TextField valueField = new TextField(12); 
   private List elementList = new List(); 
}

Summing Up

For every property editor you write, you have to choose one of three ways to display and edit the property value:

  1. As a text string (define getAsText and setAsText )

  2. As a choice field (define getAsText, setAsText, and getTags )

  3. Graphically, by painting it (define isPaintable, paintValue, supportsCustomEditor, and getCustomEditor )

You saw examples of all three cases in the chart bean.

Finally, some property editors might want to support a method called getJavaInitializationString. With this method, you can give the builder tool the Java code that sets a property to allow automatic code generation. We did not show you an example for this method.

Going Beyond “Design Patterns”—Building a BeanInfo Class

You have already seen that if you use the standard naming conventions for the members of your bean, then a builder tool can use reflection to determine the properties, events, and methods of your bean. This process makes it simple to get started with bean programming but is rather limiting in the end. As your beans become in any way complex, there will be features of your bean that naming patterns and reflection will simply not reveal. (Not to mention that using English naming patterns as the basis for all GUI builders in all languages for all times seems to be rather against the spirit of Java’s internationalization support.)

Luckily, the JavaBeans specification allows a far more flexible and powerful mechanism for storing information about your bean for use by a builder. As with many features of beans, the mechanism is simple in theory but can be tedious to carry out in practice. The idea is that you can again use an object that implements the BeanInfo interface. (Recall that we used one small feature of the BeanInfo class when we supplied property editors for the chart bean class.)

When you implement this interface to describe your bean, a builder tool will look to the methods from the BeanInfo interface to tell it (potentially quite detailed) information about the properties, events, and methods your bean supports. The BeanInfo is supposed to free you from the tyranny of naming patterns. Somewhat ironically, the beans specification does require that you use a naming pattern to associate a BeanInfo object to the bean. You specify the name of the bean info class by adding BeanInfo to the name of the bean. For example, the bean info class associated to the class ChartBean must be named ChartBeanBeanInfo. The bean info class must be part of the same package as the bean itself.

NOTE

NOTE

Any descriptions you supply in the bean info associated to your bean override any information that the builder might obtain by reflecting on the member names. Moreover, if you supply information about a feature set (such as the properties that your bean supports), you must then provide information about all the properties in the associated bean info. This, of course, gives you a way to have members of your bean that are not exposed in a builder environment yet are still public.

As you already saw, you won’t normally write from scratch a class that implements the BeanInfo interface. Instead, you will probably turn again to the SimpleBeanInfo convenience class that has empty implementations (returning null ) for all the methods in the BeanInfo interface. This practice is certainly convenient—just override the methods you really want to change. Moreover, this convenience class includes a useful method called loadImage that you can use to load an image (such as an icon—see below) for your bean. We use this class for all our examples of BeanInfo classes. For example, our ChartBeanBeanInfo class starts out:

public class ChartBeanBeanInfo extends SimpleBeanInfo

NOTE

NOTE

That the methods in the SimpleBeanInfo class return null is actually quite important. This is exactly how the builder tool knows how to use naming patterns to find out the members of that feature set. A non-null return value turns off the reflective search.

For a taste of what you can do with the bean info mechanism, let’s start with an easy-to-use, but most useful, method in the BeanInfo interface: the getIcon method that lets you give your bean a custom icon. (After all, builder tools will usually want to have an icon for the bean for some sort of palette. In the BeanBox, the icon shows up to the left of the bean name in its Toolbox—see Figure 7-3.) Actually, you can specify separate icon bitmaps. The BeanInfo interface has four constants that cover the standard sizes.

ICON_COLOR_16x16 
ICON_COLOR_32x32 
ICON_MONO_16x16 
ICON_MONO_32x32

Here is an example of how you might use the loadImage convenience method in the SimpleBeanInfo class to add an icon to a class:

public Image getIcon(int iconType) 
{  String name = ""; 
   if (iconType == BeanInfo.ICON_COLOR_16x16) 
      name = "COLOR_16x16"; 
   else if (iconType == BeanInfo.ICON_COLOR_32x32) 
      name = "COLOR_32x32"; 
   else if (iconType == BeanInfo.ICON_MONO_16x16) 
      name = "MONO_16x16"; 
   else if (iconType == BeanInfo.ICON_MONO_32x32) 
      name = "MONO_32x32"; 
   else return null; 
   return loadImage("ChartBean_" + name + ".gif"); 
}

where we have cleverly named (as you can see in the CH7 directory) the image files to be

ChartBean_COLOR_16x16.gif 
ChartBean_COLOR_32x32.gif

and so on.

NOTE

NOTE

In the present version of Java, icons must be GIFs. JavaSoft says that in the future, other formats may be supported. Also note that JavaSoft recommends you provide at the very least a 16x16 color GIF for your bean. It is also best if the GIF images are transparent so the background can shine through.

FeatureDescriptor Objects

The key to using any of the more advanced features of the BeanInfo class is the FeatureDescriptor class and its various subclasses. As its name suggests, a FeatureDescriptor object provides information about a feature. Examples of features are properties, events, methods, and so on. More precisely, the FeatureDescriptor class is the base class for all descriptors, and it factors out the common operations that you need to deal with when trying to describe any feature. (For example, the name of the feature is obtained via a method in it called, naturally enough, getName. Since this method is in the base class, it works for all feature descriptors, no matter what they describe.) Here are the subclasses of the FeatureDescriptor class:

  • BeanDescriptor

  • EventSetDescriptor

  • MethodDescriptor

  • ParameterDescriptor

  • PropertyDescriptor (with a further subclass— IndexedPropertyDescriptor )

These classes all work basically the same. You create a descriptor object for each member you are trying to describe, and you collect all descriptors of a feature set in an array and return it as the return value of one of the BeanInfo methods.

For example, to turn off reflection for event sets, you’ll return an array of EventSetDescriptor objects in your bean info class:

class MyBeanBeanInfo extends SimpleBeanInfo 
{  public EventSetDescriptor[] getEventSetDescriptors() 
   {  . . . 
   } 
   . . . 
}

Next, you’ll construct all the various EventSetDescriptor objects that will go into this array. Generally, all the constructors for the various kinds of FeatureDescriptor objects work in the same way. In particular, for events, the most common constructor takes:

  • The class of the bean that has the event

  • The base name of the event

  • The class of the EventListener interface that corresponds to the event

  • The methods in the specified EventListener interface that are triggered by the event

Other constructors let you specify the methods of the bean that should be used to add and remove EventListener objects.

A good example of all this can be found in the BeanInfo class associated to the ExplicitButtonBean that ships with the BDK. Let’s analyze the code that creates the needed event descriptors for this bean. The ExplicitButtonBean fires two events: when the button is pushed and when the state of the button has changed. Here’s how you build these two EventSetDescriptor objects associated to these two events:

EventSetDescriptor push = new EventSetDescriptor(beanClass, 
   "actionPerformed", 
   java.awt.event.ActionListener.class, 
   "actionPerformed"); 

EventSetDescriptor changed = new EventSetDescriptor(beanClass, 
   "propertyChange", 
   java.beans.PropertyChangeListener.class, 
   "propertyChange");

The next step is to set the various display names for the events for the EventSetDescriptor. In the code for the ExplicitButton in the BeanInfo class, this is done via

push.setDisplayName("button push"); 
changed.setDisplayName("bound property change");

Actually, it is a little more messy to code the creation of the needed EventSetDescriptor objects than the above fragments indicate because all constructors for feature descriptor objects can throw an IntrospectionException. So, you actually have to build the array of descriptors in a try/catch block, as the following code indicates:

public EventSetDescriptor[] getEventSetDescriptors() 
{  try 
   {   EventSetDescriptor push = new 
         EventSetDescriptor(beanClass, 
            "actionPerformed", 
            java.awt.event.ActionListener.class, 
            "actionPerformed"); 

      EventSetDescriptor changed = new 
         EventSetDescriptor(beanClass, 
            "propertyChange", 
            java.beans.PropertyChangeListener.class, 
            "propertyChange"); 

      push.setDisplayName("button push"); 
      changed.setDisplayName("bound property change"); 

      return new EventSetDescriptor[] { push, changed }; 
   } catch (IntrospectionException e) 
   {  throw new Error(e.toString()); 
   } 
}

Why was this particular event set descriptor needed? The event descriptors differ from the standard naming pattern in two ways.

  • The listener classes are not the name of the bean + Listener.

  • The display names are not the same as the event names.

To summarize: When any feature of your bean differs from the standard naming pattern, you must do the following:

  • Create feature descriptors for all features in that set (events, properties, methods).

  • Return an array of the descriptors in the appropriate BeanInfo method.

Customizers

A property editor, no matter how sophisticated, is responsible for allowing the user to set one property at a time. Especially if certain properties of a bean relate to each other, it may be more user friendly to give users a way to edit multiple properties at the same time. To enable this feature, you supply a customizer instead of (or in addition to) multiple property editors.

In the example program for this section, we develop a customizer for the chart bean. The customizer lets you set several properties of the chart bean at once, and it lets you specify a file from which to read the data points for the chart. Figure 7-25 shows you one card of the customizer for the ChartBean.

The customizer for the ChartBean

Figure 7-25. The customizer for the ChartBean

To add a customizer to your bean, you must supply a BeanInfo class and override the getBeanDescriptor method, as shown in the following example:

public BeanDescriptor getBeanDescriptor() 
{  return new BeanDescriptor(ChartBean.class, 
      ChartBeanCustomizer.class); 
   }

The general procedure for your customizers follows the same model:

  • Override the getBeanDescriptor method by returning a new BeanDescriptor object for your bean.

  • Specify the customizer class as the second parameter of the constructor for the BeanDescriptor object.

Note that you need not follow any naming pattern for the customizer class. The builder can locate it by

  • Finding the associated BeanInfo class

  • Invoking its getBeanDescriptor method

  • Calling the getCustomizerClass method

(Nevertheless, it is customary to name the customizer as BeanName Customizer.)

Example 7-15 has the code for the ChartBeanBeanInfo class that references the ChartBeanCustomizer. You will see in the next section how that customizer is implemented.

Example 7-15. ChartBeanBeanInfo.java

import java.beans.*; 

public class ChartBeanBeanInfo extends SimpleBeanInfo 
{  public PropertyDescriptor[] getPropertyDescriptors() 
   {  try 
      {  PropertyDescriptor titlePositionDescriptor 
            = new PropertyDescriptor("titlePosition", 
               ChartBean.class); 
         titlePositionDescriptor.setPropertyEditorClass 
            (TitlePositionEditor.class); 
         PropertyDescriptor inverseDescriptor 
            = new PropertyDescriptor("inverse", 
               ChartBean.class); 
         inverseDescriptor.setPropertyEditorClass 
            (InverseEditor.class); 

         return new PropertyDescriptor[] 
         {  new PropertyDescriptor("title", 
               ChartBean.class), 
            titlePositionDescriptor, 
            new PropertyDescriptor("values", 
               ChartBean.class), 
            new PropertyDescriptor("graphColor", 
               ChartBean.class), 
            inverseDescriptor 
         }; 
      } 
      catch(IntrospectionException e) 
      {  System.out.println("Error: " + e); 
         return null; 
      } 
   } 

   static 
   {  PropertyEditorManager.registerEditor(double[].class, 
         DoubleArrayEditor.class); 
   } 
}

Writing a Customizer Class

Any customizer class you write must implement the Customizer interface. There are only three methods in this interface:

  • The setObject method, which takes a parameter that specifies the bean being customized

  • The addPropertyChangeListener and removePropertyChangeListener methods, which manage the collection of listeners that are notified when a property is changed in the customizer

JavaSoft suggests that the target bean’s visual appearance should be updated by broadcasting a PropertyChangeEvent whenever the user changes any of the property values, not just when the user is at the end of the customization process.

Unlike property editors, customizers are not automatically displayed. In the BeanBox, you must select Edit|Customize to pop up the customizer of a bean. At that point, the BeanBox will call the setObject method of the customizer that takes the bean being customized as a parameter. Notice that your customizer is thus created before it is actually linked to an instance of your bean. Therefore, you cannot assume any information about the state of a bean in the customizer, and you must provide a constructor without arguments.

There are three parts to writing a customizer class:

  • Building the visual interface

  • Initializing the customizer in the setObject method

  • Updating the bean by firing property change events when the user changes properties in the interface

By definition, a customizer class is visual. It must, therefore, extend Component or a subclass of Component, such as Panel. Since customizers typically present the user with many options, it is often handy to use a “wizard” interface, in which the possible settings are presented as a sequence of cards in a logical order, with buttons labeled “Next” and “Previous” to navigate between the individual cards. You use a CardLayout to flip through the cards.

We use this wizard approach for the customizer of our chart bean. The customizer gathers the information in three cards, in the following order:

  1. Data points (which are read in from a file)

  2. Graph color and inverse mode

  3. Title and title position

Of course, developing this kind of user interface can be tedious to code—our example devotes over 100 lines just to set it up in the constructor. However, this task requires only the usual AWT programming skills, and we won’t dwell on the details here. (See Chapters 79 of Volume 1 for more information.)

There is one trick that is worth keeping in mind. You often need to edit property values in a customizer. Rather than implementing a new interface for setting the property value of a particular class, you can simply locate an existing property editor and add it to your user interface! For example, in our ChartBean customizer, we need to set the graph color. Since we know that the BDK has a perfectly good property editor for colors, we locate it as follows:

PropertyEditor colorEditor 
   = PropertEditorManager.findEditor(Color.Class);

We then call getCustomEditor to get the component that contains the user interface for setting the colors and add it to one of the cards.

add(card, colorEditor.getCustomEditor(), . . .);

Once we have all components laid out, we initialize their values by using the setObject method. The setObject method is called when the customizer is displayed. Its parameter is the bean that is being customized. To proceed, we store that bean reference—we’ll need it later to notify the bean of property changes. Then, we initialize each user interface component. Here is a part of the setObject method of the chart bean customizer that does this initialization:

public void setObject(Object obj) 
{  bean = (ChartBean)obj; 
   titleField.setText(bean.getTitle()); 
   colorEditor.setValue(bean.getGraphColor()); 
   . . . 
}

Finally, we hook up event handlers to track the user’s activities. Whenever the user changes the value of a component, the component fires an event that our customizer must handle. The event handler must update the value of the property in the bean and must also fire a PropertyChangeEvent so that other listeners (such as the Property Sheet) can be updated. Let us follow that process with a couple of user interface elements in the chart bean customizer.

First, we implement property support listeners in the usual way, with a PropertyChangeSupport object:

public void addPropertyChangeListener 
   (PropertyChangeListener l) 
{  pcs.addPropertyChangeListener(l); 
} 
public void removePropertyChangeListener 
   (PropertyChangeListener l) 
{  pcs.removePropertyChangeListener(l); 
} 
private PropertyChangeSupport pcs 
   = new PropertyChangeSupport(this);

When the user types a new title, we want to update the title property. We attach a TextListener to the text field into which the user types the title.

titleField.addTextListener(new TextListener() 
{  public void textValueChanged(TextEvent evt) 
   {  setTitle(titleField.getText()); 
   } 
});

The textValueChanged method calls the setTitle method of the customizer. That method calls the bean to update the property value and then fires a property change event. (This update is necessary only for properties that are not bound.) Here is the code for the setTitle method:

public void setTitle(String newValue) 
{  String oldValue = bean.getTitle(); 
   bean.setTitle(newValue); 
   pcs.firePropertyChange("title", oldValue, newValue); 
}

When the color value changes in the color property editor, we want to update the graph color of the bean. We track the color changes by attaching a listener to the property editor. Perhaps confusingly, that editor also sends out property change events.

colorEditor.addPropertyChangeListener 
   (new PropertyChangeListener() 
{  public void propertyChange(PropertyChangeEvent 
      evt) 
   {  setGraphColor((Color)colorEditor.getValue()); 
   } 
});

Whenever the color value of the color property editor changes, we call the setGraphColor method of the customizer. That method updates the graphColor property of the bean and fires a different property change event that is associated with the graphColor property.

public void setValues(double[] newValue) 
{  double[] oldValue = bean.getValues(); 
   bean.setValues(newValue); 
   pcs.firePropertyChange("graphColor", oldValue, newValue); 
}

Example 7-16 provides the full code of the chart bean customizer.

This particular customizer just set properties of the bean. In general, customizers can call any methods of the bean, whether or not they are property setters. That is, customizers are more general than property editors. (Some beans may have features that are not exposed as properties and that can be edited only through the customizer.)

Example 7-16. ChartBeanCustomizer.java

import java.awt.*; 
import java.awt.event.*; 
import java.beans.*; 
import java.io.*; 
import java.text.*; 
import java.util.*; 

public class ChartBeanCustomizer extends Panel 
   implements Customizer 
{  public ChartBeanCustomizer() 
   {  Panel p; 
      setLayout(new BorderLayout()); 

      final Panel cardPanel = new Panel(); 
      final CardLayout cardLayout = new CardLayout(); 
      cardPanel.setLayout(cardLayout); 
      Panel card = new Panel(); 
      card.setLayout(new BorderLayout()); 

      TextArea fileInfo = new TextArea(
         "You can read data values for the chart
" 
         + "from a text file. The file must contain one
" 
         + "data value in each line.", 
         3, 50, TextArea.SCROLLBARS_NONE); 
      fileInfo.setEditable(false); 
      card.add(fileInfo, "Center"); 

      Button loadButton = new Button("Load"); 
      loadButton.addActionListener(new ActionListener() 
         {  public void actionPerformed(ActionEvent evt) 
            {  loadFile(); 
            } 
         }); 
      p = new Panel(); 
      p.add(loadButton); 
      card.add(p, "South"); 
      cardPanel.add(card, "0"); 

      add(cardPanel, "Center"); 

      card = new Panel(); 
      card.setLayout(new GridBagLayout()); 

      CheckboxGroup g = new CheckboxGroup(); 
      normal = new Checkbox("Normal", g, true); 
      inverse = new Checkbox("Inverse", g, false); 

      p = new Panel(); 
      p.add(normal); 
      p.add(inverse); 
      normal.addItemListener(new ItemListener() 
         {  public void itemStateChanged(ItemEvent evt) 
            {  if (evt.getStateChange() == ItemEvent.SELECTED) 
                  setInverse(false); 
            } 
         }); 

      inverse.addItemListener(new ItemListener() 
         {  public void itemStateChanged(ItemEvent evt) 
            {  if (evt.getStateChange() == ItemEvent.SELECTED) 
                  setInverse(true); 
            } 
         }); 
      colorEditor 
         = PropertyEditorManager.findEditor(Color.class); 
      colorEditor.addPropertyChangeListener 
         (new PropertyChangeListener() 
         {  public void propertyChange(PropertyChangeEvent 
               evt) 
            {  setGraphColor((Color)colorEditor.getValue()); 
            } 
         }); 

      GridBagConstraints gbc = new GridBagConstraints(); 
      gbc.weightx = 100; 
      gbc.weighty = 100; 
      gbc.fill = GridBagConstraints.NONE; 
      gbc.anchor = GridBagConstraints.WEST; 

      add(card, new Label("Set Color"), gbc, 0, 0, 1, 1); 
      add(card, p, gbc, 0, 1, 1, 1); 
      add(card, colorEditor.getCustomEditor(), 
         gbc, 0, 2, 1, 1); 
      cardPanel.add(card, "1"); 

      card = new Panel(); 
      card.setLayout(new GridBagLayout()); 

      positionGroup = new CheckboxGroup(); 
      position = new Checkbox[3]; 
      position[0] = new Checkbox("Left", g, false); 
      position[1] = new Checkbox("Center", g, true); 
      position[2] = new Checkbox("Right", g, false); 

      p = new Panel(); 
      for (int i = 0; i < position.length; i++) 
      {  final int value = i; 
         p.add(position[i]); 
         position[i].addItemListener(new ItemListener() 
            {  public void itemStateChanged(ItemEvent evt) 
               {  if (evt.getStateChange() 
                     == ItemEvent.SELECTED) 
                     setTitlePosition(value); 
               } 
            }); 
      } 

      titleField = new TextField(); 
      titleField.addTextListener(new TextListener() 
         {  public void textValueChanged(TextEvent evt) 
            {  setTitle(titleField.getText()); 
            } 
         }); 

      add(card, new Label("Set Title"), gbc, 0, 0, 1, 1); 
      add(card, p, gbc, 0, 1, 1, 1); 
      gbc.fill = GridBagConstraints.HORIZONTAL; 
      add(card, titleField, gbc, 0, 2, 1, 1); 
      cardPanel.add(card, "2"); 

      p = new Panel(); 
      Button nextButton = new Button("Next"); 
      Button previousButton = new Button("Previous"); 
      nextButton.addActionListener(new ActionListener() 
         {  public void actionPerformed(ActionEvent evt) 
            {  cardLayout.next(cardPanel); 
            } 
         }); 
      previousButton.addActionListener(new ActionListener() 
         {  public void actionPerformed(ActionEvent evt) 
            {  cardLayout.previous(cardPanel); 
            } 
         }); 
      p.add(previousButton); 
      p.add(nextButton); 
      add(p, "South"); 
   } 

   public static void add(Container t, 
      Component c, GridBagConstraints gbc, 
      int x, int y, int w, int h) 
   {  gbc.gridx = x; 
      gbc.gridy = y; 
      gbc.gridwidth = w; 
      gbc.gridheight = h; 
      t.add(c, gbc); 
   } 

   public void loadFile() 
   {  Container c = getParent(); 
      while (c != null && !(c instanceof Frame)) 
         c = c.getParent(); 
      FileDialog dlg = new FileDialog((Frame)c, 
            "Open data file", FileDialog.LOAD); 
      dlg.show(); 
      String filename = dlg.getFile(); 
      if (filename != null) 
      {  filename = dlg.getDirectory() + filename; 
         NumberFormat fmt = NumberFormat.getNumberInstance(); 
         try 
         {  Vector v = new Vector(); 
            BufferedReader in 
               = new BufferedReader(new FileReader(filename)); 
            String line; 
            while ((line = in.readLine()) != null) 
               v.addElement(fmt.parse(line)); 
            double[] d = new double[v.size()]; 
            for (int i = 0; i < d.length; i++) 
               d[i] = ((Number)v.elementAt(i)).doubleValue(); 
            setValues(d); 
         } 
         catch(IOException e) {} 
         catch(ParseException e) {} 
      } 
   } 

   public void setTitle(String newValue) 
   {  String oldValue = bean.getTitle(); 
      bean.setTitle(newValue); 
      pcs.firePropertyChange("title", oldValue, newValue); 
   } 

   public void setTitlePosition(int i) 
   {  Integer oldValue = new Integer(bean.getTitlePosition()); 
      Integer newValue = new Integer(i); 
      bean.setTitlePosition(i); 
      pcs.firePropertyChange("title", oldValue, newValue); 
   } 

   public void setInverse(boolean b) 
   {  Boolean oldValue = new Boolean(bean.isInverse()); 
      Boolean newValue = new Boolean(b); 
      bean.setInverse(b); 
      pcs.firePropertyChange("inverse", oldValue, newValue); 
   } 

   public void setValues(double[] newValue) 
   {  double[] oldValue = bean.getValues(); 
      bean.setValues(newValue); 
      pcs.firePropertyChange("inverse", oldValue, newValue); 
   } 

   public void setGraphColor(Color newValue) 
   {  Color oldValue = bean.getGraphColor(); 
      bean.setGraphColor(newValue); 
      pcs.firePropertyChange("inverse", oldValue, newValue); 
   } 
   public Dimension getPreferredSize() 
   {  return new Dimension(300, 200); 
   } 

   public void setObject(Object obj) 
   {  bean = (ChartBean)obj; 

      normal.setState(!bean.isInverse()); 
      inverse.setState(bean.isInverse()); 

      titleField.setText(bean.getTitle()); 

      positionGroup.setSelectedCheckbox 
         (position[bean.getTitlePosition()]); 

      colorEditor.setValue(bean.getGraphColor()); 
   } 

   public void addPropertyChangeListener 
      (PropertyChangeListener l) 
   {  pcs.addPropertyChangeListener(l); 
   } 

   public void removePropertyChangeListener 
      (PropertyChangeListener l) 
   {  pcs.removePropertyChangeListener(l); 
   } 

   ChartBean bean; 
   PropertyChangeSupport pcs = new PropertyChangeSupport(this); 
   PropertyEditor colorEditor; 

   Checkbox normal; 
   Checkbox inverse; 
   Checkbox[] position; 
   CheckboxGroup positionGroup; 
   TextField titleField; 
}

Advanced Use of Introspection

From the point of view of the Java beans specification, introspection is simply the process by which a builder tool finds out which properties, methods, and events a Java bean supports. Introspection is carried out in two ways:

  • By searching for classes and methods that follow certain naming patters

  • By querying the BeanInfo of a class

Normally, introspection is an activity that is reserved for bean environments. The bean environment uses introspection to learn about beans, but the beans themselves don’t need to carry out introspection. However, there are some cases when one bean needs to use introspection to analyze other beans. A good example is when you want to tightly couple two beans on a form in a builder tool. Consider, for example, a spin bean, a small control element with two buttons, to increase or decrease a value (see Figure 7-26).

The spin bean

Figure 7-26. The spin bean

A spin bean by itself is not useful. It needs to be coupled with another bean. For example, a spin bean can to be coupled to an integer text field. Each time the user clicks on one of the buttons of the spin bean, the integer value is incremented or decremented. We will call the coupled bean the buddy of the spin bean. The buddy does not have to be an IntTextBean. It can be any other bean with an integer property.

You use the customizer of the spin bean to attach the buddy (see Figure 7-27).

The customizer of the SpinBean

Figure 7-27. The customizer of the SpinBean

Here is how you can try it out:

  1. Add the SpinBean and an IntTextBean on the form.

  2. Click on the IntTextBean and look at the value of the name property in the Property Sheet. The name will be something like textfield12. Remember the name, or copy it into the clipboard.

  3. Pop up the customizer of the spin bean by selecting it and selecting Edit|Customize from the menu.

  4. Type or paste the name of the IntTextBean into the buddy text field.

  5. Watch how all the int properties in the choice box are automatically filled in (see Figure 7-28).

    The SpinBean coupled with an IntTextBean

    Figure 7-28. The SpinBean coupled with an IntTextBean

  6. Select value and Done.

  7. Then, click on + and - and watch the integer text field value increase and decrease (see Figure 7-28).

It looks easy, but there were two challenges to implementing this customization.

  1. How do yoice field (define getAsText, setAsText, and getTag s)

  2. Graphically, by painting it (define isPaintable, paintValue, supportsCustomEditor, and getCustomEditor)

You saw examples of all three cases in the chart bean.

Finally, th of these problems. To analyze the properties of a bean, first get the bean info by calling the static getBeanInfo method of the Introspector class.

BeanInfo info 
   = Introspector.getBeanInfo(buddy.getClass());

Once we have the bean info, we can obtain an array of property descriptors:

PropertyDescriptor[] props = info.getPropertyDescriptors();

In the spin bean customizer, the next step is to loop through this array, picking out all properties of type int and adding them to a choice field.

for (int i = 0; i < props.length; i++) 
{  if (props[i].getPropertyType().equals(int.class)) 
   {  propChoice.add(props[i].getName()); 
   } 
}

This code shows how you can find out about the properties of a bean.

Next, we need to be able to get and set the property that the user selected. We obtain the get and set methods by calls to getReadMethod and getWriteMethod :

Method getMethod = prop.getReadMethod(); 
Method setMethod = prop.getWriteMethod();

(Why is it called get Read Method ? Probably because getGetMethod sounds too silly.)

Now, we invoke the methods to get a value, increment it, and set it. This process again uses the Reflection API—see, for example, Chapter 5 of Volume 1. Note that we must use an Integer wrapper around the int value.

int value = ((Integer)getMethod.invoke(buddy, 
   null)).intValue(); 
value += increment; 
setMethod.invoke(buddy, 
   new Object[] { new Integer(value) });

Could we have avoided reflection if we had demanded that the buddy have methods getValue and setValue ? No. You can only call

int value = buddy.getValue();

when the compiler knows that buddy is an object of a type that has a getValue method. But buddy can be of any type—there is no type hierarchy for beans. Whenever one bean is coupled with another arbitrary bean, then you need to use introspection and reflection.

Finally, we should point out that the code we use is more complicated than it might be. This complexity arises because the current JavaBean specification gives no way for beans in the same project in a builder to know about each other.

NOTE

NOTE

The next version of beans, the so-called Glasgow specification, does allow this mutual knowledge. You can obtain the specification from http://java.sun.com/beans/index.html.

In our case, it would have been nice if the customizer of the spin bean simply enumerated all other beans on the form instead of forcing the BeanBox user to cut and paste the name of the buddy. The problem is that with the current beans specification, it is very difficult to enumerate all other beans on a form in a builder like the BeanBox. In the BeanBox, for example, you can’t simply call

Component[] siblings = getParent().getComponents()

to get all the siblings of the spin bean. The reason you can’t do this is that, unfortunately, the BeanBox surrounds every bean by a panel within a panel. (We suspect that is done to detect mouse clicks that select the bean and to draw the outline around a selected bean.) So, in the BeanBox, we’d have to write

Component[] siblings 
   = getParent().getParent().getParent().getComponents()

However, there is no guarantee that this solution would work in another builder environment—since those environments might be smart enough not to need all these extra panels.

In fact, even when the name of a bean is known, it is not easy to locate the bean object. Fortunately, you can locate the object in a way that is independent of the design environment, by the following algorithm:

  1. Find the top-level parent by calling getParent until the parent is null :

    Container parent = bean.getParent(); 
    while (parent.getParent() != null) 
       parent = parent.getParent();
  2. Look through all children of that top-level parent. Since the children can themselves be containers, the child containers must be searched recursively.

    public static Component findBuddy(Container parent, String 
       name) 
    {  Component[] children = parent.getComponents(); 
       for (int i = 0; i < children.length; i++) 
       {  if (children[i].getName().equals(name)) 
             return children[i]; 
          if (children[i] instanceof Container) 
          {  Component ret = findBuddy((Container)children[i], 
                name); 
             if (ret != null) return ret; 
          } 
       } 
       return null; 
    }

We also wanted to program the spin bean to move next to its buddy. In principle, this should not be hard—get the location of the buddy and call setLocatio n to move the spin bean next to it. But setLocation moves the spin bean only within its container, which is, in the case of the BeanBox, a panel. We really would need to move the two nested panels as well. Since these panels are not present in other builder environments, we gave up.

Another possible user interface would have been to have the user drag the buddy bean on top of the spin bean. Unfortunately, though, a bean gets no event notification when another bean is dropped onto it in the current specification, so it is hard to develop good design interfaces for container beans until the Glasgow specification is implemented.

Examples 7-17 through 7-19 contain the full code for the SpinBean, including the needed bean info class to hook in the customizer.

Example 7-17. SpinBean.java

import java.awt.*; 
import java.awt.event.*; 
import java.beans.*; 
import java.lang.reflect.*; 
import java.io.*; 

public class SpinBean extends Panel 
   implements ActionListener, Serializable 
{  public SpinBean() 
   {  setLayout(new GridLayout(1, 2)); 
      Button plusButton = new Button("+"); 
      Button minusButton = new Button("-"); 
      add(plusButton); 
      add(minusButton); 
      plusButton.addActionListener(this); 
      minusButton.addActionListener(this); 
   } 

   public void setBuddy(Component b, PropertyDescriptor p) 
   {  buddy = b; 
      prop = p; 
   } 

   public void actionPerformed(ActionEvent evt) 
   {  if (buddy == null) return; 
      if (prop == null) return; 
      String arg = evt.getActionCommand(); 
      int increment = 0; 
      if (arg.equals("+")) increment = 1; 
      else if (arg.equals("-")) increment = -1; 
      else return; 
      Method readMethod = prop.getReadMethod(); 
      Method writeMethod = prop.getWriteMethod(); 
      try 
      {  int value = ((Integer)readMethod.invoke(buddy, 
            null)).intValue(); 
         value += increment; 
         writeMethod.invoke(buddy, 
            new Object[] { new Integer(value) }); 
      } 
      catch(Exception e) {} 
   } 

   public Dimension getPreferredSize() 
   {  return new Dimension(MINSIZE, MINSIZE); 
   } 

   String buddyName = ""; 

   private static final int MINSIZE = 20; 
   private Component buddy = null; 
   private PropertyDescriptor prop = null; 
}

Example 7-18. SpinBeanCustomizer.java

import java.awt.*; 
import java.awt.event.*; 
import java.beans.*; 
import java.io.*; 
import java.text.*; 
import java.util.*; 

public class SpinBeanCustomizer extends Panel 
   implements Customizer, ItemListener, TextListener 
{  public SpinBeanCustomizer() 
   {  setLayout(new GridBagLayout()); 
      GridBagConstraints gbc = new GridBagConstraints(); 
      gbc.weightx = 0; 
      gbc.weighty = 100; 
      gbc.fill = GridBagConstraints.NONE; 
      gbc.anchor = GridBagConstraints.EAST; 
      add(new Label("Buddy"), gbc, 0, 0, 1, 1); 
      add(new Label("Property"), gbc, 0, 1, 1, 1); 
      gbc.weightx = 100; 
      gbc.anchor = GridBagConstraints.WEST; 
      gbc.fill = GridBagConstraints.HORIZONTAL; 
      add(buddyTextField, gbc, 1, 0, 1, 1); 
      add(propChoice, gbc, 1, 1, 1, 1); 

      buddyTextField.addTextListener(this); 
      propChoice.addItemListener(this); 
   } 

   public void add(Component c, GridBagConstraints gbc, 
      int x, int y, int w, int h) 
   {  gbc.gridx = x; 
      gbc.gridy = y; 
      gbc.gridwidth = w; 
      gbc.gridheight = h; 
      add(c, gbc); 
   } 

   public void textValueChanged(TextEvent evt) 
   {  findBuddyMethods(); 
   } 

   public void findBuddyMethods() 
   {  propChoice.removeAll(); 
      Container parent = bean.getParent(); 
      while (parent.getParent() != null) 
         parent = parent.getParent(); 

      buddy = findBuddy(parent, buddyTextField.getText()); 
      if (buddy == null) 
      {  return; 
      } 

      try 
      {  BeanInfo info 
            = Introspector.getBeanInfo(buddy.getClass()); 
         props = info.getPropertyDescriptors(); 
         int j = 0; 
         for (int i = 0; i < props.length; i++) 
         {  if (props[i].getPropertyType().equals(int.class)) 
            {  propChoice.add(props[i].getName()); 
               props[j++] = props[i]; 
            } 
         } 
      } 
      catch(IntrospectionException e){} 
   } 

   public static Component findBuddy(Container parent, 
      String name) 
   {  Component[] children = parent.getComponents(); 
      for (int i = 0; i < children.length; i++) 
      {  if (children[i].getName().equals(name)) 
            return children[i]; 
         if (children[i] instanceof Container) 
         {  Component ret 
               = findBuddy((Container)children[i], name); 
            if (ret != null) return ret; 
         } 
      } 
      return null; 
   } 

   public void itemStateChanged(ItemEvent evt) 
   {  if (evt.getStateChange() == ItemEvent.SELECTED) 
      {  bean.setBuddy(buddy, 
            props[propChoice.getSelectedIndex()]); 
      } 
   } 

   public Dimension getPreferredSize() 
   {  return new Dimension(200, 100); 
   } 

   public void setObject(Object obj) 
   {  bean = (SpinBean)obj; 
   } 

   public void addPropertyChangeListener 
      (PropertyChangeListener l) 
   {  support.addPropertyChangeListener(l); 
   } 

   public void removePropertyChangeListener 
      (PropertyChangeListener l) 
   {  support.removePropertyChangeListener(l); 
   } 

   SpinBean bean; 
   PropertyChangeSupport support 
      = new PropertyChangeSupport(this); 
   TextField buddyTextField = new TextField(); 
   Choice propChoice = new Choice(); 
   Component buddy; 
   PropertyDescriptor[] props; 
}

Example 7-19. SpinBeanBeanInfo.java

import java.awt.*; 
import java.beans.*; 

public class SpinBeanBeanInfo extends SimpleBeanInfo 
{  public BeanDescriptor getBeanDescriptor() 
   {  return new BeanDescriptor(SpinBean.class, 
         SpinBeanCustomizer.class); 
   } 
}
..................Content has been hidden....................

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