On Fri, 14 Oct 2022 23:01:48 GMT, Andy Goryachev <ango...@openjdk.org> 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

Reply via email to