July 31, 2014
Hot Topics:
RSS RSS feed Download our iPhone app

Typed and Targeted Property Change Events in Java

  • January 29, 2007
  • By Garret Wilson
  • Send Email »
  • More Articles »

Targeted Property Change Events

Our current scheme works well for changing automobile engines, and for objects with simple properties the standard JavaBeans bound property paradigm gets the job done. But for complex properties—those properties the values of which have their own properties that can change—this paradigm has it limitations. What if some outside object want to listen for changes in the engine size as well, for example?

One solution would be for the external object to register a property change listener directly with the Automobile's engine property value. This quickly becomes cumbersome, because the value of the engine property can change. The external class must therefore also register a property change listener with the Automobile instance, listening for the "engine" property to change, and then unregister its event listener from the old engine and register it with the new one. Remembering to do this (not to mention documenting that this must be done) becomes unmanageable. Wouldn't it be better if we could do this once in the Automobile class, and propagate any property changes in the Engine to the property change listeners of Automobile?

This is a valid approach, but it leaves one detail unresolved: How does a PropertyChangeListener determine whether a property change was performed on the Automobile or on its contained Engine? We could have the Automobile forward the Engine property change events unchanged, so that PropertyChangeEvent.getSource() would equal the Engine instance as appropriate, but if one property change listener is listening to several Automobiles it prevents them from using PropertyChangeEvent.getSource() to find which Automobile's Engine changed properties. Besides, the contract of PropertyChangeEvent.getSource() (as stated by the API of java.util.EventObject) is that "All Events are constructed with a reference to the object, the "source", that is logically deemed to be the object upon which the Event in question initially occurred upon." While this is slightly ambiguous in our case as to whether "initially occurred upon" refers to the object that changed its properties or the object that fired the event, it seems pragmatically useful for a PropertyChangeListener to expect PropertyChangeEvent.getSource() to refer to the object with which the listener registered. This would favor the "object that fired the event as source" interpretation.

So there needs to be a separate method of PropertyChangeEvent that indicates the object the property of which changed, regardless of which object is firing the event. This value would stay the same regardless of how many times the event was propagated. Indeed it is possible the authors of Java had something similar in mind for the future, for the API of java.beans.PropertyChangeEvent.getPropagationId() states:

The "propagationId" field is reserved for future use. In Beans 1.0 the sole requirement is that if a listener catches a PropertyChangeEvent and then fires a PropertyChangeEvent of its own, then it should make sure that it propagates the propagationId field from its incoming event to its outgoing event.

That sounds a lot like the functionality we want, but with no more information on semantics, and with no conventions relating to this field in widespread use, it seems foolhardy to hijack this field for own purposes. If future versions of Java were to use the propagationId field for some other purpose, it could cause serious backwards-compatibility issues with our code. It again seems that the safest route is to add in our own extensions to the JavaBeans component architecture.

We can start by creating an interface that can be mixed in with any event and that indicates the target of the event—the object to which the action was directed that caused the event, whether or not that object actually fired the event object in question. This is done in com.garretwilson.event.TargetedEvent, which could be used with any type of event, such as a property change event or an action event:

/**An interface for an event that knows its target,
    or the object to which the event applies
    (which may be different than the object that fired the event).
@author Garret Wilson
*/
public interface TargetedEvent
{

  /**Returns the object to which the event applies.
  This may be a different than <dfn>source</dfn>,
      which is the object that generated this event instance.
  @return The target of the event, or <code>null</code>
      if the event target is not known.
  */
  public Object getTarget();
}

We can now make GenericPropertyChangeEvent<V>, which we created earlier, compatible with our new targeted event framework. We'll add new copy constructors that will allow new events to be created with different sources while still maintaining the same target. We'll also upgrade the old source-only constructors to automatically set the target to the same value as the source, for those instances in which there is no need to maintain a separate target and the class is being used traditionally:

