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
ISideEffectcan self-optimize based on branches in the run method. It will remove listeners from anyIObservablewhich 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
ISideEffectwill batch changes together and run asynchronously. If firstName and lastName change at the same time, theISideEffectwill only run once. - Since the
ISideEffectdoesn'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 TypeMethodDescriptionvoidaddDisposeListener(Consumer<ISideEffect> disposalConsumer) Adds a listener that will be invoked when thisISideEffectinstance is disposed.static <T> ISideEffectconsumeOnceAsync(Supplier<T> supplier, Consumer<T> consumer) Runs the given supplier until it returns a non-null result.static ISideEffectRuns the given runnable once synchronously.static <T> ISideEffectRuns the supplier and passes its result to the consumer.static ISideEffectcreatePaused(Runnable runnable) Creates a newISideEffecton the defaultRealmbut does not run it immediately.static ISideEffectcreatePaused(Realm realm, Runnable runnable) Creates a newISideEffecton the given Realm but does not activate it immediately.static <T> ISideEffectcreateResumed(Supplier<T> supplier, Consumer<T> consumer) Runs the supplier and passes its result to the consumer.voiddispose()Disposes the side-effect, detaching all listeners and deallocating all memory used by the side-effect.booleanReturns true if this side-effect has been disposed.voidpause()Increments the count of the number of times theISideEffecthas been paused.voidremoveDisposeListener(Consumer<ISideEffect> disposalConsumer) Removes a dispose listener from thisISideEffectinstance.voidresume()Increments the count of the number of times theISideEffecthas been resumed.voidIncrements the count of the number of times theISideEffecthas been resumed.voidCauses 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 theISideEffecthas 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
ISideEffectis 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 theISideEffectis paused, theISideEffectwill 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 theISideEffecthas 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 resumeISideEffectmore often than it has been paused.When a
ISideEffectis 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 theISideEffectis dirty, it will be run at the earliest opportunity after this method returns. -
resumeAndRunIfDirty
void resumeAndRunIfDirty()Increments the count of the number of times theISideEffecthas 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
ISideEffectis 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 theISideEffectis 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 theISideEffectis currently paused. -
addDisposeListener
Adds a listener that will be invoked when thisISideEffectinstance 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 thisISideEffectis disposed.
-
removeDisposeListener
Removes a dispose listener from thisISideEffectinstance. 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 newISideEffecton the defaultRealmbut 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
ISideEffectwhich has not yet been activated. Callers are responsible for callingdispose()on the result when it is no longer needed.
-
createPaused
Creates a newISideEffecton 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
ISideEffectwhich 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 returnedISideEffectis disposed. The returnedISideEffectis associated with the default realm. The caller must dispose the returnedISideEffectwhen 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
ISideEffectinterface that may be used to stop the side-effect from running. TheRunnablewill 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 givenISideEffectis disposed. The returnedISideEffectis associated with the default realm. The caller must dispose the returnedISideEffectwhen 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
ISideEffectinterface that may be used to stop the side-effect from running. TheRunnablewill 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 givenISideEffectis disposed. The returnedISideEffectis associated with the default realm. The caller must dispose the returnedISideEffectwhen 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
ISideEffectin 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
ISideEffectinterface that may be used to stop the side-effect from running. TheRunnablewill 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.
-