Binding is one of the best features in JavaFX. The possibility to class Java APIs from JavaFX is another one.
But unfortunately there is no easy way to combine JavaFX binding and Java PropertyChangeEvents. Therefore I have taken a look at the JavaFX code and I think I have found a possible solution:
The manual way
Of course you can add on replace triggers on every var and fire your events manually:
[cc lang="c"]
class Customer{
public var name:String = “” on replace old {
pcs.firePropertyChange(“name”, old, name );
};
public var address:String = “” on replace old {
pcs.firePropertyChange(“address”, old, address );
};
public var mail:Email on replace old {
pcs.firePropertyChange(“mail”, old, mail );
};
public-read def pcs = new PropertyChangeSupport( this );
}
[/cc]
This has at least three disadvantages:
- At first your code becomes very ugly. A lot of boilerplate code without any type checking. Very easy to make faults. Very hard to find them.
- But the second problem is even worse – if you don’t know for sure whether there is a listener registered. This approach destroys lazy binding which has been introduced in JavaFX 1.3. Every time the binding is invalidated, the replace trigger forces an update – even if nobody is registered at the PropertyChangeSupport.
- It can’t be “attached” to existing JavaFX classes.
Using an automated bridge
I have created a bridge that can be attached to every JavaFX object. This bridge uses the JavaFX binding stuff and translates the binding updates to PropertyChangeEvents.
There is no reflection involved when vars are updated (just when setting it up). So performance shouldn’t be a problem.
(Of course this approach also prevents lazy bindings since the events have to be created every time the vars change). So only attach the bridge if it is necessary.
The bridge can be used like that:
[cc lang="java"]
* FXObject fxObject = …
* Fx2PropertyChangeEvent bridge = Fx2PropertyChangeEvent.bindProperties( “name”, “id” ).to( fxObject );
* bridge.addPropertyChangeListener( “name”, new PropertyChangeListener(){…} );
[/cc]
Of course the bridge can also be used within your JavaFX scripts/classes.
And here comes the code: Feel free to use it:
[cc lang="java"]
package com.cedarsoft.fx;
import com.sun.javafx.runtime.DependentsManager;
import com.sun.javafx.runtime.FXBase;
import com.sun.javafx.runtime.FXObject;
import com.sun.javafx.runtime.annotation.Public;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.List;
/**
* This is a bridge that connects a PropertyChangeSupport to one or more JavaFX vars.
*
* This bridge does use reflection only to set things up! Whenever a var is updated there no(!) reflection is used!
* Therefore the performance is quite good.
*
*
* The Code might look like that:
*
* FXObject fxObject = ...
* Fx2PropertyChangeEvent bridge = Fx2PropertyChangeEvent.bindProperties( "name", "id" ).to( fxObject );
* bridge.addPropertyChangeListener( "name", new PropertyChangeListener(){...} );
*
*/
@Public
public class Fx2PropertyChangeEvent extends FXBase {
@NotNull
private final PropertyChangeSupport pcs;
@NotNull
private final List entries = new ArrayList();
@NotNull
private final FXObject bindee;
public Fx2PropertyChangeEvent( @NotNull FXObject bindee ) {
this( bindee, null );
}
public Fx2PropertyChangeEvent( @NotNull FXObject bindee, @Nullable PropertyChangeSupport pcs ) {
super( false );
this.bindee = bindee;
this.pcs = pcs == null ? new PropertyChangeSupport( this ) : pcs;
initialize$( true );
}
/**
* Convert to property change events.
*
* @param propertyName the name of the property (var)
*/
void bindTo( @NotNull String propertyName ) {
try {
String fieldName = “$” + propertyName;
bindee.getClass().getField( fieldName );
} catch ( Exception ignore ) {
throw new IllegalArgumentException( “Invalid property name <" + propertyName + ">” );
}
try {
String varNumFieldName = “VOFF$” + propertyName;
Integer varNumField = ( Integer ) bindee.getClass().getField( varNumFieldName ).get( null );
int index = addEntry( propertyName, varNumField );
DependentsManager.addDependent( bindee, varNumField, this, index );
} catch ( Exception e ) {
throw new RuntimeException( e );
}
}
@Override
public boolean update$( FXObject src, int depNum, int startPos, int endPos, int newLength, int phase ) {
Entry entry = getEntry( depNum );
if ( phase == 65 ) {
Object oldValue = src.get$( entry.getVarNumField() );
entry.setOldValue( oldValue );
} else if ( phase == 92 ) {
Object newValue = src.get$( entry.getVarNumField() );
pcs.firePropertyChange( new PropertyChangeEvent( src, entry.getPropertyName(), entry.getOldValue(), newValue ) );
} else {
throw new IllegalStateException( “Invalid phase: ” + phase );
}
super.update$( src, depNum, startPos, endPos, newLength, phase );
return true;
}
private int addEntry( @NotNull @NonNls String propertyName, int varNumField ) {
int index = entries.size();
entries.add( new Entry( propertyName, varNumField ) );
return index;
}
@NotNull
private Entry getEntry( int depNum ) {
return entries.get( depNum );
}
public void addPropertyChangeListener( PropertyChangeListener listener ) {
pcs.addPropertyChangeListener( listener );
}
public void removePropertyChangeListener( PropertyChangeListener listener ) {
pcs.removePropertyChangeListener( listener );
}
public void addPropertyChangeListener( String propertyName, PropertyChangeListener listener ) {
pcs.addPropertyChangeListener( propertyName, listener );
}
public void removePropertyChangeListener( String propertyName, PropertyChangeListener listener ) {
pcs.removePropertyChangeListener( propertyName, listener );
}
@NotNull
public FXObject getBindee() {
return bindee;
}
@NotNull
public PropertyChangeSupport getPcs() {
return pcs;
}
public static class Entry {
private final String propertyName;
private final int varNumField;
private Object oldValue;
public Entry( String propertyName, int varNumField ) {
this.propertyName = propertyName;
this.varNumField = varNumField;
}
public String getPropertyName() {
return propertyName;
}
public int getVarNumField() {
return varNumField;
}
public Object getOldValue() {
return oldValue;
}
public void setOldValue( Object oldValue ) {
this.oldValue = oldValue;
}
}
/**
* Binds the given property names
*
* @param propertyNames the property names
* @return the fluent factory used to create a Fx2PropertyChangeEvent
*/
@NotNull
public static FluentFactory bindProperties( @NotNull @NonNls String… propertyNames ) {
return new FluentFactory( propertyNames );
}
/**
* Binds the given property names
*
* @param propertyNames the property names
* @return the fluent factory used to create a Fx2PropertyChangeEvent
*/
@NotNull
public static FluentFactory bind( @NotNull @NonNls String… propertyNames ) {
return bindProperties( propertyNames );
}
/**
* Fluent factory implementation
*/
public static class FluentFactory {
@NotNull
private final String[] propertyNames;
private FluentFactory( @NonNls @NotNull String[] propertyNames ) {
//noinspection AssignmentToCollectionOrArrayFieldFromParameter
this.propertyNames = propertyNames;
}
/**
* Binds the property names to the given bindee
*
* @param bindee the bindee the properties are bound to
* @return the bridge
*
* @noinspection InstanceMethodNamingConvention
*/
@NotNull
public Fx2PropertyChangeEvent to( @NotNull FXObject bindee ) {
if ( propertyNames.length == 0 ) {
throw new IllegalArgumentException( “Need at least one property to bind to” );
}
Fx2PropertyChangeEvent bridge = new Fx2PropertyChangeEvent( bindee );
for ( String propertyName : propertyNames ) {
bridge.bindTo( propertyName );
}
return bridge;
}
}
}
/**
* Copyright (C) cedarsoft GmbH.
*
* Licensed under the GNU General Public License version 3 (the “License”)
* with Classpath Exception; you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.cedarsoft.org/gpl3ce
* (GPL 3 with Classpath Exception)
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 3 only, as
* published by the Free Software Foundation. cedarsoft GmbH designates this
* particular file as subject to the “Classpath” exception as provided
* by cedarsoft GmbH in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 3 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 3 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact cedarsoft GmbH, 72810 Gomaringen, Germany,
* or visit www.cedarsoft.com if you need additional information or
* have any questions.
*/
[/cc]
I will upload a complete project very soon (Monday). This will contain several samples and unit tests….