Previous | Next | Trail Map | JavaBeans Tutorial | Writing a Simple JavaBean


Adding Properties

Beans become significant when you add properties and events. Properties are analogous to instance variables inside of objects. Event handlers are analogous to behaviors or methods defined for objects. The main difference is that properties and events are exposed to builder tools to enable visual manipulation of beans. Values assigned to properties can be modified at design time. Similarly you can visually select event handlers from list boxes to link the events generated by one bean to corresponding event handlers available in another bean. Properties are the natural place to start.

Bean with color property (Acme02Bean)

The next step is to add a property to the beans. This is straightforward.

Begin by adding a private instance variable, beanColor, to draw the foreground of the bean when it is rendered with the paint callback method.

     private Color beanColor = Color.blue;
Next you want to make a property, which you do by defining two methods with nearly identical names.

    public Color getColor() {
        return beanColor;
    }

    public void setColor(Color newColor) {
        beanColor = newColor;
        repaint();
    }
The definition of a property is based on the signature pattern of the methods defined in the class. The general semantics of the names are:

    getPROPERTY
    setPROPERTY

Of course, the string "PROPERTY" must be altered for each newly defined property. Thus the previous method pair implements the interface for a color property. You can define an unlimited pair of methods following this pattern; in each case, PROPERTY must be a unique name.

The actual name of the instance variable that is modified or retrieved by the pair does not matter. The instance variable modified by getColor and setColor is beanColor, though it could just as easily have been color or foo.

Here's the full source code for the second example, Acme02Bean.

import java.awt.*;
import java.io.Serializable;


public class Acme02Bean extends Canvas implements Serializable {

    public Acme02Bean() {
        setSize(60,40);
    }

    public void paint(Graphics g) {
        g.setColor(beanColor);
        g.fillRect(20, 5, 20, 30);
    }

    public Color getColor() {
        return beanColor;
    }

    public void setColor(Color newColor) {
        beanColor = newColor;
        repaint();
    }

    private Color beanColor = Color.blue;
}
Compare the property sheet for the first bean:

and the second bean:

This figure shows that the introspection mechanism in BeanBox finds the matching pair of getColor and setColor methods required to define a color property.

Beans and Packages (Acme03Bean)

Typically, beans you buy from a third party, or beans that you make yourself should be installed in a unique location in relation to other beans. The way to keep everything in its proper place is to use Java packages. The next change is simple in deed, but there are some important concepts to learn from this simple change. Add the following line to the top of your file:
package acme.beans;

With this third example (Acme03Bean) based on packages, you will now need to create a directory structure so the Java compiler can put the generated class files in the proper locations. You'll want to define the package name for classes that go in your package.

You will also need to import the package to files that use classes from the package.

Include the full name of the package and class when making a JAR file to include beans that are part of a package. JAR files are a new form of archive file introduced in JDK 1.1 and are the preferred packaging mechanism for shipping beans which are built from multiple files--including both class files and image files.

Here's how to create the appropriate directory structure for the acme.beans package below the current working directory.

Assuming a package name of "acme.beans" create directories for Java source files and Java class files:

    mkdir -p ./src/acme/beans
    mkdir -p ./classes/acme/beans
Now copy the Java source file (or files) to the appropriate place in the source tree:

    cp -p Acme03Bean.java ./src/acme/beans

Bean with label property (Acme04Bean)

Next, add an instance variable to hold a String label for the Bean.

    private String label;

Now that an instance variable is defined, you'll want to assign it a default value in the bean's constructor. It makes sense to set the font used to render the label inside the constructor as well. Add the following two lines.

    this.label="Bean";
    setFont(new Font("Dialog", Font.PLAIN, 12));
Eventually this bean will be crafted to behave like a button. You'll want to be able to customize the label for the button from within a builder tool (BeanBox, for example). To enable design-time customization you'll need to add a labelproperty to the bean. To do this, add getLabel and setLabel methods to the class.

    public String getLabel() {
        return label;
    }

    public void setLabel(String newLabel) {
        String oldLabel = label;
        label = newLabel;
    }
    

The approach is identical to adding the color property earlier in the lesson.

