Reputation: 1382
I am currently developing some custom JavaFX components in my application and stumbled upon an issue with the binding.
The following is a component which allows to rate an item with one to five stars (Rating is an enum with the possible ratings).
public final class RatingControl extends HBox {
private final ReadOnlyObjectWrapper<Rating> ratingProperty = new ReadOnlyObjectWrapper<>( Rating.NO_RATING );
public RatingControl( final Rating initialRating ) {
createVisibleStars( );
ratingProperty.set( initialRating );
ratingProperty.addListener( ( observable, oldValue, newValue ) -> updateVisibleStars( newValue ) );
updateVisibleStars( ratingProperty.get( ) );
}
private void createVisibleStars( ) {
// omitted
}
private void onStarClicked( final Rating rating ) {
if ( ratingProperty.get( ) == rating ) {
setRating( Rating.NO_RATING );
} else {
setRating( rating );
}
}
private void updateVisibleStars( final Rating newValue ) {
// omitted
}
public ReadOnlyObjectProperty<Rating> ratingProperty( ) {
return ratingProperty.getReadOnlyProperty( );
}
public void setRating( final Rating rating ) {
ratingProperty.set( rating );
}
}
So my idea is as following: A click on a star changes the internal state (ratingProperty
) by calling setRating
. This can also be triggered from the outside. An update of this property changes the visual representation and makes sure that the visible rating is updated as well. This, again, can also be read readonly from the outside (via ratingProperty( )
).
This works, but I am not happy with it. The calling component has to do something like the following (assuming that model
has a Property named rating
):
final RatingControl ratingControl = new RatingControl( model.rating( ).getValue( ) );
ratingControl.ratingProperty( ).addListener( ( observable, oldValue, newValue ) -> model.rating( ).set( newValue ) );
Using "normal" JavaFX components one would assume that a binding like the following is possible:
final RatingControl ratingControl = new RatingControl( );
ratingControl.ratingProperty( ).bindBidirectional( model.rating( ) );
final RatingControl ratingControl = new RatingControl( );
ratingControl.ratingProperty( ).bind( model.rating( ) );
So I wonder how I would achieve this. If I simply allow access to my internal property, I would not be able to set it when clicking a star, as a bound property cannot be set ("A bound value cannot be set).
Upvotes: 1
Views: 71
Reputation: 1382
After thinking about this a little bit more, I realized that it is not the bindings per se that are a problem, but only the unidirectional binding, as this leads to a contradiction when changing the value from within the component.
I tested the same structure with a StringProperty
and a default TextField
and noticed something. When using a bidirectional binding between the property and the field's textProperty
, everything works as expected. However, when using a unidirectional binding, the text field no longer allows the user to change the input (this can actually be seen in TextInputControl.replaceText
).
So I think it is not a problem to expose the internal property for the rating in my case. I just have to make sure that my onStarClicked
method checks whether the property is bound or not.
private final ObjectProperty<Rating> ratingProperty = new SimpleObjectProperty<>( Rating.NO_RATING );
...
private void onStarClicked( final Rating rating ) {
if ( !ratingProperty.isBound( ) ) {
if ( ratingProperty.get( ) == rating ) {
setRating( Rating.NO_RATING );
} else {
setRating( rating );
}
}
}
public ObjectProperty<Rating> ratingProperty( ) {
return ratingProperty;
}
This allows to bind a property both uni- and bidirectional without leading to an exception in any case.
Upvotes: 1