To this point we’ve assumed that all the property changes that take place are acceptable. Often this will not be the case; it’s possible that an attempt to set a property to a given value will be unacceptable and therefore rejected. The simple case is when the object that owns the property wants to reject the change, but there may be times when another object wants a chance to voice its disapproval. Properties that go through this approval process are known as constrained properties.
The
design pattern for setting and
getting constrained properties is similar to the design pattern for
properties that are not constrained. The difference is that the set
method declares that it throws the exception
java.beans.PropertyVetoException
. The method
signatures look as follows:
public <PropertyType> get<PropertyName>(); public void set<PropertyName>(<PropertyType> value) throws java.beans.PropertyVetoException;
Those objects that want to participate in
the approval process for a change to a constrained property must
implement the
java.beans.VetoableChangeListener
interface. This interface contains a
method called
vetoableChange()
that takes a single parameter of type
PropertyChangeEvent
. This method may throw the
java.beans.PropertyVetoException
if it wants to reject the change.
Objects that support constrained properties must provide methods for
registering and unregistering
VetoableChangeListener
objects. The methods should
look like the following:
public void addVetoableChangeListener(VetoableChangeListener p); public void removeVetoableChangeListener(VetoableChangelistener p);
It’s possible for the object that owns the property to reject a
change itself. So if a set<PropertyName>()
method is called with an unacceptable property value, the
java.beans.PropertyVetoException
can be thrown. If
the owning object does not reject the change, it must send
notifications to all registered
VetoableChangeListener
objects. This interaction
is shown in Figure 4.2.
A
VetoableChangeEvent
for a constrained property should be
fired before the actual value of the property has changed. This gives
any VetoableChangeListener
a chance to reject the
change when its vetoableChange()
method is called.
The event source must catch the
java.beans.PropertyVetoException
. If a listener
throws the exception, the source object must fire a new
Vetoable-ChangeEvent
to all the registered
VetoableChangeListener
objects, using the current
value for the property. This is necessary because if there were
multiple listeners, some of them may already have been notified of
the change that was subsequently rejected. So the second round of
events gives those listeners the opportunity to revert to the old
value. In this case, any PropertyVetoException
that is thrown can be caught and ignored. The convention that the
second round of PropertyVetoExceptions
can be
ignored prevents infinite loops in which no satisfactory value can be
found.
When a listener is informed of a change to a constrained property, it
should not assume that the change has already taken place, because
some other object may veto the change. If this happens, the listener
will be notified again with the property reverted to its old value.
Unfortunately, there is no way for the listener to know if the change
will be universally accepted at the time of the first notification.
So what should the listening object do? For example, let’s say
that we have a dialog box that includes a component that allows the
user to enter the font size to use for all of the controls on the
dialog. This font sizer may have a constrained property named
FontSize. All of the controls on the dialog are
registered listeners implementing the
java.beans.VetoableChangeListener
interface, and
each control has its own notion of the maximum font size. When each
is notified of a request to change the value of the
FontSize property, each compares the proposed
value to its own maximum and throws the
java.beans.PropertyVetoException
if the value is
too high. The control that rejects the change may do so after other
controls have already been notified, in which case the previously
notified controls may already have repainted themselves with the new
font size. Now that another control has subsequently rejected the
change, all of the controls will be notified again with the old font
size value. The result is that the controls have to repaint
themselves again. This is certainly not visually appealing, and is
potentially time-consuming as well.
The JavaBeans architecture addresses this issue by stating that
properties can be both bound and constrained at the same time. In
this case the property owner should fire a
VetoableChangeEvent
before the property value is
actually changed. If no VetoableChangeListener
rejects the change, the property value should be changed and then the
PropertyChangeEvent
should be fired. The dialog
controls I just discussed could be registered with the font sizer as
both a PropertyChangeListener
and a
VetoableChangeListener
at the same time. This
would allow one of the controls to reject the change when the
vetoableChange()
method is called, but defer
reacting to it until the propertyChange()
method
gets called.
Another way to deal with this kind of problem is to have each control inform the font sizer of its maximum font size at initialization time. This way the font sizer itself will have the opportunity to reject an unacceptable change to the FontSize property before any of the listening objects are ever notified.
The class
java.beans.VetoableChangeSupport
is provided to make managing constrained
properties easier. You can either inherit from this class or use an
instance of it. The public methods of
VetoableChangeSupport
are shown here:
public class java.beans.VetoableChangeSupport implements java.io.Serializable { // construct the object public VetoableChangeSupport(Object source); // add a vetoable change listener public synchronized void addVetoableChangeListener(VetoableChangeListener l); // fire a vetoable change event to any listeners public void fireVetoableChange(String propertyName, Object oldValue, Object newValue) throws java.beans.PropertyVetoException; // remove a vetoable change listener public synchronized void removeVetoableChangeListener(VetoableChangeListener l); }
The
fireVetoableChange()
method performs a valuable service. It fires
the VetoableChangeEvent
to all registered
listeners, and if any of them veto the change it refires the event to
all listeners to revert them to the old property value and then
rethrows the java.beans.PropertyVetoException
.
This can be a real convenience, as we will see in the next example.
Let’s look again at our Thermometer
class.
If an instance of this class is used to control a heating device, we
might have a property of the Thermometer
named
MinimumTemperature. This is a read/write
property that is used to set the temperature threshold that will
trigger the turning on and off of the connected heating device. When
the temperature drops 1 degree below this threshold, the heater will
be turned on. The heater will be shut off when the temperature
reaches the MinimumTemperature value.
Let’s constrain the MinimumTemperature
value to be no less than 10 degrees Celsius (50 degrees Fahrenheit),
the minimum value that the Thermometer
class
itself will allow. The relevant additions to the
Thermometer
class are shown next.
package BeansBook.Simulator;
import java.beans.*;
public class Thermometer implements PropertyChangeListener
{
// the minimum temperature threshold value defaults to
// 15 degrees Celsius
protected double minTemperature = 15.0;
// the support object for constrained properties
protected VetoableChangeSupport constrainedHandler;
// constructor
Thermometer(Temperature temperature)
{
theTemperature = temperature;
// register for property change events
theTemperature.addPropertyChangeListener(this);
// construct the constrained property support object
constrainedHandler = new VetoableChangeSupport(this);
}
[The existing code goes here]
// add a VetoableChangeListener
public void addVetoableChangeListener(VetoableChangeListener l)
{
// defer to the support handler
constrainedHandler.addVetoableChangeListener(l);
}
// remove a VetoableChangeListener
public void removeVetoableChangeListener(VetoableChangeListener l)
{
// defer to the support handler
constrainedHandler.removeVetoableChangeListener(l);
}
// get the MinimumTemperature property value
public double getMinimumTemperature()
{
return minTemperature;
}
// set the MinimumTemperature property value
public void setMinimumTemperature(double newVal)
throws PropertyVetoException
{
// let's check against our own criterion first
if (newVal < 10.0)
{
PropertyChangeEvent e = new PropertyChangeEvent(this,
"MinimumTemperature", null, new Double(newVal));
throw new PropertyVetoException("Bad MinimumTemperature", e);
}
// defer to the support handler
constrainedHandler.fireVetoableChange("MinimumTemperature",
new Double(minTemperature), new Double(newVal));
// if the previous call did not throw an exception, then we are
// free to make the change
minTemperature = newVal;
}
}
The setMinimumTemperature()
method first checks to
see that the new value is not less than the absolute minimum value of
10 degrees Celsius. If it’s not, the
fireVetoableChange()
method is called on the
support object. This method handles all of the details of notifying
the registered VetoableChangeListener
objects and
reverting them to the old value if any of them reject the change. If
the change is rejected, the PropertyVetoException
will be thrown. If not, the last line saves the new value of
the
property.
Get Developing Java Beans now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.