While you're at it, you can render the bean to look more like a button by adding a few lines to the end of the paint callback method.

        g.fillArc(5, 5, 30, 30, 0, 360);
        g.fillArc(25, 5, 30, 30, 0, 360);
        g.setColor(Color.blue);

        int width = size().width;
        int height = size().height;
        FontMetrics fm = g.getFontMetrics();
        g.drawString(label, (width - fm.stringWidth(label)) / 2, 
                          (height + fm.getMaxAscent() - fm.getMaxDescent()) / 2);
The definition for paint should look like this.
    public void paint(Graphics g) {
        g.setColor(beanColor);
        g.fillRect(20, 5, 20, 30);
        g.fillArc(5, 5, 30, 30, 0, 360);
        g.fillArc(25, 5, 30, 30, 0, 360);
        g.setColor(Color.blue);

        int width = size().width;
        int height = size().height;
        FontMetrics fm = g.getFontMetrics();
        g.drawString(label, (width - fm.stringWidth(label)) / 2, 
                          (height + fm.getMaxAscent() - fm.getMaxDescent()) / 2);
    }
When the button is drawn, it looks something like a bean.

The property sheet now shows fields for both the color and label properties. You can customize either one of these properies at design time by editing the property sheet. You'll notice that the color of the button has been changed from blue to cyan. This was done by changing the initializer for the beanColor instance variable.

    private Color beanColor = Color.cyan;
This new bean-like shape is due to the two new calls to fillArc in the bean's paint method
        g.fillArc(5, 5, 30, 30, 0, 360);
        g.fillArc(25, 5, 30, 30, 0, 360);
These calls add a rounded left and right side to the original square rendering of the bean. The remaining code in the paint method ensures the String label is centered within the button's bounding box.

        int width = size().width;
        int height = size().height;
        FontMetrics fm = g.getFontMetrics();
        g.drawString(label, (width - fm.stringWidth(label)) / 2, 
                          (height + fm.getMaxAscent() - fm.getMaxDescent()) / 2);

Automatic Sizing of Beans (Acme05Bean)

So far, a default label has been supplied for the bean by its constructor. A good design should let you override this default label by defining a second constructor which accepts a String argument.

    public Acme05Bean(String label) {
        super();
        this.label = label;
        setFont(new Font("Dialog", Font.PLAIN, 12));
    }
Having a customizable label displayed on a bean poses new challenges. Keep in mind that the label can be set programatically by the constructor, changed at design time by a builder tool, or modified at run time by a call to setLabel.

Some flexibility must be built in to the sizing of the bean. What happens, for example, if the label is too long? Obviously the bean's bounding box must be adjusted to accommodate the label supplied to the constructor. A call to resize command, hard coded in the constructor, makes no sense once you make it possible to customize the bean's label.

    public Acme04Bean() {       //    obsolete constructor
        resize(60,40);          // << not flexible enoungh 
                                //    for customizable label
        this.label="Bean";
        ...
    }
It turns out that the Beans API provides a convenient way for you to specify the preferred size of a bean and to do so on the fly. You simply define methods specifying the preferred size and the minimum size of your bean. The details will be covered in the following paragraphs.

Knowing that a technique for automatic sizing is available, you can drop the call to resize from the constructor. Because you have defined a constructor accepting a String argument, the role of the default constructor can be reduced to supplying a default label for the button.

    public Acme05Bean() {
        this("AcmeBean Serial# 05");
    }
The default label is long intentially to show how a button can size itself automatically. There are two occasions where a button needs to adjust itself to accommodate the size of its label. The first is when the button is first created, for example when the button is selected from the palette of a builder tool and dropped onto a form. The second case is when the button label is modified during design time by editing the label with the property sheet.

Defining Preferred and Minimum Sizes for Beans

If you defined a method named getPreferredSize, BeanBox calls it automatically when you drag a new instance of the bean from the component palette and drop it on the application form.

    public Dimension getPreferredSize() {
        FontMetrics fm = getFontMetrics(getFont());
        return new Dimension(fm.stringWidth(label) + TEXT_XPAD, 
                             fm.getMaxAscent() + fm.getMaxDescent() + TEXT_YPAD);
    }
This algorithm is similar to the size calculation carried out in the bean's paint method to center the string within the button. Padding is added outside of the string so the label doesn't appear crowded. In this case, however, a dimension for the preferred size is returned, rather than drawing the string inside of the button. Symbolic constants for the padding are defined as instance variables for the bean to be used as padding around the button.

    static final int TEXT_XPAD = 12;
    static final int TEXT_YPAD = 8;