/**A property value change event is a Java Beans
    property change event retrofitted to use generics
    to cast to proper value type.
This event is also <dfn>targeted</dfn>,
    specifying an event target which may or may not
    be the same as the source object firing this event.
@param <V> The type of property value.
@author Garret Wilson
*/
public class GenericPropertyChangeEvent<V>
    extends PropertyChangeEvent implements TargetedEvent
{

  /**The target of the event, or <code>null</code>
      if the event target is not known.*/
  private final Object target;

    /**Returns the object to which the event applies.
    This may be a different than <dfn>source</dfn>,
        which is the object that generated this event instance.
    @return The target of the event.
    */
    public Object getTarget() {return target;}

  /**Source  and property name constructor with old and new values.
  The target will be set to be the same as the given source.
  @param source The bean that fired the event.
  @param propertyName The programmatic name of the property
      that was changed.
  @param oldValue The old value of the property,
      or <code>null</code> if no old value is not available.
  @param newValue The new value of the property,
      or <code>null</code> if the new value is not available.
  */
  public GenericPropertyChangeEvent(final Object source,
      final String propertyName, final V oldValue, V newValue)
  {
    this(source, source, propertyName, oldValue, newValue);
  }

  /**Source, target, and property name constructor
      with old and new values.
  @param source The bean that fired the event.
  @param target The target of the event.
  @param propertyName The programmatic name of the property
      that was changed.
  @param oldValue The old value of the property,
      or <code>null</code> if no old value is not available.
  @param newValue The new value of the property,
      or <code>null</code> if the new value is not available.
  */
  public GenericPropertyChangeEvent(final Object source,
      final Object target, final String propertyName,
      final V oldValue, V newValue)
  {
    super(source, propertyName, oldValue, newValue);
    this.target= target;
  }

  /**Property change event copy constructor.
  @param propertyChangeEvent A property change event the values
      of which will later cast to this class' generic type.
  */
  public GenericPropertyChangeEvent(final PropertyChangeEvent
      propertyChangeEvent)
  {
    this(propertyChangeEvent.getSource(), propertyChangeEvent);
  }

  /**Property change event copy constructor
      that specifies a different source.
  If the property change event is a {@link TargetedEvent},
      the target is copied from that event;
      otherwise, the given source will be used as the target.
  @param source The object on which the event initially occurred.
  @param propertyChangeEvent A property change event the values
      of which will later cast to this class' generic type.
  */
  @SuppressWarnings("unchecked")
  public GenericPropertyChangeEvent(final Object source,
      final PropertyChangeEvent propertyChangeEvent)
  {
    this(source, propertyChangeEvent instanceof TargetedEvent
        ? ((TargetedEvent)propertyChangeEvent).getTarget() : source,
        propertyChangeEvent.getPropertyName(),
        (V)propertyChangeEvent.getOldValue(),
        (V)propertyChangeEvent.getNewValue());
    setPropagationId(propertyChangeEvent.getPropagationId());
  }

  /**@return The old value of the property,
      or <code>null</code> if the old value is not available.*/
  @SuppressWarnings("unchecked")
  public V getOldValue()
  {
    return (V)super.getOldValue();
  }

  /**@return The new value of the property,
      or <code>null</code> if the new value is not available.*/
  @SuppressWarnings("unchecked")
  public V getNewValue()
  {
    return (V)super.getNewValue();
  }
}

The Automobile class can now repeat any property changes of the contained Engine object by creating a new event object with a new source, but keeping the same target. This is best done using the GenericPropertyChangeEvent<V> copy constructor above that takes a different source as one of its arguments. The new Automobile class is shown below:

public class Automobile
{
  protected final PropertyChangeSupport propertyChangeSupport;
  protected final PropertyChangeListener repeatPropertyChangeListener=
      new PropertyChangeListener()
      {
        public void propertyChange(final PropertyChangeEvent
            propertyChangeEvent)
            {
              final PropertyChangeEvent repeatPropertyChangeEvent=
                  new GenericPropertyChangeEvent<Object>(
                  Automobile.this, propertyChangeEvent);
              propertyChangeSupport.firePropertyChange(
                  repeatPropertyChangeEvent);
            }
      };
  private Engine engine;

  /**Default constructor with a default engine.*/
  public Automobile()
  {
    propertyChangeSupport=new PropertyChangeSupport(this);
    engine=new Engine();
    engine.addPropertyChangeListener(repeatPropertyChangeListener);
    addPropertyChangeListener("engine",
        new AbstractGenericPropertyChangeEvent<Engine>()
        {
          public void propertyChange(
              final GenericPropertyChangeEvent<Engine>
              propertyChangeEvent)
          {
            propertyChangeEvent.getOldValue().
                removePropertyChangeListener(repeatPropertyChangeListener);
            propertyChangeEvent.getNewValue().
                addPropertyChangeListener(repeatPropertyChangeListener);
          }
        });
  }

  /**@return The car's current engine.*/
  public Engine getEngine() {return engine;}

  /**Sets the car's engine.
  @param newEngine The new engine.
  @exception NullPointerException if the engine
      is <code>null</code>.
  */
  public void setEngine(final Engine newEngine)
  {
    if(newEngine==null)
    {
      throw new NullPointerException("No engine provided.");
    }
    if(!engine.equals(newEngine))
    {
      final Engine oldEngine= engine;
      engine=newEngine;
      final PropertyChangeEvent propertyChangeEvent=
          new GenericPropertyChangeEvent<Engine>(this,
                                                       "engine",
                                                       oldEngine,
                                                       newEngine);
      propertyChangeSupport.firePropertyChange(propertyChangeEvent);
    }
  }

