Search code examples
javaswingjavafxjava-11java-platform-module-system

JavaBeanProperties in JavaFX without pulling in java.desktop (Swing, AWT)


I have some model classes that are instantiated many times and have many fields. While I could initialize all fields as Simple*Propertys, this significantly slows performance due to the allocations (and lazy-creating the properties is not an option).

Thus, I would prefer to use JavaBeanProperties to create on-demand bindings where needed in the application like so (see this answer for a full example JavaBean wrapping with JavaFX Properties):

Entity bean = ...
StringProperty nameProperty = JavaBeanStringPropertyBuilder()
  .bean(bean)
  .name("name")
  .build();

However, I do not want to depend on java.desktop and related Swing components in my module-info.java

I could potentially re-write the desired functionality by setting a SimpleIntegerProperty that is incremented on each of the Entity's set* methods, and then add listeners in the GUI, but this is still less efficient (unnecessary updates) and expressive as using JavaBean*Propertys

How may I use JavaBeanProperties (or similar on-demand binding functionality) without using java.desktop?


Solution

  • You could use a generic solution that doesn’t use Reflection at all:

    public class DelegatingProperty<B,T> extends ObjectPropertyBase<T>
                                         implements JavaBeanProperty<T> {
        /**
         * Create a property without PropertyChangeEvent based notification.
         */
        public static <O, V> JavaBeanProperty<V> get(O bean, String name,
            Function<? super O, ? extends V> getter,
            BiConsumer<? super O, ? super V> setter) {
            return new DelegatingProperty<>(bean, name, getter, setter, null, null);
        }
        /**
         * Create a property with PropertyChangeEvent based notification.
         */
        public static <O, V> JavaBeanProperty<V> get(O bean, String name,
            Function<? super O, ? extends V> getter, BiConsumer<? super O, ? super V> setter,
            BiConsumer<? super O, ? super PropertyChangeListener> register,
            BiConsumer<? super O, ? super PropertyChangeListener> unregister) {
            return new DelegatingProperty<>(bean, name, getter, setter, register, unregister);
        }
        B bean;
        String name;
        Function<? super B, ? extends T> getter;
        BiConsumer<? super B, ? super T> setter;
        BiConsumer<? super B, ? super PropertyChangeListener> unregister;
        PropertyChangeListener listener;
    
        private DelegatingProperty(B bean, String name,
            Function<? super B, ? extends T> getter,
            BiConsumer<? super B, ? super T> setter,
            BiConsumer<? super B, ? super PropertyChangeListener> register,
            BiConsumer<? super B, ? super PropertyChangeListener> unregister) {
            this.bean = Objects.requireNonNull(bean);
            this.name = name;
            this.getter = Objects.requireNonNull(getter);
            this.setter = Objects.requireNonNull(setter);
            if(register != null || unregister != null) {
                Objects.requireNonNull(register);
                this.unregister = Objects.requireNonNull(unregister);
                register.accept(bean, listener = event -> fireValueChangedEvent());
            }
        }
    
        @Override
        public Object getBean() {
            return bean;
        }
    
        @Override
        public String getName() {
            return name;
        }
    
        @Override
        public T get() {
            return getter.apply(bean);
        }
    
        @Override
        public void set(T value) {
            if(isBound()) throw new IllegalStateException("bound property");
            T old = getter.apply(bean);
            setter.accept(bean, value);
            T now = getter.apply(bean);
            if(!Objects.equals(old, now)) fireValueChangedEvent();
        }
    
        @Override
        protected void invalidated() {
            if(isBound()) {
                setter.accept(bean, super.get());
            }
        }
    
        @Override
        public void fireValueChangedEvent() {
            super.fireValueChangedEvent();
        }
    
        @Override
        public void dispose() {
            if(unregister != null && listener != null) {
                unregister.accept(bean, listener);
                listener = null;
            }
        }
    }
    

    Then, to stay at your example, you could get the name property of Entity as

    JavaBeanProperty<String> prop = DelegatingProperty.get(bean, "name",
        Entity::getName, Entity::setName,
        Entity::addPropertyChangeListener, Entity::removePropertyChangeListener);
    

    It’s more verbose, but on the other hand, provides more compile time safety, as the presence of all methods required for the property is checked at compile-time, and will likely have a higher runtime performance.

    When you have a lot of properties in one bean class with event support, you may benefit from a dedicated factory method, e.g.

    public static <V> JavaBeanProperty<V> property(Entity theBean, String name,
        Function<? super Entity, ? extends V> getter,
        BiConsumer<? super Entity, ? super V> setter) {
        return DelegatingProperty.get(theBean, name, getter, setter,
            Entity::addPropertyChangeListener, Entity::removePropertyChangeListener);
    }
    

    which you then can use as

    JavaBeanProperty<String> nameProp
        = property(bean, "name", Entity::getName, Entity::setName);
    JavaBeanProperty<String> otherProp
        = property(bean, "other", Entity::getOther, Entity::setOther);
    

    Of course, it would also be possible to provide them via instance methods of the bean itself instead of a static factory method, too, perhaps with a lazily populated field holding the property, etc.

    There are several roads you can go from this starting point.