All Known Implementing Classes:
CompositeSideEffect

public interface ISideEffect
An ISideEffect allows you to run code whenever one or more observables change. An ISideEffect is a lot like a listener except that it doesn't need to be attached to anything. Instead, it reacts automatically to changes in tracked getters that are invoked by the listener.

Observables form a directed graph of dependencies. Classes like WritableValue form the inputs to the graph (nodes which have only outputs), classes like ComputedValue form the interior nodes (they receive inputs from observables and produce an output which is used by other observables), and ISideEffect is used for the leaf nodes (nodes which receive inputs but produce no output).

Side-effects have a life-cycle which passes through a number of states:

  • Paused: The side-effect will listen for changes but will not react to them. If any change occurs while the side-effect is paused, it will react when and if the side-effect is resumed. Some side-effects are paused immediately on construction. This is useful, for example, for creating a side-effect in an object's constructor which should not begin running until a later time. When using a side-effect to update a control or a view, it is common to pause the side-effect when the view is hidden and resume the side-effect when the view becomes visible.
  • Resumed: The side-effect will listen for changes and react to them asynchronously. Side-effects may be paused and resumed any number of times.
  • Disposed: The side-effect will not listen to or react to changes. It will also remove any strong references to its dependencies. Once a side-effect enters the disposed state it remains in that state until it is garbage collected.
Example usage:
 IObservableValue<String> firstName = ...
 IObservableValue<String> lastName = ...
 IObservableValue<Boolean> showFullNamePreference = ...
 Label userName = ...

 ISideEffect sideEffect = ISideEffect.create(() -> {
     String name = showFullNamePreference.get()
         ? (firstName.get() + " " + lastName.get())
         : firstName.get();
     userName.setText("Your name is " + name);
 });
 

The above example uses an ISideEffect to fill in a label with a user's name. It will react automatically to changes in the username and the showFullNamePreference.

The same thing could be accomplished by attaching listeners to all three observables, but there are several advantages to using ISideEffect over listeners.

  • The ISideEffect can self-optimize based on branches in the run method. It will remove listeners from any IObservable which wasn't used on the most recent run. In the above example, there is no need to listen to the lastName field when showFullNamePreference is false.
  • The ISideEffect will batch changes together and run asynchronously. If firstName and lastName change at the same time, the ISideEffect will only run once.
  • Since the ISideEffect doesn't need to be explicitly attached to the observables it affects, it is impossible for it to get out of sync with the underlying data.

Please be aware of a common anti-pattern. Don't create new observables inside an ISideEffect unless you remember them for future runs. Creating new observables inside an ISideEffect can easily create infinite loops.

 // Bad: May create an infinite loop, since each AvatarObservable instance may
 // fire an asynchronous event after creation
 void createControls() {
        ISideEffect sideEffect = ISideEffect.create(() -> {
                IObservableValue<Image> myAvatar = new AvatarObservable();

                myIcon.setImage(myAvatar.getValue());
        });
 }

 // Good: The AvatarObservable instance is remembered between invocations of
 // the side-effect.
 void createControls() {
        final IObservableValue<Image> myAvatar = new AvatarObservable();
        ISideEffect sideEffect = ISideEffect.create(() -> {
                myIcon.setImage(myAvatar.getValue());
        });
 }
 
