On Fri, 14 Oct 2022 23:01:48 GMT, Andy Goryachev <[email protected]> wrote:
>> I agree with your first point, but I think limiting any changes to `Node`
>> because it would add another field is not a good enough argument. It would
>> depend on its merits.
>>
>> The second point, there is much more to the `shownProperty` then you might
>> think at first glance. In combination with `when`, it offers a standard way
>> to manage life cycles for controls. It is a very natural fit to tie the
>> lifecycle of bindings and listeners to whether a control is actually
>> currently shown. You may think that a Listener helper class would be just
>> as flexible, but it isn't. The helper is not available everywhere (unless
>> you propose to integrate it into Node), which means when constructing
>> complicated UI's which may have smaller groups of controls that are created
>> by other classes, methods or factories, that you may have to pass along an
>> instance of your helper to all these to make sure they register their
>> bindings and listeners with it (or perhaps to link multiple Listener helper
>> classes together in some kind of hierarchy, that matches the control
>> groupings that might have separate life cycles).
>>
>> The `shownProperty` does not have this problem. Since you can tie the
>> lifecycle to your own small piece of the UI (ie, a factory that creates a
>> `BorderPane` with several controls, can tie all these to the lifecycle of
>> the `BorderPane`). When this `BorderPane` is just a piece of UI used as
>> part of a larger whole, its lifecycle still works as expected -- if it is
>> not shown, its listeners disappear. If the whole window is destroyed, then
>> the same happens. If however the Pane next to it is destroyed, only their
>> listeners disappear. With a helper system, you'd need to coordinate this
>> somehow, perhaps having that `BorderPane` return its helper, so the large UI
>> can tie it to its helper, so when the main helper is called to unsubscribe,
>> that all its child helpers are also unsubscribed...
>>
>> Another advantage is that a listener does not have to start working right
>> away. When setting up a new pane of controls, many subscriptions may need
>> to be made. When using `when(control::shownProperty)` these listeners don't
>> immediately become active (potentially while still constructing and
>> initializing your control). Only when the control is fully constructed, and
>> added to an active Scene and Window do all the listeners become active.
>> This can be important when listening to external properties that change
>> often and may even change during construction.
>>
>> In ReactFX it the `shownProperty` was deemed important enough to go one step
>> further and even create a separate method on what would be `ObservableValue`
>> called `conditionOnShowing`.
>>
>> In short, `when` and the `shownProperty` is basically used every time you
>> create a binding or add a listener between two properties that have a
>> different lifecycle, like between a model and a control. This completely
>> removes the responsibility of having to track these bindings or listeners
>> separately; it becomes automatic. Subscribing all at once when it becomes
>> first shown becomes automatic (and you immediately get the first value),
>> unsubscribing when it goes invisible is automatic, and resubscribing if a
>> control becomes shown again is completely transparent.
>>
>> In my projects, I've completely eliminated memory leaks without a single
>> call to `removeListener` or `unbind` anywhere.
>> There are no explicit weak listeners (and not just because they're
>> incredibly cumbersome to use), nor is there anywhere where subscriptions or
>> bindings are being tracked (aside from the `when` construct). I'm very
>> picky about memory leaks, so I have a Scene scanner, that maintains weak
>> references to all Nodes at all times, and warns me if any are not getting
>> GC'd after not being part of a Scene for a while. In 99% of the cases, I
>> forgot a `when`, where in plain JavaFX you'd be forced to use a weak
>> listener.
>>
>> The simple rule to follow is, whenever you bind or listen to or on a
>> property, if they have different lifecycles, then use a `when`, otherwise
>> bind or listen directly.
>>
>> Some examples of how it is used:
>>
>> Below is a very common pattern when binding any property that has a
>> different lifecycle than its target, this is one of many:
>>
>> longLivedModel.selectedItem
>> .when(this::shownProperty) // this = BorderPane, and the code is
>> part of its constructor)
>> .addListener((obs, old, current) -> {
>> // code that starts a transition from the previous item to the
>> next item
>> }
>> );
>>
>> Below some binding examples where several properties are bound to long lived
>> versions:
>>
>> ObservableValue<Boolean> shown = pane.shownProperty();
>>
>> pane.model.work.bind(presentation.root.when(shown));
>>
>> pane.model.missingFraction.bind(presentation.missingFraction.when(shown));
>>
>> pane.model.watchedFraction.bind(presentation.watchedFraction.when(shown));
>>
>> Below a "bidirectional binding" for a `Slider` control, where one direction
>> is long lived. The binding vanishes on its own when the UI that is using it
>> is closed -- this is part of a UI generator, so this code has no idea where
>> the UI might end up being used or for how long, but it can still manage the
>> lifecycle easily:
>>
>>
>> longLivedValue.when(slider::shownProperty).map(toNumber).subscribe(slider.valueProperty()::setValue);
>>
>> slider.valueProperty().map(fromNumber).subscribe(longLivedValue::setValue);
>>
>> Another example to make sure a `Timeline` is stopped/started automatically
>> based on visibility (Timeline's will keep references around if not stopped):
>>
>> center.shownProperty() // center here is a Pane
>> .addListener((obs, old, visible) -> {
>> if(visible) timeline.playFromStart();
>> else timeline.stop();
>> });
>>
>> So, I think it certainly has some merit to include it :)
>>
>>> And another thing - Window and Dialog are not Nodes, but they do have
>>> showing property.
>>
>> I'm unsure what you are saying here. I don't think we need to tie anything
>> directly to a Window or Dialog, only to controls they might contain. Also
>> the `shownProperty` is not useful for properties not associated with
>> controls; depending on what the properties are used for people may have to
>> create their own `active` property or something else that identifies the
>> lifecycle of those properties. For control properties though, their
>> lifecycle is quite clear and I think it makes great sense to tie to whether
>> they're actually visible or not.
>
> What I meant is you can add it in Node.getProperties() and not to add a
> pointer to every Node everywhere. If it's used, the actual properties will
> be created in a few containers.
private static final Object KEY = new Object();
public final ReadOnlyBooleanProperty shownProperty() {
Object x = getProperties().get(KEY);
if (x instanceof ReadOnlyBooleanProperty p) {
return p;
}
ReadOnlyBooleanProperty p = ... // create
getProperties().put(KEY, p);
return p;
}
-------------
PR: https://git.openjdk.org/jfx/pull/830