In addition to a preferred size for your bean, the beans API lets you specify a minimum size. Both are used to help builder tools determine how to draw your bean at design time. To keep things simple, the minimum size of the bean, in this case, is defined to be the same as the preferred size.
    public Dimension getMinimumSize() {
        return getPreferredSize();
    }
The other sizing issue you must worry about, is when someone using your bean want to change its size at design time by editing the label from a property sheet. In previous version of the bean, you could change the label at design time, but the button would not resize itself when the label gets too long to be visible inside the button. For typical buttons, you want the button to change its size each time the label is changed from the property editor--that is with each key stroke the label should grow or shrink according the the new size of the lable. The workhorse behind this resizing is a new method, sizeToFit.

    private void sizeToFit() {
        Dimension d = getPreferredSize();
        resize(d.width, d.height);
        Component p = getParent();
        if (p != null) {
            p.invalidate();
            p.layout();
        }
    }
Notice that getPreferredSize is called here, as well as by the builder tool, whenever the size of the bean must be recalculated. The call to resize is made here now, instead of in the constructor as in previous version of the bean. You could name this sizing method anything you want, as long as you call it from setLabel. The setLabel method is called automatically by the builder tool (BeanBox for example) with each keystroke modification made to the label property from the property sheet editor. Defining sizeToFit as a workhorse method simplifies the modifications that must be made to setLabel; you need only add a single line to the end of the method.

    public void setLabel(String newLabel) {
        String oldLabel = label;
        label = newLabel;
        sizeToFit();                    // << new
    }
The final change in version 5 of AcmeBean causes it to be drawn using a 3D effect. It now looks more like a standard AWT button than a bean.

The calls to fillArc and fillRec with hard-coded dimensions have been removed

    // obsolete version
    public void paint(Graphics g) {
        ...
        g.fillRect(20, 5, 20, 30);
        g.fillArc(5, 5, 30, 30, 0, 360);
        g.fillArc(25, 5, 30, 30, 0, 360);
        ...
    }
and replaced with a rectangular rendering of the button, based on the width and height of the Canvas on which it is drawn.

        g.fillRect(1, 1, width - 2, height - 2);
        g.draw3DRect(0, 0, width - 1, height - 1, true);
The last argument to draw3DRect is a boolean that determines whether the rectangle should appear raised or depressed. Eventually this call will be modified so that the visual display of the button reflects the up or down state of the left mouse button as the mouse button is pressed and released over the bean.

In addition, a line border is drawn just around the outside edge of the button.

        g.drawRect(2, 2, width - 4, height - 4);

The paint method should also be synchronized to keep things tidy. Here's the entire new definition:

    public synchronized void paint(Graphics g) {

        int width = size().width;
        int height = size().height;

        g.setColor(beanColor);
        g.fillRect(1, 1, width - 2, height - 2);
        g.draw3DRect(0, 0, width - 1, height - 1, true);

        g.setColor(getForeground());
        g.setFont(getFont());

        g.drawRect(2, 2, width - 4, height - 4);

        FontMetrics fm = g.getFontMetrics();
        g.drawString(label, (width - fm.stringWidth(label)) / 2, 
                          (height + fm.getMaxAscent() - fm.getMaxDescent()) / 2);
    }

Event Handlers for Beans (Acme06Bean)

The next modification lays the groundwork for turning a bean into a useful component. Event handling is the key. Define a new method:

    public boolean handleEvent(Event evt) {
        if (! isEnabled()) {
            return false;
        }
        switch (evt.id) {
        case Event.MOUSE_UP:
            System.err.println("MOUSE_UP fired:");
            return true;
        }
        return false;
    }
The handler doesn't do anything useful yet; it merely let's you test that events are being caught by the bean. It's best to start with a small method defninition for handleEvent to make sure everything is working; the switch statement can grow large rather quickly, so it's best to be sure you are building on a solid foundation.

For now, a message is printed to the applications stderr file whenever a MOUSE_UP event is handled by the bean. Overriding the handleEvent uses the older JDK 1.0.2 style of event handling, as opposed to the new JDK 1.1 event source and event listener style of event handling. Either technique will work. The older style is used here to reduce the number of new concepts you need to master to get simple beans up and running. For an explantion of the new delegation event model found in JDK 1.1 see [href to JDK 1.1 Spec, event section].