Since:
1.6
Restriction:
This interface is not intended to be implemented by clients.
  • Method Details

    • dispose

      void dispose()
      Disposes the side-effect, detaching all listeners and deallocating all memory used by the side-effect. The side-effect will not execute again after this method is invoked.

      This method may be invoked more than once.

    • isDisposed

      boolean isDisposed()
      Returns true if this side-effect has been disposed. A disposed side-effect will never execute again or retain any strong references to the observables it uses. A side-effect which has not been disposed has some possibility of executing again in the future and of retaining strong references to observables.
      Returns:
      true if this side-effect has been disposed.
    • pause

      void pause()
      Increments the count of the number of times the ISideEffect has been paused. If the side-effect has been paused a greater number of times than it has been resumed, it enters the paused state.

      When a ISideEffect is paused, this prevents it from running again until it is resumed. Note that the side-effect will continue listening to its dependencies while it is paused. If a dependency changes while the ISideEffect is paused, the ISideEffect will run again after it is resumed.

      A side-effect may be paused and resumed any number of times. You should use pause instead of dispose if there is a chance you may want to resume the SideEffect later.

    • resume

      void resume()
      Increments the count of the number of times the ISideEffect has been resumed. If the side-effect has been resumed an equal number of times than it has been paused, it leaves the paused state and enters the resumed state. It is an error to resume ISideEffect more often than it has been paused.

      When a ISideEffect is resumed, it starts reacting to changes in tracked getters invoked by its runnable. It will continue to react to changes until it is either paused or disposed. If the ISideEffect is dirty, it will be run at the earliest opportunity after this method returns.

    • resumeAndRunIfDirty

      void resumeAndRunIfDirty()
      Increments the count of the number of times the ISideEffect has been resumed. If the side-effect has been resumed an equal or greater number of times than it has been paused, it leaves the paused state and enters the resumed state.

      When a ISideEffect is resumed, it starts reacting to changes in TrackedGetters invoked by its runnable. It will continue to react to changes until it is either paused or disposed. If the ISideEffect is dirty, it will be run synchronously.

      This is a convenience method which is fully equivalent to calling resume() followed by runIfDirty(), but slightly faster.

    • runIfDirty

      void runIfDirty()
      Causes the side effect to run synchronously if and only if it is currently dirty (that is, if one of its dependencies has changed since the last time it ran). Does nothing if the ISideEffect is currently paused.
    • addDisposeListener

      void addDisposeListener(Consumer<ISideEffect> disposalConsumer)
      Adds a listener that will be invoked when this ISideEffect instance is disposed. The listener will not be invoked if the receiver has already been disposed at the time when the listener is attached.
      Parameters:
      disposalConsumer - a consumer which will be notified once this ISideEffect is disposed.
    • removeDisposeListener

      void removeDisposeListener(Consumer<ISideEffect> disposalConsumer)
      Removes a dispose listener from this ISideEffect instance. Has no effect if no such listener was previously attached.
      Parameters:
      disposalConsumer - a consumer which is supposed to be removed from the dispose listener list.
    • createPaused

      static ISideEffect createPaused(Runnable runnable)
      Creates a new ISideEffect on the default Realm but does not run it immediately. Callers are responsible for invoking resume() or resumeAndRunIfDirty() when they want the runnable to begin executing.
      Parameters:
      runnable - the runnable to execute. Must be idempotent.
      Returns:
      a newly-created ISideEffect which has not yet been activated. Callers are responsible for calling dispose() on the result when it is no longer needed.
    • createPaused

      static ISideEffect createPaused(Realm realm, Runnable runnable)
      Creates a new ISideEffect on the given Realm but does not activate it immediately. Callers are responsible for invoking resume() when they want the runnable to begin executing.
      Parameters:
      realm - the realm to execute
      runnable - the runnable to execute. Must be idempotent.
      Returns:
      a newly-created ISideEffect which has not yet been activated. Callers are responsible for calling dispose() on the result when it is no longer needed.
    • create

      static ISideEffect create(Runnable runnable)
      Runs the given runnable once synchronously. The runnable will then run again after any tracked getter invoked by the runnable changes. It will continue doing so until the returned ISideEffect is disposed. The returned ISideEffect is associated with the default realm. The caller must dispose the returned ISideEffect when they are done with it.
      Parameters:
      runnable - an idempotent runnable which will be executed once synchronously then additional times after any tracked getter it uses changes state
      Returns:
      an ISideEffect interface that may be used to stop the side-effect from running. The Runnable will not be executed anymore after the dispose method is invoked.
    • create

      static <T> ISideEffect create(Supplier<T> supplier, Consumer<T> consumer)
      Runs the supplier and passes its result to the consumer. The same thing will happen again after any tracked getter invoked by the supplier changes. It will continue to do so until the given ISideEffect is disposed. The returned ISideEffect is associated with the default realm. The caller must dispose the returned ISideEffect when they are done with it.

      The ISideEffect will initially be in the resumed state.

      The first invocation of this method will be synchronous. This version is slightly more efficient than createResumed(Supplier, Consumer) and should be preferred if synchronous execution is acceptable.

      Parameters:
      supplier - a supplier which will compute a value and be monitored for changes in tracked getters. It should be side-effect-free.
      consumer - a consumer which will receive the value. It should be idempotent. It will not be monitored for tracked getters.
      Returns:
      an ISideEffect interface that may be used to stop the side-effect from running. The Runnable will not be executed anymore after the dispose method is invoked.
    • createResumed

      static <T> ISideEffect createResumed(Supplier<T> supplier, Consumer<T> consumer)
      Runs the supplier and passes its result to the consumer. The same thing will happen again after any tracked getter invoked by the supplier changes. It will continue to do so until the given ISideEffect is disposed. The returned ISideEffect is associated with the default realm. The caller must dispose the returned ISideEffect when they are done with it.

      The ISideEffect will initially be in the resumed state.

      The first invocation of this method will be asynchronous. This is useful, for example, when constructing an ISideEffect in a constructor since it ensures that the constructor will run to completion before the first invocation of the ISideEffect. However, this extra safety comes with a small performance cost over create(Supplier, Consumer).

      Parameters:
      supplier - a supplier which will compute a value and be monitored for changes in tracked getters. It should be side-effect-free.
      consumer - a consumer which will receive the value. It should be idempotent. It will not be monitored for tracked getters.
      Returns:
      an ISideEffect interface that may be used to stop the side-effect from running. The Runnable will not be executed anymore after the dispose method is invoked.
    • consumeOnceAsync

      static <T> ISideEffect consumeOnceAsync(Supplier<T> supplier, Consumer<T> consumer)
      Runs the given supplier until it returns a non-null result. The first time it returns a non-null result, that result will be passed to the consumer and the ISideEffect will dispose itself. As long as the supplier returns null, any tracked getters it invokes will be monitored for changes. If they change, the supplier will be run again at some point in the future.

      The resulting ISideEffect will be dirty and resumed, so the first invocation of the supplier will be asynchronous. If the caller needs it to be invoked synchronously, they can call runIfDirty()

      Unlike create(Supplier, Consumer), the consumer does not need to be idempotent.

      This method is used for gathering asynchronous data before opening an editor, saving to disk, opening a dialog box, or doing some other operation which should only be performed once.

      Consider the following example, which displays the content of a text file in a message box without doing any file I/O on the UI thread.

       IObservableValue<String> loadFileAsString(String filename) {
         // Uses another thread to load the given filename. The resulting observable returns
         // null if the file is not yet loaded or contains the file contents if the file is
         // fully loaded
         // ...
       }
      
       void showFileContents(Shell parentShell, String filename) {
         IObservableValue<String> webPageContent = loadFileAsString(filename);
         ISideEffect.runOnce(webPageContent::getValue, (content) -> {
              MessageDialog.openInformation(parentShell, "Your file contains", content);
         })
       }
       
      Parameters:
      supplier - supplier which returns null if the side-effect should continue to wait or returns a non-null value to be passed to the consumer if it is time to invoke the consumer
      consumer - a (possibly non-idempotent) consumer which will receive the first non-null result returned by the supplier.
      Returns:
      a side-effect which can be used to control this operation. If it is disposed before the consumer is invoked, the consumer will never be invoked. It will not invoke the supplier if it is paused.