|
|
|
|
JGoodies Binding is an open-source framework for binding Java Beans to UI components. The JGoodies Binding API:
This article uses version 1.0 of the JGoodies Binding API. It is available for download at the JGoodies Binding home page. Just follow the links to locate version 1.0, then download the zip file.
Simply expand the zip file and add binding-1.0.jar to your classpath. It requires JDK 1.4 or later and the tutorials also require JGoodies Forms.
Binding values to and from your domain model to UI components can be tedious. Let's look at a typical Swing application that binds properties from a simple Java bean to UI components.
Figure 1: Typical GUI Application
public class CustomerBean extends Model {
public static final String NAME_PROPERTY = "name";
public static final String PREFERRED_PROPERTY = "preferred";
private String name = "";
private boolean preferred;
public String getName() {
return name;
}
public void setName(String newName) {
String oldValue = name;
name = newName;
firePropertyChange(NAME_PROPERTY, oldValue, newName);
}
public boolean isPreferred() {
return preferred;
}
public void setPreferred(boolean isPreferred) {
boolean oldValue = preferred;
preferred = isPreferred;
firePropertyChange(PREFERRED_PROPERTY, oldValue, isPreferred);
}
}
The CustomerBean class extends the Model
class provided by the Binding API. The Model class
simplifies adding change support for bound and constrained Bean
properties without having to duplicate the
addPropertyChangeListener,
removePropertyChangeListener and
firePropertyChange methods in all of our Bean classes.
The drawback of using the Model class is the single
inheritance limitation.
In order for the Binding framework to be able to update UI
components when domain object properties change, the
CustomerBean properties must be bound properties.
Changes to bound properties cause a
PropertyChangeEvent to be fired when the property
value changes.
The CustomerBean class provides constants for the
name and preferred properties. This is a
good convention to follow to avoid using "magic" strings to
refer to the property names of the CustomerBean Bean
throughout our code.
public class TypicalGUI extends JPanel
implements ActionListener, ItemListener, PropertyChangeListener {
....
public TypicalGUI() {
preferredValueCB.setEnabled(false);
nameFld.addActionListener(this);
preferredCB.addItemListener(this);
myCustomerBean.addPropertyChangeListener(this);
// set components initial values
modelToView();
}
public void actionPerformed(ActionEvent e) {
viewToModel();
}
public void itemStateChanged(ItemEvent e) {
viewToModel();
}
public void propertyChange(PropertyChangeEvent evt) {
modelToView();
}
private void modelToView() {
nameFld.setText(myCustomerBean.getName());
preferredCB.setSelected(myCustomerBean.isPreferred());
}
private void viewToModel() {
myCustomerBean.setName(nameFld.getText());
myCustomerBean.setPreferred(preferredCB.isSelected());
}
....
}
In this example the name and preferred
properties of the CustomerBean are bound to a
JTextField and JCheckBox. In order to
update the CustomerBean with UI component state changes,
component specific event listeners were added to each component.
In order to update the components with state changes from the
CustomerBean, a PropertyChangeListener
was added to the CustomerBean object.
Here's a sequence diagram of what occurs when the
JTextField's contents are modified.
Figure 2: Typical GUI Sequence Diagram
This approach to writing applications is easy to implement and understand but it leads to code bloat and it is error prone to implement the same boilerplate code through out an application.
The Binding API is centered around the ValueModel
interface.
public interface ValueModel {
Object getValue();
void setValue(Object o);
void addValueChangeListener(PropertyChangeListener l);
void removeValueChangeListener(PropertyChangeListener l);
}
The ValueModel interface abstracts getting and setting
an individual value, and provides the ability to observe changes to
the value with PropertyChangeListeners. There are three
"glue code" classes we can use to bind object properties to
UI components:
PropertyAdapter
BeanAdapter
PresentationModel
The PropertyAdapter converts a single Java Bean property
into a ValueModel implementation. Here's an example
using the PropertyAdapter class to bind properties from
the CustomerBean class to UI components.
public class PropertyAdapterExample extends JPanel {
....
private void createComponents() {
myCustomerBean = new CustomerBean();
ValueModel stringValueModel = new PropertyAdapter(
myCustomerBean, CustomerBean.NAME_PROPERTY, true);
ValueModel boolValueModel = new PropertyAdapter(
myCustomerBean, CustomerBean.PREFERRED_PROPERTY, true);
nameFld = BasicComponentFactory.createTextField(stringValueModel);
preferredCB = BasicComponentFactory.createCheckBox(boolValueModel,
"Preferred");
}
...
}
The example above creates PropertyAdapters that bind
the CustomerBean name and
preferred properties to a JTextField and
JCheckBox, respectively. The property adaptor observes
changes made in the bound UI component and changes made to
the bean property (because the setter methods in the CustomerBean
fire PropertyChangeEvents). If a user types a new value
into the text field, the name property value changes.
If the programmer sets the value of the name property,
the view updates.
Here's a sequence diagram of what occurs when the
JTextField's contents are modified.
Figure 3: PropertyAdapter Sequence Diagram
The BasicComponentFactory is a convenience class
provided by the framework to create components bound to a
property value. It creates and returns standard Swing components.
I will discuss the BasicComponentFactory class in more
detail later in the article and also show how to manually bind
properties to UI components.
The boolean parameter passed to the
PropertyAdapter constructor instructs it to register
itself as a PropertyChangeListener for the property to
which it is bound. The PropertyAdapter class utilizes
standard JavaBean introspection to invoke the bean's getter and
setter methods for the property specified in the constructor. It
has overloaded constructors for specifying getter and setter method
names for properties that do not adhere to JavaBean conventions.
This example does not contain any code to explicitly bind the bean
properties to the UI components. The framework handles the
binding, making our code easier to read and maintain. Using the
Binding framework makes debugging somewhat more difficult. As
evident by the sequence diagram above, it is much harder to
visualize what is happening in the code, due to all
of the PropertyChangeEvents being fired back and forth.
The benefits of using the Binding framework such as code reduction
and easier maintenance, more than make up for this one drawback.
Creating PropertyAdapters for each bound property is
a lot work. The BeanAdapter class provides
ValueModel adapters for multiple Java Bean properties.
Here's the previous example modified to use the
BeanAdapter class.
public class BeanAdapterExample extends JPanel {
....
private void createComponents() {
myCustomerBean = new CustomerBean();
BeanAdapter beanAdapter = new BeanAdapter(myCustomerBean, true);
ValueModel stringValueModel = beanAdapter.getValueModel(
CustomerBean.NAME_PROPERTY);
ValueModel boolValueModel = beanAdapter.getValueModel(
CustomerBean.PREFERRED_PROPERTY);
nameFld = BasicComponentFactory.createTextField(stringValueModel);
preferredCB = BasicComponentFactory.createCheckBox(boolValueModel,
"Preferred");
}
....
}
The
PresentationModel provides binding functionality
in the same manner as the BeanAdapter class but it
also can be used to
implement the
Presentation Model pattern. The Presentation Model is described
in the upcoming addition to Martin Fowler's
"Patterns of Enterprise Application Architecture". Fowler's web site
states that the purpose of the Presentation Model pattern is to
"Represent the state and behavior of the presentation independently
of the GUI controls used in the interface. Presentation Model pulls
the state and behavior of the view out into a model class that is
part of the presentation. The Presentation Model coordinates with
the domain layer and provides an interface to the view that
minimizes decision-making in the view. The view either stores all
its state in the Presentation Model or synchronizes its state with
Presentation Model frequently".
Discussion of the Presentation Model pattern itself is enough to
take up an entire article. This article will focus on how the
PresentationModel class provides support for
buffering changes to its underlying bean. The following example
shows how the PresentationModel can be used to add
buffering support to our previous example.
Figure 4: Presentation Model GUI
public class PresentationModelExample extends JPanel {
....
private void createComponents() {
MyPresentationModel presentationModel =
new MyPresentationModel(new CustomerBean());
nameFld = BasicComponentFactory.createTextField(
presentationModel.getBufferedModel(
CustomerBean.NAME_PROPERTY));
preferredCB = BasicComponentFactory.createCheckBox(
presentationModel.getBufferedModel(
CustomerBean.PREFERRED_PROPERTY), "Preferred");
applyBtn = new JButton(presentationModel.getApplyAction());
resetBtn = new JButton(presentationModel.getResetAction());
}
...
}
public class MyPresentationModel extends PresentationModel {
....
public MyPresentationModel(CustomerBean customerBean) {
super(customerBean);
applyAction = new ApplyAction();
resetAction = new ResetAction();
}
public Action getApplyAction() {
return applyAction;
}
public Action getResetAction() {
return resetAction;
}
private class ApplyAction extends AbstractAction {
public ApplyAction() {
super("Apply");
}
public void actionPerformed(ActionEvent e) {
triggerCommit();
}
}
private class ResetAction extends AbstractAction {
public ResetAction() {
super("Reset");
}
public void actionPerformed(ActionEvent e) {
triggerFlush();
}
}
}
This example creates a MyPresentationModel that provides
the capability to buffer changes to the CustomerBean by
calling the getBufferedModel method. This will return
a ValueModel that will buffer changes until the
triggerCommit method is invoked. The
triggerCommit method will be invoked when the user
clicks the "Apply" button. The components can have their state
restored to the properties original values by invoking the
triggerFlush method on the
PresentationModel. The triggerFlush
method will be invoked when the user clicks the "Reset" button.
The PropertyConnector class keeps two bound JavaBean
properties synchronized. The previous example could be improved by
enabling the Apply button only when changes have occurred.
We can easily implement this behavior using a
PropertyConnector in the
MyPresentationModel class.
public class MyPresentationModel extends PresentationModel {
public MyPresentationModel(CustomerBean customerBean) {
super(customerBean);
....
// Disable the applyAction by default.
applyAction.setEnabled(false);
// We have to use a magic string for the "enabled" property
// because the javax.swing.Action interface does not define a
// constant for it.
PropertyConnector.connect(this, PROPERTYNAME_BUFFERING,
applyAction, "enabled");
}
....
}
The PropertyConnector class is simple and powerful.
With one line of code we have synchronized the
buffering property of the PresentationModel
with the enabled property of
the JButton. This causes the Apply button to enable
when changes are made to the underlying bean.
Here's a sequence diagram of what occurs when the user enters a new name value for the Customer.
Figure 5: PropertyConnector Sequence Diagram
So far the examples used the BasicComponentFactory
class to create all bound components. It is important to understand
what the BasicComponentFactory is doing behind the scenes.
The Binding API contains adapter classes for adapting the
ValueModel interface to Swing model classes.
For instance, Binding uses the ToggleButtonAdapter class
for adapting the ValueModel interface to the
JToggleButton.ToggleButtonModel interface required for
the JCheckBox's model. The following example shows how
to bind a JCheckBox to a boolean property without using
the BasicComponentFactory class.
public class WithoutBasicComponentFactoryExample extends JPanel {
....
private void createComponents() {
....
BufferedValueModel preferredValueModel =
presentationModel.getBufferedModel(
CustomerBean.PREFERRED_PROPERTY);
ButtonModel btnModel = new ToggleButtonAdapter(
preferredValueModel);
preferredCB = new JCheckBox("Preferred");
preferredCB.setModel(btnModel);
....
}
....
}
Seeing this example makes us appreciate how much work the
BasicComponentFactory saves us. It is worth mentioning
that the BasicComponentFactory class uses the
Bindings class to setup the bindings for the components
it creates. You may need to use the Bindings class
yourself to perform binding for custom components subclassed
from the standard Swing components.
The Binding API contains adapters for many other Swing components. The following table lists Swing components with their associated Binding adapters.
| Component | Adapter |
| JCheckBox | ToggleButtonAdapter |
| JCheckBoxMenuItem | ToggleButtonAdapter |
| JFormattedTextField | PropertyConnector |
| JLabel | PropertyConnector |
| JPasswordField | DocumentAdapter |
| JSlider | BoundedRangeAdapter |
| JTextArea | DocumentAdapter |
| JTextField | DocumentAdapter |
| JToggleButton | ToggleButtonAdapter |
| JRadioButton | RadioButtonAdapter |
| JRadioButtonMenuItem | RadioButtonAdapter |
Working with the various adapters is straightforward and is done
similarly to our previous examples. The two adapters that are a
little different and worth a closer look are the
RadioButtonAdapter and
BoundedRangeAdapter.
The Binding API provides the RadioButtonAdapter class
to bind values to a JRadioButton.
public class RadioButtonExample {
....
private void createComponents() {
ValueModel favoriteTeamModel = new PropertyAdapter(
new FavoriteTeamBean(),
FavoriteTeamBean.FAVORITE_TEAM_PROP,
true);
cardsBtn = createRadioBtn(
favoriteTeamModel, "Cardinals", "Cardinals");
ramsBtn = createRadioBtn(
favoriteTeamModel, "Rams", "Rams");
bluesBtn = createRadioBtn(
favoriteTeamModel, "Blues", "Blues");
}
private JRadioButton createRadioBtn(
ValueModel valueModel, Object choice, String text) {
JRadioButton radioBtn = new JRadioButton(text);
RadioButtonAdapter rbAdapter = new RadioButtonAdapter(
valueModel, choice);
radioBtn.setModel(rbAdapter);
return radioBtn;
}
....
}
The RadioButtonAdapter binds a
JRadioButton to the ValueModel for the
favoriteTeam property. The
RadioButtonAdapter also contains an associated choice
Object. When a radio button is selected, the value of
its adapter's choice Object is set to the
favoriteTeam property of the bean.
For instance, if the cardsBtn radio button is
selected the favoriteTeam property of the
FavoriteTeamBean object will be set to
"Cardinals".
The Binding API provides the BoundedRangeAdapter class
to bind values to a JSlider.
public class SliderExample {
....
private void createComponents() {
ValueModel intValueModel = new PropertyAdapter(
new IntBean(), IntBean.VALUE_PROP, true);
BoundedRangeAdapter boundedRangeAdapter =
new BoundedRangeAdapter(intValueModel, 0, 0, 1000);
slider = new JSlider(boundedRangeAdapter);
}
....
}
The BoundedRangeAdapter binds a
JSlider to the value property of the
IntBean. It is provided
an extent, minimum and maximum values for the range just as you
would provide to a DefaultBoundedRangeModel. As the
JSlider is moved on the screen, the value property of the
IntBean will be kept in sync with the JSlider model's
value.
The SelectionInList class provides the capability to
bind lists of objects to UI components. Lists of objects can be
bound to JLists, JComboBoxes and
JTables. The SelectionInList implements
the ValueModel interface for a list of objects, the
selected object in the list, and the selected index of the list.
Here's an example of the SelectionInList class in
action.
public class ListExample {
....
private java.util.List<String> createData() {
java.util.List<String> data = new ArrayList<String>();
data.add("Cardinals");
data.add("Rams");
data.add("Blues");
return data;
}
private void createComponents() {
TeamBean teamBean = new TeamBean();
BeanAdapter beanAdapter = new BeanAdapter(teamBean, true);
ValueModel teamNameValueModel = beanAdapter.getValueModel(
TeamBean.NAME_PROP);
SelectionInList selectionList =
new SelectionInList(createData(), teamNameValueModel);
jlist = BasicComponentFactory.createList(selectionList);
textFld = BasicComponentFactory.createTextField(
teamNameValueModel);
}
....
}
This example provides a list of team names to select as your
favorite team. The selected name will be bound to the name property
of a TeamBean instance. The selected name will be
displayed in the JTextField. The
SelectionInList class implements the
ListModel interface and can be set as the data model
for the JList. Also by using the
BasicComponentFactory.createList method (which in
turn uses Binding.bind) a
SelectionModel for the JList is created
that is bound to the selected index of the
SelectionInList.
Here's the same example using a JComboBox instead of a
JList.
public class ComboBoxExample {
....
private void createComponents() {
TeamBean teamBean = new TeamBean();
BeanAdapter beanAdapter = new BeanAdapter(teamBean, true);
ValueModel teamNameValueModel = beanAdapter.getValueModel(
TeamBean.NAME_PROP);
ComboBoxModel cbModel = new ComboBoxAdapter(createData(),
teamNameValueModel);
comboBox = new JComboBox(cbModel);
textFld = BasicComponentFactory.createTextField(teamNameValueModel);
}
....
}
JGoodies Binding makes binding domain object properties to UI components a breeze. This article has demonstrated how using the Binding API can make your code easier to implement and maintain. If you find the Binding API useful you should consider licensing the JGoodies Swing Suite. The Suite is built on top of the various open source JGoodies libraries and provides a strong foundation and quick start for building rich client applications in Swing.
OCI is the leading provider of Object Oriented technology training in the Midwest. More than 3,000 students participated in our training program over the last 12 months. Targeted toward Software Engineers and the development community, our extensive program of over 50 hands-on workshops is delivered to corporations and individuals throughout the U.S. and internationally. OCI's Educational Services include Group Training events and Open Enrollment classes.
For further information regarding OCI's Educational Services programs, please visit our Educational Services section on the web or contact us at training@ociweb.com.
|
|
|