Visual
programming
and
Beans
So
far in this book you’ve seen how valuable Java
is for creating reusable
pieces of code. The “most reusable” unit of code has been the
class, since it comprises a cohesive unit of characteristics (fields) and
behaviors (methods) that can be reused either directly via composition or
through inheritance.
Inheritance
and polymorphism are essential parts of object-oriented programming, but in the
majority of cases when you’re putting together an application, what you
really want is components that do exactly what you need. You’d like to
drop these parts into your design like the electronic engineer puts together
chips on a circuit board (or even, in the case of Java, onto a Web page). It
seems, too, that there should be some way to accelerate this “modular
assembly” style of programming.
“Visual
programming” first became successful –
very
successful – with Microsoft’s
Visual Basic (VB), followed by a second-generation design in Borland’s
Delphi (the primary inspiration for the Java Beans design). With these
programming tools the components are represented visually, which makes sense
since they usually display some kind of visual component such as a button or a
text field. The visual representation, in fact, is often exactly the way the
component will look in the running program. So part of the process of visual
programming involves dragging a component from a pallet and dropping it onto
your form. The application
builder tool writes code as you do this, and that code will cause the component
to be created in the running program.
Simply
dropping the component onto a form is usually not enough to complete the
program. Often, you must change the characteristics of a component, such as
what color it is, what text is on it, what database it’s connected to,
etc. Characteristics that can be modified at design time are referred to as properties.
You can manipulate the properties of your component inside the application
builder tool, and when you create the program this configuration data is saved
so that it can be rejuvenated when the program is started.
By
now you’re probably used to the idea that an object is more than
characteristics; it’s also a set of behaviors. At design-time, the
behaviors of a visual component are partially represented by events,
meaning “Here’s something that can happen to the component.”
Ordinarily, you decide what you want to happen when an event occurs by tying
code to that event.
Here’s
the critical part: the application builder tool is able to dynamically
interrogate (using reflection)
the component to find out which properties and events the component supports.
Once it knows what they are, it can display the properties and allow you to
change those (saving the state when you build the program), and also display
the events. In general, you do something like double clicking on an event and
the application builder tool creates a code body and ties it to that particular
event. All you have to do at that point is write the code that executes when
the event occurs.
All
this adds up to a lot of work that’s done for you by the application
builder tool. As a result you can focus on what the program looks like and what
it is supposed to do, and rely on the application builder tool to manage the
connection details for you. The reason that visual programming tools have been
so successful is that they dramatically speed up the process of building an
application – certainly the user interface, but often other portions of
the application as well.
What
is a Bean?
After
the dust settles, then, a component
is really just a block of code, typically embodied in a class. The key issue is
the ability for the application builder tool to discover the properties and
events for that component. To create a VB component, the programmer had to
write a fairly complicated piece of code following certain conventions to
expose the properties and events. Delphi was a second-generation visual
programming tool and the language was actively designed around visual
programming so it is much easier to create a visual component. However, Java
has brought the creation of visual components to its most advanced state with
Java Beans, because a Bean is just a class. You don’t have to write any
extra code or use special language extensions in order to make something a
Bean. The only thing you need to do, in fact, is slightly modify the way that
you name your methods. It is the method name that tells the application builder
tool whether this is a property, an event, or just an ordinary method.
In
the Java documentation, this naming convention is mistakenly termed a
“design pattern.” This is unfortunate since design patterns (see
Chapter 16) are challenging enough without this sort of confusion. It’s
not a design pattern, it’s just a naming
convention and it’s fairly simple:
- For
a property named
xxx,
you typically create two methods:
getXxx( )
and
setXxx( ).
Note that the first letter after get or set is automatically lowercased to
produce the property name. The type produced by the “get” method is
the same as the type of the argument to the “set” method. The name
of the property and the type for the “get” and “set”
are not related.
- For
a boolean property, you can use the “get” and “set”
approach above, but you can also use “is” instead of
“get.”
- Ordinary
methods of the Bean don’t conform to the above naming convention, but
they’re
public.
- For
events, you use the “listener” approach. It’s exactly the
same as you’ve been seeing:
addFooBarListener(FooBarListener)
and
removeFooBarListener(FooBarListener)
to handle a
FooBarEvent.
Most of the time the built-in events and listeners will satisfy your needs, but
you can also create your own events and listener interfaces.
Point
1 above answers a question about something you might have noticed in the change
from Java 1.0
to Java 1.1:
a number of method names have had small, apparently meaningless name changes.
Now you can see that most of those changes had to do with adapting to the
“get” and “set” naming conventions in order to make
that particular component into a Bean.
We
can use these guidelines to create a simple Bean:
//: Frog.java
// A trivial Java Bean
package frogbean;
import java.awt.*;
import java.awt.event.*;
class Spots {}
public class Frog {
private int jumps;
private Color color;
private Spots spots;
private boolean jmpr;
public int getJumps() { return jumps; }
public void setJumps(int newJumps) {
jumps = newJumps;
}
public Color getColor() { return color; }
public void setColor(Color newColor) {
color = newColor;
}
public Spots getSpots() { return spots; }
public void setSpots(Spots newSpots) {
spots = newSpots;
}
public boolean isJumper() { return jmpr; }
public void setJumper(boolean j) { jmpr = j; }
public void addActionListener(
ActionListener l) {
//...
}
public void removeActionListener(
ActionListener l) {
// ...
}
public void addKeyListener(KeyListener l) {
// ...
}
public void removeKeyListener(KeyListener l) {
// ...
}
// An "ordinary" public method:
public void croak() {
System.out.println("Ribbet!");
}
} ///:~
First,
you can see that it’s just a class. Usually, all your fields will be
private,
and accessible only through methods. Following the naming convention, the
properties are
jumps,
color,
spots,
and
jumper
(notice the change in case of the first letter in the property name). Although
the name of the internal identifier is the same as the name of the property in
the first three cases, in
jumper
you can see that the property name does not force you to use any particular
name for internal variables (or, indeed, to even
have
any internal variable for that property).
The
events this Bean handles are
ActionEvent
and
KeyEvent,
based on the naming of the “add” and “remove” methods
for the associated listener. Finally, you can see that the ordinary method
croak( )
is still part of the Bean simply because it’s a
public
method, not because it conforms to any naming scheme.
Extracting
BeanInfo
with
the Introspector
One
of the most critical parts of the Bean scheme occurs when you drag a Bean off a
palette and plop it down on a form. The application builder tool must be able
to create the Bean (which it can do if there’s a default constructor) and
then, without access to the Bean’s source code, extract all the necessary
information to create the property sheet and event handlers.
Part
of the solution is already evident from the end of Chapter 11: Java 1.1
reflection
allows all the methods of an anonymous class to be discovered. This is perfect
for solving the Bean problem without requiring you to use any extra language
keywords like those required in other visual programming languages. In fact,
one of the prime reasons that reflection was added to Java 1.1 was to support
Beans (although reflection also supports object serialization and remote method
invocation). So you might expect that the creator of the application builder
tool would have to reflect each Bean and hunt through its methods to find the
properties and events for that Bean.
This
is certainly possible, but the Java designers wanted to provide a standard
interface for everyone to use, not only to make Beans simpler to use but also
to provide a standard gateway to the creation of more complex Beans. This
interface is the Introspector
class, and the most important method in this class is the
static
getBeanInfo( ).
You pass a
Class
handle to this method and it fully interrogates that class and returns a
BeanInfo
object that you can then dissect to find properties, methods, and events.
Usually
you won’t care about any of this – you’ll probably get most
of your Beans off the shelf from vendors, and you don’t need to know all
the magic that’s going on underneath. You’ll simply drag your Beans
onto your form, then configure their properties and write handlers for the
events you’re interested in. However, it’s an interesting and
educational exercise to use the
Introspector
to display information about a Bean, so here’s a tool that does it
(you’ll find it in the
frogbean
subdirectory):
//: BeanDumper.java
// A method to introspect a Bean
import java.beans.*;
import java.lang.reflect.*;
public class BeanDumper {
public static void dump(Class bean){
BeanInfo bi = null;
try {
bi = Introspector.getBeanInfo(
bean, java.lang.Object.class);
} catch(IntrospectionException ex) {
System.out.println("Couldn't introspect " +
bean.getName());
System.exit(1);
}
PropertyDescriptor[] properties =
bi.getPropertyDescriptors();
for(int i = 0; i < properties.length; i++) {
Class p = properties[i].getPropertyType();
System.out.println(
"Property type:\n " + p.getName());
System.out.println(
"Property name:\n " +
properties[i].getName());
Method readMethod =
properties[i].getReadMethod();
if(readMethod != null)
System.out.println(
"Read method:\n " +
readMethod.toString());
Method writeMethod =
properties[i].getWriteMethod();
if(writeMethod != null)
System.out.println(
"Write method:\n " +
writeMethod.toString());
System.out.println("====================");
}
System.out.println("Public methods:");
MethodDescriptor[] methods =
bi.getMethodDescriptors();
for(int i = 0; i < methods.length; i++)
System.out.println(
methods[i].getMethod().toString());
System.out.println("======================");
System.out.println("Event support:");
EventSetDescriptor[] events =
bi.getEventSetDescriptors();
for(int i = 0; i < events.length; i++) {
System.out.println("Listener type:\n " +
events[i].getListenerType().getName());
Method[] lm =
events[i].getListenerMethods();
for(int j = 0; j < lm.length; j++)
System.out.println(
"Listener method:\n " +
lm[j].getName());
MethodDescriptor[] lmd =
events[i].getListenerMethodDescriptors();
for(int j = 0; j < lmd.length; j++)
System.out.println(
"Method descriptor:\n " +
lmd[j].getMethod().toString());
Method addListener =
events[i].getAddListenerMethod();
System.out.println(
"Add Listener Method:\n " +
addListener.toString());
Method removeListener =
events[i].getRemoveListenerMethod();
System.out.println(
"Remove Listener Method:\n " +
removeListener.toString());
System.out.println("====================");
}
}
// Dump the class of your choice:
public static void main(String[] args) {
if(args.length < 1) {
System.err.println("usage: \n" +
"BeanDumper fully.qualified.class");
System.exit(0);
}
Class c = null;
try {
c = Class.forName(args[0]);
} catch(ClassNotFoundException ex) {
System.err.println(
"Couldn't find " + args[0]);
System.exit(0);
}
dump(c);
}
} ///:~
BeanDumper.dump( )
is the method that does all the work. First it tries to create a
BeanInfo
object, and if successful calls the methods of
BeanInfo
that produce information about properties, methods, and events. In
Introspector.getBeanInfo( ),
you’ll see there is a second argument. This tells the
Introspector
where to stop in the inheritance hierarchy. Here, it stops before it parses all
the methods from
Object,
since we’re not interested in seeing those.
For
properties, getPropertyDescriptors( )
returns an array of PropertyDescriptors.
For each
PropertyDescriptor
you can call getPropertyType( )
to find the class of object that is passed in and out via the property methods.
Then, for each property you can get its pseudonym (extracted from the method
names) with getName( ),
the method for reading with getReadMethod( ),
and the method for writing with getWriteMethod( ).
These last two methods return a
Method
object that can actually be used to invoke the corresponding method on the
object (this is part of reflection).
For
the public methods (including the property methods), getMethodDescriptors( )
returns an array of MethodDescriptors.
For each one you can get the associated Method
object and print out its name.
For
the events, getEventSetDescriptors( )
returns an array of (what else?) EventSetDescriptors.
Each of these can be queried to find out the class of the listener, the methods
of that listener class, and the add- and remove-listener methods. The
BeanDumper
program
prints out all of this information.
If
you invoke
BeanDumper
on the
Frog
class like this:
java
BeanDumper frogbean.Frog
the
output, after removing extra details that are unnecessary here, is:
class name: Frog
Property type:
Color
Property name:
color
Read method:
public Color getColor()
Write method:
public void setColor(Color)
====================
Property type:
Spots
Property name:
spots
Read method:
public Spots getSpots()
Write method:
public void setSpots(Spots)
====================
Property type:
boolean
Property name:
jumper
Read method:
public boolean isJumper()
Write method:
public void setJumper(boolean)
====================
Property type:
int
Property name:
jumps
Read method:
public int getJumps()
Write method:
public void setJumps(int)
====================
Public methods:
public void setJumps(int)
public void croak()
public void removeActionListener(ActionListener)
public void addActionListener(ActionListener)
public int getJumps()
public void setColor(Color)
public void setSpots(Spots)
public void setJumper(boolean)
public boolean isJumper()
public void addKeyListener(KeyListener)
public Color getColor()
public void removeKeyListener(KeyListener)
public Spots getSpots()
======================
Event support:
Listener type:
KeyListener
Listener method:
keyTyped
Listener method:
keyPressed
Listener method:
keyReleased
Method descriptor:
public void keyTyped(KeyEvent)
Method descriptor:
public void keyPressed(KeyEvent)
Method descriptor:
public void keyReleased(KeyEvent)
Add Listener Method:
public void addKeyListener(KeyListener)
Remove Listener Method:
public void removeKeyListener(KeyListener)
====================
Listener type:
ActionListener
Listener method:
actionPerformed
Method descriptor:
public void actionPerformed(ActionEvent)
Add Listener Method:
public void addActionListener(ActionListener)
Remove Listener Method:
public void removeActionListener(ActionListener)
====================
This
reveals most of what the
Introspector
sees as it produces a
BeanInfo
object from your Bean. You can see that the type of the property and its name
are independent. Notice the lowercasing of the property name. (The only time
this doesn’t occur is when the property name begins with more than one
capital letter in a row.) And remember that the method names you’re
seeing here (such as the read and write methods) are actually produced from a
Method
object that can be used to invoke the associated method on the object.
The
public method list includes the methods that are not associated with a property
or event, such as
croak( ),
as well as those that are. These are all the methods that you can call
programmatically for a Bean, and the application builder tool can choose to
list all of these while you’re making method calls, to ease your task.
Finally,
you can see that the events are fully parsed out into the listener, its
methods, and the add- and remove-listener methods. Basically, once you have the
BeanInfo,
you can find out everything of importance for the Bean. You can also call the
methods for that Bean, even though you don’t have any other information
except the object (again, a feature of reflection).
A
more sophisticated Bean
This
next example is slightly more sophisticated, albeit frivolous. It’s a
canvas that draws a little circle around the mouse whenever the mouse is moved.
When you press the mouse, the word “Bang!” appears in the middle of
the screen, and an action listener is fired.
The
properties you can change are the size of the circle as well as the color,
size, and text of the word that is displayed when you press the mouse. A
BangBean
also has its own addActionListener( )
and removeActionListener( )
so you can attach your own listener that will be fired when the user clicks on
the
BangBean.
You should be able to recognize the property and event support:
//: BangBean.java
// A graphical Bean
package bangbean;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
public class BangBean extends Canvas
implements Serializable {
protected int xm, ym;
protected int cSize = 20; // Circle size
protected String text = "Bang!";
protected int fontSize = 48;
protected Color tColor = Color.red;
protected ActionListener actionListener;
public BangBean() {
addMouseListener(new ML());
addMouseMotionListener(new MML());
}
public int getCircleSize() { return cSize; }
public void setCircleSize(int newSize) {
cSize = newSize;
}
public String getBangText() { return text; }
public void setBangText(String newText) {
text = newText;
}
public int getFontSize() { return fontSize; }
public void setFontSize(int newSize) {
fontSize = newSize;
}
public Color getTextColor() { return tColor; }
public void setTextColor(Color newColor) {
tColor = newColor;
}
public void paint(Graphics g) {
g.setColor(Color.black);
g.drawOval(xm - cSize/2, ym - cSize/2,
cSize, cSize);
}
// This is a unicast listener, which is
// the simplest form of listener management:
public void addActionListener (
ActionListener l)
throws TooManyListenersException {
if(actionListener != null)
throw new TooManyListenersException();
actionListener = l;
}
public void removeActionListener(
ActionListener l) {
actionListener = null;
}
class ML extends MouseAdapter {
public void mousePressed(MouseEvent e) {
Graphics g = getGraphics();
g.setColor(tColor);
g.setFont(
new Font(
"TimesRoman", Font.BOLD, fontSize));
int width =
g.getFontMetrics().stringWidth(text);
g.drawString(text,
(getSize().width - width) /2,
getSize().height/2);
g.dispose();
// Call the listener's method:
if(actionListener != null)
actionListener.actionPerformed(
new ActionEvent(BangBean.this,
ActionEvent.ACTION_PERFORMED, null));
}
}
class MML extends MouseMotionAdapter {
public void mouseMoved(MouseEvent e) {
xm = e.getX();
ym = e.getY();
repaint();
}
}
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
// Testing the BangBean:
public static void main(String[] args) {
BangBean bb = new BangBean();
try {
bb.addActionListener(new BBL());
} catch(TooManyListenersException e) {}
Frame aFrame = new Frame("BangBean Test");
aFrame.addWindowListener(
new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
aFrame.add(bb, BorderLayout.CENTER);
aFrame.setSize(300,300);
aFrame.setVisible(true);
}
// During testing, send action information
// to the console:
static class BBL implements ActionListener {
public void actionPerformed(ActionEvent e) {
System.out.println("BangBean action");
}
}
} ///:~
The
first thing you’ll notice is that
BangBean
implements the Serializable
interface. This means that the application builder tool can
“pickle” all the information for the
BangBean
using serialization after the program designer has adjusted the values of the
properties. When the Bean is created as part of the running application, these
“pickled” properties are restored so that you get exactly what you
designed.
You
can see that all the fields are
private,
which is what you’ll usually do with a Bean – allow access only
through methods, usually using the “property” scheme.
When
you look at the signature for
addActionListener( ),
you’ll see that it can throw a TooManyListenersException.
This indicates that it is unicast,
which means it notifies only one listener when the event occurs. Ordinarily,
you’ll use multicast
events so that many listeners can be notified of an event. However, that runs
into issues that you won’t be ready for until the next chapter, so it
will be revisited there (under the heading “Java Beans revisited”).
A unicast event sidesteps the problem.
When
you press the mouse, the text is put in the middle of the
BangBean,
and if the
actionListener
field is not
null,
its
actionPerformed( )
is called, creating a new ActionEvent
object
in the process. Whenever the mouse is moved, its new coordinates are captured
and the canvas is repainted (erasing any text that’s on the canvas, as
you’ll see).
The
main( )
is added to allow you to test the program from the command line. When a Bean is
in a development environment,
main( )
will not be used, but it’s helpful to have a
main( )
in each of your Beans because it provides for rapid testing.
main( )
creates
a
Frame
and places a
BangBean
within it, attaching a simple
ActionListener
to the
BangBean
to print to the console whenever an
ActionEvent
occurs. Usually, of course, the application builder tool would create most of
the code that uses the Bean.
When
you run the
BangBean
through
BeanDumper
or put the
BangBean
inside a Bean-enabled development environment, you’ll notice that there
are many more properties and actions than are evident from the above code.
That’s because
BangBean
is inherited from
Canvas,
and
Canvas
is a Bean, so you’re seeing its properties and events as well.
Packaging
a Bean
Before
you can bring a Bean into a Bean-enabled visual builder tool, it must be put
into the standard Bean container, which is a JAR
(Java ARchive) file that includes all the Bean classes as well as a
“manifest” file that says “This is a Bean.” A manifest
file is simply a text file that follows a particular form. For the
BangBean,
the manifest file looks like this:
Manifest-Version: 1.0
Name: bangbean/BangBean.class
Java-Bean: True
The
first line indicates the version of the manifest scheme, which until further
notice from Sun is 1.0. The second line (empty lines are ignored) names the
BangBean.class
file, and the third says, “It’s a Bean.” Without the third
line, the program builder tool will not recognize the class as a Bean.
The
only tricky part is that you must make sure that you get the proper path in the
“Name:” field. If you look back at
BangBean.java,
you’ll see it’s in
package
bangbean
(and
thus in a subdirectory called “bangbean” that’s off of the
classpath), and the name in the manifest file must include this package
information. In addition, you must place the manifest file in the directory
above
the root of your package path, which in this case means placing the file in the
directory above the “bangbean” subdirectory. Then you must invoke
jar
from the same directory as the manifest
file, as follows:
jar
cfm BangBean.jar BangBean.mf bangbean
This
assumes that you want the resulting JAR file to be named
BangBean.jar
and that you’ve put the manifest in a file called
BangBean.mf. You
might wonder “What about all the other classes that were generated when I
compiled
BangBean.java?”
Well, they all ended up inside the
bangbean
subdirectory, and you’ll see that the last argument for the above
jar
command line is the
bangbean
subdirectory. When you give
jar
the name of a subdirectory, it packages that entire subdirectory into the jar
file (including, in this case, the original
BangBean.java
source-code file – you might not choose to include the source with your
own Beans). In addition, if you turn around and unpack the JAR file
you’ve just created, you’ll discover that your manifest file
isn’t inside, but that
jar
has created its own manifest file (based partly on yours) called
MANIFEST.MF
and
placed it inside the subdirectory
META-INF
(for “meta-information”). If you open this manifest file
you’ll also notice that digital signature information has been added by
jar
for
each file, of the form:
Digest-Algorithms: SHA MD5
SHA-Digest: pDpEAG9NaeCx8aFtqPI4udSX/O0=
MD5-Digest: O4NcS1hE3Smnzlp2hj6qeg==
In
general, you don’t need to worry about any of this, and if you make
changes you can just modify your original manifest file and re-invoke
jar
to create a new JAR file for your Bean. You can also add other Beans to the JAR
file simply by adding their information to your manifest.
One
thing to notice is that you’ll probably want to put each Bean in its own
subdirectory, since when you create a JAR file you hand the
jar
utility the name of a subdirectory and it puts everything in that subdirectory
into the JAR file. You can see that both
Frog
and
BangBean
are in their own subdirectories.
Once
you have your Bean properly inside a JAR file you can bring it into a
Beans-enabled program-builder environment. The way you do this varies from one
tool to the next, but Sun provides a freely-available test bed for Java Beans
in their “Beans Development Kit” (BDK) called the “beanbox.”
(Download the BDK from
www.javasoft.com.)
To place your Bean in the beanbox, copy the JAR file into the BDK’s
“jars” subdirectory before you start up the beanbox.
More
complex Bean support
You
can see how remarkably simple it is to make a Bean. But you aren’t
limited to what you’ve seen here. The Java Bean design provides a simple
point of entry but can also scale to more complex situations. These situations
are beyond the scope of this book but they will be briefly introduced here. You
can find more details at
http://java.sun.com/beans. One
place where you can add sophistication is with properties. The examples above
have shown only single properties, but it’s also possible to represent
multiple properties in an array. This is called an indexed
property
.
You simply provide the appropriate methods (again following a naming convention
for the method names) and the
Introspector
recognizes an indexed property so your application builder tool can respond
appropriately.
Properties
can be bound,
which means that they will notify other objects via a
PropertyChangeEvent.
The other objects can then choose to change themselves based on the change to
the Bean.
Properties
can be constrained,
which means that other objects can veto a change to that property if it is
unacceptable. The other objects are notified using a PropertyChangeEvent,
and they can throw a ProptertyVetoException
to prevent the change from happening and to restore the old values.
You
can also change the way your Bean is represented at design time:
- You
can provide a custom
property sheet for your particular Bean. The ordinary property sheet will be
used for all other Beans, but yours is automatically invoked when your Bean is
selected.
- You
can create a custom
editor for a particular property, so the ordinary property sheet is used, but
when your special property is being edited, your editor will automatically be
invoked.
- You
can provide a custom
BeanInfo
class for your Bean that produces information that’s different from the
default created by the
Introspector.
- It’s
also possible to turn “expert” mode on and off in all FeatureDescriptors
to distinguish between basic features and more complicated ones.
More
to Beans
There’s
another issue that couldn’t be addressed here. Whenever you create a
Bean, you should expect that it will be run in a multithreaded environment.
This means that you must understand the issues of threading, which will be
introduced in the next chapter. You’ll find a section there called
“Java Beans revisited” that will look at the problem and its
solution.