  /**Add a property change listener for a specific property.
  @param propertyName The name of the property to listen on.
  @param listener The <code>PropertyChangeListener</code>
      to be added.
  */
  public void addPropertyChangeListener(final String propertyName,
      final PropertyChangeListener listener)
  {
    propertyChangeSupport.addPropertyChangeListener(propertyName,
                                                    listener);
  }

  /**Remove a property change listener for a specific property.
  @param propertyName The name of the property that was listened on.
  @param listener The <code>PropertyChangeListener</code>
      to be removed
  */
  public void removePropertyChangeListener(final String propertyName,
      final PropertyChangeListener listener)
  {
    propertyChangeSupport.removePropertyChangeListener(propertyName,
                                                       listener);
  }
}

There are three important parts to this property change repeater pattern. The first is the repeater PropertyChangeListener, which can be registered with any class. It takes whatever property change that has occurred and refires the event after creating a new event with an updated source of the object firing the event. (It is important to note that Automobile.this is used to represent the Automobile instance as the source rather than the anonymous inner class.) Secondly, the repeater is registered with the Engine instance as soon as it is created. Thirdly, because the engine property can change, a separate listener is registered with the Automobile instance itself for notification of when the engine property value changes. When this happens, the repeater is unregistered from the old Engine and then registered with the new Engine.

Now an external class can be notified of property changes of the Engine property of the Automobile by registering with the Automobile either for all property change events or only for specific Engine-specific properties. If there is ever a question of whether a property of Automobile or Engine has changed, this may be resolved by examining TargetedEvent.getTarget() if the PropertyChangeEvent in question implements TargetedEvent.

The concept of a targeted event is especially useful when an object can have an arbitrary number of contained objects over an arbitrary number of hierarchical levels. As an example, consider the TreeControl component in the com.guiseframework.component package of the Guise™ Internet application framework. A TreeControl contains a root TreeNodeModel<V> which itself can contain an arbitrary number of TreeNodeModel<V> children, each of which in turn can contain other tree nodes, and so on. A common use case is to listen for the selection by the user of one of the nodes in a tree.

An application simply cannot practically register and unregister a property change listeners with each TreeNodeModel<V> as it is added or removed from the tree. The Guise™ TreeControl therefore uses the typed, targeted property change architecture described above to propagate property changes of each TreeNodeModel<V> up the tree hierarchy until it is finally repeated from the TreeControl. Any property change event fired from the control will properly indicate the TreeControl as its source, and indicate the tree node with the changed property as its target. An application can therefore listen directly to the TreeControl to be notified when a tree node is selected, for example:

final TreeControl treeControl=new TreeControl();
treeControl.addPropertyChangeListener(
    TreeNodeModel.SELECTED_PROPERTY,
    new AbstractGenericPropertyChangeListener<Boolean>()
    {
      public void propertyChange(
          final GenericPropertyChangeEvent<Boolean>
          propertyChangeEvent)
      {
        if(propertyChangeEvent.getTarget() instanceof TreeNodeModel)
        {
          final TreeNodeModel<?> treeNodeModel=
              (TreeNodeModel<?>)propertyChangeEvent.getTarget();
          if(Boolean.TRUE.equals(propertyChangeEvent.getNewValue()))
          {
            //the tree node was just selected
          }
        }
      }
    });

The application listens for the TreeNodeModel.SELECTED_PROPERTY changing by registering one property change listener with the TreeControl instance. Once the property changes, the listener verifies that a tree node is indeed the object with the changed selection status. If the new selected status is true, the application may decide to perform some special action resulting from the selection change, perhaps based upon the specific tree node target as reported in propertyChangeEvent.getTarget().

Summary

This JavaBean extension architecture of typed, targeted property change events not only is simple and powerful, it also is interoperable with the existing JavaBeans component architecture. Through the use of generics it obviates the need for tedious manual casting of values while imposing consistency and internal type checking within a property change listener. By introducing the concept of an event target, the framework allows facile propagation of property change events to a single listening point without losing the identification of the original object the property of which was changed. A developer can experiment with the examples presented here using the free development version of the Guise™ framework library, available at http://www.guiseframework.com/.

About the Author

Garret Wilson provides consulting on Internet standards through his company, GlobalMentor, Inc. He is currently perfecting the AJAX-enabled Guise™ Internet application framework.

Copyright © 2006 Garret Wilson.





Page 2 of 2



Comment and Contribute

 


(Maximum characters: 1200). You have characters left.

 

 


Sitemap | Contact Us

Rocket Fuel