Interface ISideEffect
- All Known Implementing Classes:
CompositeSideEffect
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.
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 anyIObservable
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, theISideEffect
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 Summary
Modifier and TypeMethodDescriptionvoid
addDisposeListener
(Consumer<ISideEffect> disposalConsumer) Adds a listener that will be invoked when thisISideEffect
instance is disposed.static <T> ISideEffect
consumeOnceAsync
(Supplier<T> supplier, Consumer<T> consumer) Runs the given supplier until it returns a non-null result.static ISideEffect
Runs the given runnable once synchronously.static <T> ISideEffect
Runs the supplier and passes its result to the consumer.static ISideEffect
createPaused
(Runnable runnable) Creates a newISideEffect
on the defaultRealm
but does not run it immediately.static ISideEffect
createPaused
(Realm realm, Runnable runnable) Creates a newISideEffect
on the given Realm but does not activate it immediately.static <T> ISideEffect
createResumed
(Supplier<T> supplier, Consumer<T> consumer) Runs the supplier and passes its result to the consumer.void
dispose()
Disposes the side-effect, detaching all listeners and deallocating all memory used by the side-effect.boolean
Returns true if this side-effect has been disposed.void
pause()
Increments the count of the number of times theISideEffect
has been paused.void
removeDisposeListener
(Consumer<ISideEffect> disposalConsumer) Removes a dispose listener from thisISideEffect
instance.void
resume()
Increments the count of the number of times theISideEffect
has been resumed.void
Increments the count of the number of times theISideEffect
has been resumed.void
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).
-
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 theISideEffect
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 theISideEffect
is paused, theISideEffect
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 theISideEffect
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 resumeISideEffect
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 theISideEffect
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 theISideEffect
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 theISideEffect
is dirty, it will be run synchronously.This is a convenience method which is fully equivalent to calling
resume()
followed byrunIfDirty()
, 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 theISideEffect
is currently paused. -
addDisposeListener
Adds a listener that will be invoked when thisISideEffect
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 thisISideEffect
is disposed.
-
removeDisposeListener
Removes a dispose listener from thisISideEffect
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
Creates a newISideEffect
on the defaultRealm
but does not run it immediately. Callers are responsible for invokingresume()
orresumeAndRunIfDirty()
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 callingdispose()
on the result when it is no longer needed.
-
createPaused
Creates a newISideEffect
on the given Realm but does not activate it immediately. Callers are responsible for invokingresume()
when they want the runnable to begin executing.- Parameters:
realm
- the realm to executerunnable
- the runnable to execute. Must be idempotent.- Returns:
- a newly-created
ISideEffect
which has not yet been activated. Callers are responsible for callingdispose()
on the result when it is no longer needed.
-
create
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 returnedISideEffect
is disposed. The returnedISideEffect
is associated with the default realm. The caller must dispose the returnedISideEffect
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. TheRunnable
will not be executed anymore after the dispose method is invoked.
-
create
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 givenISideEffect
is disposed. The returnedISideEffect
is associated with the default realm. The caller must dispose the returnedISideEffect
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. TheRunnable
will not be executed anymore after the dispose method is invoked.
-
createResumed
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 givenISideEffect
is disposed. The returnedISideEffect
is associated with the default realm. The caller must dispose the returnedISideEffect
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 theISideEffect
. However, this extra safety comes with a small performance cost overcreate(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. TheRunnable
will not be executed anymore after the dispose method is invoked.
-
consumeOnceAsync
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 consumerconsumer
- 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.
-