 |
All hail the PropertyChangeListener
Posted by joshy on February 26, 2006 at 07:07 PM | Comments (4)
Often times when you are building an application you need to hook multiple components together in such a way that when one component changes others must do something. When you are building custom components there is often the temptation to build a custom set of listeners to go along with it. This seems like good component etiquette; after all this is how most of the javax.swing.* components are built. Still, that's a big pain to create new listener types that must be implemented, just for observing simple changes. Plus it tightly couples your classes which can make your code brittle when making changes later. There must be a better way. And there is!
PropertyChangeListeners
All Swing components implement the property pattern, meaning that there is an addPropertyChangeListener() method on all subclasses of JComponent. There is even a special version which lets you listen to just a particular property by passing the property name into addPropertyChangeListener() along with your listener. Most component properties are already set up to send events when they change. This includes things like the text of a JTextField and the background color of a JButton. JComponent also has a firePropertyChange() method which lets your custom Swing components send their own change events without ever having to much with event classes. The result is a very easy way to hook components together with a minimum of new code, and no new interfaces or classes.
Here's an example. I was working on a tiny bitmap tile editor. There is a "+" button which lets you make the grid a bit bigger. When it's pressed the grid editor panel needs to make itself bigger. When the grid becomes bigger a small label needs to reflect the new size of the grid. You can see a chain of events here that have to be implemented. I could create custom events (like GridSizeChangeEvents or something equally horrendous), but that's a lot of work for what boils down to "update yourself". Let's see how it would work using the PropertyChangeListener instead.
First, I create the editor. It is a custom JPanel with a method called setGridwidth(). Each time the grid width is changed it rebuilds the internal grid data and then fires a property change event about it. Chose to set the property from true to false, though it doesn't really matter what I send as long as it's different. I just want to indicate that a change has happened. I have un-creatively named the property "grid" and anyone else can listen to it. Here is what it looks like:
public class TileBuilderEditorPanel extends JPanel {
...
public void setGridwidth(int gridwidth) {
this.gridwidth = gridwidth;
rebuildGrid();
}
private void rebuildGrid() {
int[][] newgrid = new int[gridwidth][gridheight];
...
this.gridcells = newgrid;
repaint();
firePropertyChange("grid",false,true);
}
}
Back in my main class I want to update the label whenever the grid size changes. This is simple with an anonymous listener class like this:
editor.addPropertyChangeListener("grid", new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent propertyChangeEvent) {
viewer.repaint();
grid_size.setText(realeditor.getGridwidth() + " x "
+ realeditor.getGridheight());
}
});
Notice that the addPropertyChangeListener takes an argument to specify the property to listen for. By telling the component that I only want grid events I don't have put a check for the property name inside of my listener. Since I don't actually care what the grid changed to, just that it changed at all, I don't need to mess with the PropertyChangeEvent (though the information is there if I want it).
Now I can create an action listener on the "+" button that will make the grid width one cell bigger whenever the width_plus button is pressed.
private void width_plusActionPerformed(java.awt.event.ActionEvent evt) {
// TODO add your handling code here:
editor.setGridwidth(realeditor.getGridwidth()+1);
}
Note, the width_plusActionPerformed method is generated by Netbean's GUI builder and then attached to the actual width_plus button behind the scenes, which means I have even less code to write.
Now when I press the button it will call editor.setGridwidth() which will update the internal data structure, repaint itself, then fire a grid property event. An anonymous listener will look for the event and then update the label. It's that simple!
Here's what the (currently unfinished) application looks like:
For more on PropertyChangeListeners you can read the javadocs here
or this section of the JavaBeans tutorial.
- Josh
Bookmark blog post: del.icio.us Digg DZone Furl Reddit
Comments
Comments are listed in date ascending order (oldest first) | Post Comment
-
Most of the state of any component is stored in the model which most likely will not be a Component or JComponent but a cmplex data structure. Whenever data is updated in the model , the view has to change which is possible only by writing custom events and event handlers.
Posted by: psychostud on February 27, 2006 at 10:38 AM
-
It's true that complex components usually have complicated internal data structures, however I have found that my other components usually only want to listen to a particular part of the data structure, not the whole thing. PropertyChangeListenters let me do this. It's not the complete solution of course and there are certainly times where I do create a full model with events, but this is a nice tool to have in my box sometimes.
Posted by: joshy on February 27, 2006 at 10:06 PM
-
psychostud: In addition to what Josh added, I've found that using the JavaBeans pattern for my domain data is really useful. The oft untold part of the JavaBeans API is the use of property change events. I like to make my custom beans extend a common supertype that does nothing but add:
public addPropertyChangeListener(PropertyChangeListener pcl);
public removePropertyChangeListener(PropertyChangeListener pcl);
protected firePropertyChanged(String name, Object old, Object new);
I can then easily reuse property change listeners for all of my normal event notification. Even in situations where changing one property via setXXX ends up altering multiple properties, this is useful:
public void setXxx(Object xxx) {
Object old = this.xxx;
Object oldyyy = getYyy();
Object oldzzz = getZzz();
this.xxx = xxx;
firePropertyChange("xxx", old, this.xxx);
firePropertyChange("yyy", old, getYyy());
firePropertyChange("zzz", old, getZzz());
}
The other nice thing about property change listeners is that they are generic. I can attach one listener to a bean and watch many events. Perhaps I want to keep a log of all events fired. Or maybe I'm writing a generic library to track which properties have changed so that I know whether the bean has been edited and needs to be saved. Etc.
Posted by: rbair on February 28, 2006 at 08:46 AM
-
Hi Josh,
This includes things like the text of a JTextField
This seems like an obvious one that would be a bound property (a property
that fires a property change event is bound), but in truth the text
property of a JTextComponent is not bound. Here's the doc:
* Note that text is not a bound property, so no PropertyChangeEvent
* is fired when it changes. To listen for changes to the text,
* use DocumentListener.
From your code:
firePropertyChange("grid",false,true);
The bean spec says the name used in the property change event should
correspond to the name of the property. In your case the method is setGridwidth, which would correspond to a property name of gridwidth.
Additionally the values in the property change event should correspond to
that of the property. In your code you're firing the change with a Boolean, it
should be an int.
I'm sure you're code works as is, so why do these changes? Consistancy!
Without looking at your code I would expect that if you have a setGridwith method I could listen for changes by way of the gridwidth property and that the values would be Integers (or null). Additionally the beans spec
is really nothing more than a pattern, and to say your object is bean
implies you're following the pattern.
Here's what your code should look like:
public void setGridwidth(int gridwidth) {
int oldWidth = this.gridwidth;
this.gridwidth = gridwidth;
rebuildGrid();
firePropertyChange("gridwidth", oldWidth, gridwidth);
}
firePropertyChange will only notify listeners if the old and new values
differ, but not your rebuild code. You may as well further refine this
to be:
public void setGridwidth(int gridwidth) {
if (gridwidth != this.gridwidth) {
int oldWidth = this.gridwidth;
this.gridwidth = gridwidth;
rebuildGrid();
firePropertyChange("gridwidth", oldWidth, gridwidth);
}
}
-Scott
Posted by: zixle on March 08, 2006 at 06:34 AM
|