This post first really quickly introduces skins for JavaFX controls, then discusses the non-public base class for skin implementations (BehaviorSkinBase
) and its pitfalls, and finally outlines how public API for Skin implementations that encourages separation of view and controller could look. This post presents a lesson learned from my recent refactoring of RichTextFX.
Skins
I cannot do a better job of introducing skins than the Javadoc of the javafx.scene.control package:
Controls follow the classic MVC design pattern. The
Control
is the “model”. It contains both the state and the functions which manipulate that state. The Control class itself does not know how it is rendered or what the user interaction is. These tasks are delegated to theSkin
(“view”), which may internally separate out the view and controller functionality into separate classes, although at present there is no public API for the “controller” aspect.[…] It is also the responsibility of the Skin, or a delegate of the Skin, to implement and respond to all relevant key events which occur on the Control when it contains the focus.
Illustrated, it looks like this:
The skin observes and acts on the control, where, “observes” means listens to events (e.g. mouse events, key events), property changes, observable collections changes, and, more generally, calls functions that do not alter the control’s state; “acts on” means calls functions to manipulate the control’s state. The arrow also means that the skin holds a reference to the control, but not the other way around.1)
The documentation further suggests to separate the view and controller functionality into separate classes. It does not currently provide any API for this, neither does it say anything about the relationship between the view and the controller:
BehaviorSkinBase
Skins in JavaFX extend from BehaviorSkinBase
, which is not part of the public API.2) BehaviorSkinBase
takes care of the view functionality, and delegates the controller functionality to Behavior. BehaviorSkinBase constructor takes a Behavior
instance and makes it visible to subclasses, so that they can invoke methods on the behavior. Thus with BehaviorSkinBase, the undefined relationship from the previous diagram is “View acts on Controller” (which, in turn, acts on the model):
From my limited exposure to skin implementations, the possibility of view invoking methods on the controller is rarely utilized, and where it is, the code could probably benefit from some refactoring (as did RichTextFX).
The problem with BehaviorSkinBase
A real complication arises when the controller, in order to implement proper behavior of the control, needs to get some additional information from the view. Consider a text area. The model of a text area knows the caret position in terms of the index in the text, but does not know the caret coordinates on the screen or how paragraphs are broken into lines. To implement Up
/Down
key navigation, the controller needs to update the model with the new caret position (new index in the text). To this end, the controller needs to get additional information from the skin, because the new caret position depends on the current screen coordinates of the caret, as well as on how paragraphs are currently broken into lines.
This means there needs to be a back reference from the controller to the view. In the previous paragraph we have seen that this reference cannot, in general, be eliminated. On the other hand, I have come to believe that the other reference, the one from the view to the controller, can and should be eliminated.
Alternative architecture
That leaves us with this architecture that we should be implementing:
The only difference from the BehaviorSkinBase architecture above is that we flipped one arrow.
Now let’s put together some API that encourages this design. Let’s have interface Visual
for views and interface Behavior
for controllers. Let’s have a final class BehaviorSkin
that implements Skin
and the only thing it does is that it creates the Visual and the Behavior.
public interface Visual {
Node getNode();
void dispose();
List<CssMetaData<? extends Styleable, ?>> getCssMetaData();
}
public interface Behavior {
void dispose();
}
public final class BehaviorSkin<C extends Control, V extends Visual>
extends SkinBase<C> {
private final V visual;
private final Behavior behavior;
private final Node node;
public BehaviorSkin(
C control,
Function<? super C, ? extends V> visualFactory,
BiFunction<? super C, ? super V, ? extends Behavior> behaviorFactory) {
super(control);
this.visual = visualFactory.apply(control);
this.behavior = behaviorFactory.apply(control, visual);
this.node = visual.getNode();
getChildren().add(node);
}
@Override
public void dispose() {
getChildren().remove(node);
behavior.dispose();
visual.dispose();
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
return visual.getCssMetaData();
}
}
The general template for the Control’s createDefaultSkin() method will look like this:
protected Skin<?> createDefaultSkin() {
return new BehaviorSkin<>(
this,
control -> new FooVisual(control),
(control, visual) -> new FooBehavior(control, visual));
}
or more concisely using constructor references
protected Skin<?> createDefaultSkin() {
return new BehaviorSkin<>(this, FooVisual::new, FooBehavior::new);
}
Note that the factory function for Visual gets only the control as the argument, while the factory function for Behavior gets the control as well as the Visual.
Also note that BehaviorSkin
does not implement Skin
directly, but instead extends SkinBase
, which is already part of the public API.
The Behavior implementation may still extend from BehaviorBase
where it did before, though this class is not part of the public API either.
UPDATE: I have fleshed out the API also for skins that are not represented by a single node, but need direct access control’s child list. Here is the Javadoc for the updated API.
Conclusion
The outlined extension to the API for skin implementations is rather conservative—it does not present any radical shifts. Its main goal is to encourage separation of view and controller aspects. It does not address other challenges connected with skin implementations, such as a mechanism to bind key events to actions (which would be the responsibility of the BehaviorBase class). On the other hand, it does not close any doors to resolving such challenges.
1) Neglecting the fact that when attached to the scene, the control holds a reference to its skin, but as the author of the control, you don’t ever access the skin from the control.
2) Not being part of the public API means that you should not use BehaviorSkinBase
in your applications. Anyway, it is used by JavaFX itself and some version of it may make it to the public API in the future, so I find it worth discussing.