Firing Events from Beans (Acme07Bean)

Now you're ready to add useful event processing to handleEvent. Most of the code will be implemented in a helper function called fireEvent.

Once you are confident that events are being caught by handleEvent, remove the println print stub


                      case Event.MOUSE_UP:
                          System.err.println("MOUSE_UP fired:");  // << remove
                          ...
                      }
and replace it with a call to fireAction.

    public boolean handleEvent(Event evt) {
        if (! isEnabled()) {
            return false;
        }
        switch (evt.id) {
        case Event.MOUSE_UP:
            fireAction();               // << new call
            return true;
        }
        return false;
    }
Before you can define fireAction, you'll have to import some classes and define some instance variables. First the new import statements:

import java.awt.event.*;
...
import java.util.Vector;
The reason for importing the event package is clear; you need it to define ActionEvent objects and generate events. The need for a Vector requires some explanation of the new event source, event listener model defined for JDK 1.1.

A Bean that wants to generate events needs a way to keep track of interested event targets. In the delegation event model, the event mechanism is broken up conceptually into event dispatch and event handling. Event dispatch is the responsibility of the event source; event handling is the responsibility of the event listener. Any object that wants to know when an event is fired by a bean can tell the bean it wants to be informed about particular events. In other words, an event listener registers interest in an event by calling a predetermined method in the event source.

To make the discussion concrete, consider a button bean (like the one you are defining in this lesson) that generates events when pressed, and a counter object that will be incremented each time a button is pressed.

If the button bean wants to be an event source, it must provide two methods that can be called by interested objects. One method adds the caller to the list of listeners who are notified when the event occurs. The other method removes the caller from the list of interested listeners.

    public synchronized void addActionListener(ActionListener l) {
        ...
    }
    public synchronized void removeActionListener(ActionListener l) {
        ...
    }
Once again, patterns are used in the signature of the method names. The pattern of the method's signature is detected by Java's new introspection mechanism, which can tell what events the source will generate from the name of the registration methods, together with the type of the arguments of the registration methods. For a detailed explanation see [href to Beans Spec, section 6.5 Event Listener Registration].

The general pattern for event generation capabilities recognized by Java's introspection mechanism is as follows:


    public synchronized void addTYPE(TYPE listener);

    public synchronized void removeTYPE(TYPE listener);

where TYPE is replaced by the class name of the particlular event listener.

In the case of the above event registration methods, defined for the bean button, Java's introspection mechanism is able to determine that an ActionEvent can be generated by the button. If the counter object wants to be notified when an ActionEvent occurs, it calls addActionListener, giving itself as an argument. In order for this to work, the counter object would have to implement the ActionListener interface, because an ActionListener object is accepted as the argument to addActionListener.

The bean button needs a way to keep track of all of the listeners who might register to receive notification of ActionEvents. This is where the Vector import statement comes into play. A list (or Vector) of listeners is maintained by the beanButton.

public class Acme07Bean extends Canvas implements Serializable {
    ...
    private Vector listeners = new Vector();
    ...
}
When addActionListener is called, the listener supplied as an argument is appended to the Vector of listeners.

    public synchronized void addActionListener(ActionListener l) {
        
        listeners.addElement(l);
    }
Similarly, when removeActionListener is called, the listener supplied as an argument to the method is removed from the list of listeners.

    public synchronized void removeActionListener(ActionListener l) {
        listeners.removeElement(l);
    }

Dispatching Events to Event Listeners

When an event is fired, the event source (the bean button) iterates over the list of listeners, sending each a notification of the ActionEvent.

    public void fireAction() {
        ...
        Vector targets;
        synchronized (this) {
            targets = (Vector) listeners.clone();
        }
        ActionEvent actionEvt = new ActionEvent(this, 0, null);
        for (int i = 0; i < targets.size(); i++) {
            ActionListener target = (ActionListener)targets.elementAt(i);
            target.actionPerformed(actionEvt);
        }
        ...
    }
Now you can define the required instance variables.
    private boolean debug = true;
    ...
    private Vector listeners = new Vector();
The new fireAction method makes use of both these instance variables. The debug instance variable can be used to control the printing of stub information when handleEvent is called. By making debug a property, you can change it's value inside a builder tool. This is very handy in BeanBox where you can turn debugging on and off for each individual button to get just the amount of feedback you need for diagnosing a particular problem.

