Constrained Properties

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;

Binding to Non-Specific Constrained Properties

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.

Vetoable property changes

Figure 4-2. Vetoable property changes

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.