To make debug a property, simply define setDebug and getDebug methods.

    public void setDebug(boolean x) {
        boolean old = debug;
        debug = x;
    }

    public boolean getDebug() {
        return debug;
    }
Making debug a property lets you alter the reporting of events through a println stub on the fly by changing the value of the property from false to true using a property sheet editor. When debug is true calls to fireAction are reported to the users by printing the label of the button that fired the event.

    public void fireAction() {
        if (debug) {
            System.err.println("Button " + getLabel() + " pressed.");
        }
        Vector targets;
        synchronized (this) {
            targets = (Vector) listeners.clone();
        }
        ActionEvent actionEvt = new ActionEvent(this, 0, null);
        for (int i = 0; i < targets.size(); i++) {
            ActionListener target = (ActionListener)targets.elementAt(i);
            target.actionPerformed(actionEvt);
        }

        Component parent = getParent();
        if (parent != null) {
            parent.postEvent(new Event(this, Event.MOUSE_DOWN, null));
        }
    }
The newly defined bean can now act as an event source for ActionEvent objects. You can verify that this is true, by adding the bean to the BeanBox component palette, then trying to hook up the bean's ActionEvent to start or stop the Duke Juggler bean.

Notice that the event menu lists an Action item for this bean, meaning it can be used like the ExplicitButton or OurButton beans provided as BeanBox examples. In fact, it would be a good exercise to use two instances of Acme07Button to start and stop Duke's juggling routine.

Notice that the sixth version of the button (Acme06Button) did not provide methods to register ActionEvent listeners and therefore could not fire action events. Java introspection does not find action events defined for Acme06Button, so no action event appears in the Events menu. When comparing property sheets, notice also that when an Acme07Bean is selected, you can edit the debug property by using a boolean choice selector.

Version seven (Acme07Bean) required a substantial number of additions. To summarize, Canvas's handleEvent method was overridden to capture and dispatch mouse events received by the button. A fireAction method was defined and called from handleEvent on mouse up events. In order for the bean to act as an event source to one or more event listener objects, two methods were defined to add and remove interested listners. A vector was added as an instance variable to keep track of registered listeners. Finally, a debug variable was defined to control the printing of debug information. Making debug a property lets you turn debugging on and off at your convenience while experimenting with beans you are developing and testing inside BeanBox.

A pointer to the entire source listing for Acme07Bean is provided again here for convenience.

Making a Bean Bounce like a Button (Acme08Bean)

With two simple changes, you can make the button behave visually, like a regular AWT button. That is, when the button is pressed, it appears to lower itself; when it is released, it appears to raise itself again.

First add an instance variable, down, to keep track of whether the button is in a pressed or released state.

    private boolean down;
Inside the paint method that draws the button, change the rendering to depend on the value of down. Change

        g.draw3DRect(0, 0, width - 1, height - 1, true);
to

        g.draw3DRect(0, 0, width - 1, height - 1, !down);
Before this modification, the button was always drawn in the raised parameter (the last argument) and was always given a value of true.

Now raised is true only when down is false.

To make this all work properly, you must write a small amount of maintenance code to be sure down always has the right value. The place to do this is in handleEvent.

First add a new case clause handle mouse down events.

    public boolean handleEvent(Event evt) {
        ...
        switch (evt.id) {
        case Event.MOUSE_DOWN:
            down = true;
            repaint();
            return true;
        ...
    }
This code sets the remembered button state to down, requests that the button be redrawn and returns true to indicate the event has been handled. Besides this, you'll have to change the case clause for mouse up from

        case Event.MOUSE_UP:
            fireAction();
            return true;
to
        ...
        case Event.MOUSE_UP:
            if (down) {
                fireAction();
                down = false;
                repaint();
            }
            return true;
       ...
Formerly, the button was not redrawn on a mouse event. Now it is always redrawn whether the event is a press or a release. More precisely, the button is only redrawn in a mouse up event if it was already in a down state. fireAction is called, as before, to dispatch the ActionEvent to any register listeners.

Next Stop: BeanBox

Now that you've seen how to build basic beans you're ready for some more complex examples. Before looking at contstrained properties and advanced event generation and handling techniques, you'll want to learn how to use BeanBox to test your beans. BeanBox will be covered in the next installment of this tutorial.


Previous | Next | Trail Map | JavaBeans Tutorial | Writing a Simple JavaBean