3 How to get started with OpenDolphin - Reference Documentation
Authors: The Dolphin team
Version: PRE-1.0
Table of Contents
3 How to get started with OpenDolphin
For an easy entry into OpenDolphin, we will follow the steps of the DolphinJumpStart project.We implement a very simple application that contains only one text field and two buttons to 'save' or 'reset' the value. 'Saving' will do nothing but printing the current field value on the server side.Both buttons are only enabled if there is really something to save/reset, i.e. the field value is dirty. The dirty state is also visualized via a CSS class (background color changes). Resetting triggers a 'shake' animation.Steps 0 to 4 solely live in the "combined" module for a simple jumpstart before we properly split client and server in step 5 and only keep a starter class in "combined".Step 7 produces a WAR file that you can deploy in an application server (e.g. Tomcat) and the code that starts the client moves to the "client" module.Setup and start of a basic JavaFX view
Let's start with the setup.Please make sure you have visited the DolphinJumpStart project and have looked at the readme.You can either choose to clone the repo for following each step (recommended) or use the provided zip files for a Maven or Gradle build of your own application.The root directory contains a maven pom.xml that you may want to import into your IDE to for creating a project. All major IDEs support projects based on maven pom files.In case you are not using an IDE, follow the readme for how to build and run the various steps.We start our development in the "combined" module with the simplest possible JavaFX view step0.JumpStart. The class has a main method such that you can start it from inside the IDE. Otherwise use the command line launcher as described in the readme.When your setup is correct, it should appear on your screen like:
public class JumpStart extends Application { @Override public void start(Stage primaryStage) throws Exception { primaryStage.setScene(new Scene(new Pane(), 300, 100)); primaryStage.setTitle("Dolphin Jump Start"); primaryStage.show(); } public static void main(String[] args) { launch(JumpStart.class); } }
3.1 Adding nodes to a stage, registering an onAction handler
We stay in the "combined" module and enhance the JavaFX view step1.JumpStart. just slightly with a TextField and a Button that prints the content of the TextField when clicked.The application should appear on your screen like:
private TextField field; private Button button;
button.setOnAction(new EventHandler<ActionEvent>() { public void handle(ActionEvent actionEvent) { System.out.println("text field contains: "+field.getText()); } });
3.2 Introducing a presentation model with one attribute and bind the value
In step 2 we refactor the JavaFX application into step2.JumpStart to make use of OpenDolphin.The visual appearance and the behavior has not changed
PresentationModel input = clientDolphin.presentationModel("input", new ClientAttribute("text"));
- the input presentation model is added to the client model store (with indexes being updated)
- the client dolphin registers itself as a property change listener to the value of the "text" attribute
- the server dolphin is asynchronously notified about the creation, which you can observe in the logs
- the server dolphin asynchronously updates its model store accordingly.
JFXBinder.bind("text").of(field).to("text").of(input);
The above is plain Java. When we use Groovy, we can make use of Groovy's command chain syntax that allows writing the exact same code asFinally, the action handler that was part of the (client) view before now moves to the (server) controller. We register it as an "action" on the server-dolphin.bind "text" of field to "text" of input
config.getServerDolphin().action("PrintText", new NamedCommandHandler() { public void handleCommand(NamedCommand namedCommand, List<Command> commands) { Object text = serverDolphin.getAt("input").getAt("text").getValue(); System.out.println("server text field contains: " + text); } });
Note that the (client) view and the (server) controller do not share any objects!The dolphin server action must therefore ask the server-dolphin for the "text" value of the "input" presentation model before he can print it.Triggering the server action becomes the remaining statement in the button's onAction handler.
button.setOnAction(new EventHandler<ActionEvent>() { public void handle(ActionEvent actionEvent) { clientDolphin.send("PrintText"); } });
[INFO] C: transmitting Command: CreatePresentationModel pmId input pmType null attributes [[propertyName:text, id:761947653, qualifier:null, value:null, tag:VALUE]] [INFO] S: received Command: CreatePresentationModel pmId input pmType null attributes [[propertyName:text, id:761947653, qualifier:null, value:null, tag:VALUE]] [INFO] C: transmitting Command: ValueChanged attr:761947653, null -> [INFO] S: received Command: ValueChanged attr:761947653, null -> [INFO] C: server responded with 0 command(s): [] [INFO] C: server responded with
[INFO] C: transmitting Command: ValueChanged attr:761947653, -> a [INFO] S: received Command: ValueChanged attr:761947653, -> a [INFO] C: server responded with 0 command(s): [] [INFO] C: transmitting Command: ValueChanged attr:761947653, a -> ab [INFO] S: received Command: ValueChanged attr:761947653, a -> ab [INFO] C: server responded with 0 command(s): [] [INFO] C: transmitting Command: ValueChanged attr:761947653, ab -> abc [INFO] S: received Command: ValueChanged attr:761947653, ab -> abc [INFO] C: server responded with 0 command(s): [] [INFO] C: transmitting Command: ValueChanged attr:761947653, abc -> abcd [INFO] S: received Command: ValueChanged attr:761947653, abc -> abcd [INFO] C: server responded with 0 command(s): []
[INFO] C: transmitting Command: PrintText server text field contains: abcd [INFO] S: received Command: PrintText [INFO] C: server responded with 0 command(s): []
All actions are executed asynchronously outside the UI thread.With the first dolphinized application running, let's clean up and add a bit more OpenDolphin goodness.
We cannot accidentally block it by long-running or failed operations, which is a common error in UI development.
3.3 Logical separation between client and server
In step3.JumpStart we first cleanup the code such that it becomes more obvious, which part belongs to the (client) view and the (server) controller. In the first place, OpenDolphin leads to a logical view-controller distinction and client-server split. The only thing that is optionally shared are constants.It is always a good idea to refactor literal values into constants, especially if they are used in more than one place for a unique purpose. Therefore, we will extract our String literals into static references:private static final String MODEL_ID = "modelId"; private static final String MODEL_ATTRIBUTE_ID = "attrId"; private static final String COMMAND_ID = "LogOnServer";
public JumpStart() { config = new DefaultInMemoryConfig(); textAttributeModel = config.getClientDolphin().presentationModel(MODEL_ID, new ClientAttribute(MODEL_ATTRIBUTE_ID, "")); config.getClientDolphin().getClientConnector().setUiThreadHandler(new JavaFXUiThreadHandler()); config.registerDefaultActions(); }
@Override public void start(Stage stage) throws Exception { Pane root = PaneBuilder.create().children( VBoxBuilder.create().children( textField = TextFieldBuilder.create().build(), button = ButtonBuilder.create().text("press me").build(), HBoxBuilder.create().children( LabelBuilder.create().text("IsDirty ?").build(), status = CheckBoxBuilder.create().disable(true).build() ).build() ).build() ).build(); addServerSideAction(); addClientSideAction(); setupBinding(); stage.setScene(new Scene(root, 300, 100)); stage.show(); }

JFXBinder.bind("text").of(textField).to(MODEL_ATTRIBUTE_ID).of(textAttributeModel); JFXBinder.bindInfo("dirty").of(textAttributeModel.getAt(MODEL_ATTRIBUTE_ID)).to("selected").of(status);
3.4 Bind the "dirty" of presentation models to the view
In step4.JumpStart we make even further use of the bindable dirty state.First, we are binding not against the dirty state of an attribute, but against the whole presentation model behind it. This simplifies the binding:JFXBinder.bindInfo("dirty").of(textAttributeModel).to("selected").of(status);
Converter converter = new Converter<Boolean,Boolean>() { public Boolean convert(Boolean value) { return !value; } }; JFXBinder.bindInfo("dirty").of(textAttributeModel).using(converter).to("disabled").of(button);
bindInfo "dirty" of textAttributeModel using { state -> !state } to "disabled" of button
3.5 Split into modules/projects
Step 5 distributes the code into multiple modules (IntelliJ IDEA parlance) or projects/subprojects (Gradle, Maven, Eclipse parlance). We use the more generic word "module".The combined module depends on both client and server and is used for starting the application with the in-memory configuration. The sole class that lives in this module is the starter class step5.TutorialStarter . It sets up the configuration, registers the application-specific actions on the (server) controller, and starts the view. This is the class to start from inside the IDE.The client module (or "view" module if you wish) contains the step5.TutorialApplication view.You can see that the view code is pretty much the same as our old application code but contains the view specific parts only. There is one additional change, though. When the button has been pressed and the command has been executed on the server we would like to interpret the current content of the text field as the new base value just as if the error-free execution of the command would imply a correct "save". The "disabled" state of the button will reflect the new non-dirty state.To this end, we make use of anonFinished
handler:
public void handle(ActionEvent actionEvent) { clientDolphin.send(CMD_LOG, new OnFinishedHandlerAdapter() { @Override public void onFinished(List<ClientPresentationModel> presentationModels) { textAttributeModel.getAt(ATT_FIRSTNAME).rebase(); } }); }
public static final String PM_PERSON = unique("modelId"); public static final String ATT_FIRSTNAME = "attrId"; public static final String CMD_LOG = unique("LogOnServer");private static String unique(String key) { return TutorialConstants.class.getName() + "." + key; }
- ability to start the code unmodified with different configurations (in-memory or client-server)
- clear and minimal dependencies when building
- a minimum of shared code (only the constants) to express semantic dependencies as syntatic dependencies
- actions cannot "accidentally" reach out to view code. The widget set is not even on the classpath!
- actions cannot possibly block the UI thread
- view changes are always displayed correctly since they happen in the UI thread
- the separation of responsibilities is enforced by the dependency structure
3.6 Enhanced view, let the "director" wire all application actions
We finish the application with some more refactorings in step6.TutorialApplication and some tweaks to the view such that it appears like
Converter converter = new Converter<Boolean,Boolean>() { public Boolean convert(Boolean dirty) { if (dirty) { textField.getStyleClass().add("dirty"); } else { textField.getStyleClass().remove("dirty"); } return null; } }; JFXBinder.bindInfo("dirty").of(textAttributeModel).using(converter).to("style").of(textField);
.root { -fx-background-color: linear-gradient(to bottom, transparent 30%, rgba(0, 0, 0, 0.15) 100%); } #content { -fx-padding : 20; -fx-spacing : 10; } .dirty { -fx-background-color: papayawhip; }
Reset by shaking the field
When we click the "reset" button, the dirty value is replaced by the last known base value and a "shake" animation is played on the text field.A shake is a rotation of the field around its center by an angle from -3 to +3 degrees. This is done 3 times during 100 ms each. It makes for a funny effect.final Transition fadeIn = RotateTransitionBuilder.create().node(textField).toAngle(0).duration(Duration.millis(200)).build(); final Transition fadeOut = RotateTransitionBuilder.create().node(textField).fromAngle(-3).interpolator(Interpolator.LINEAR). toAngle(3).cycleCount(3).duration(Duration.millis(100)). onFinished(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent actionEvent) { textAttributeModel.getAt(ATT_FIRSTNAME).reset(); fadeIn.playFromStart(); } }).build();reset.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent actionEvent) { fadeOut.playFromStart(); } });
Yes, director!
The server (controller) part has been divided in two classes: the step6.TutorialAction that contains only one application-specific action and the step6.TutorialDirector who selects which actors should appear in the play, i.e. registers actions with the server dolphin.This distinction makes it easier to evolve the application when new actions come into play since the server adapter (servlet) doesn't have to change when the list of actions changes as we will see in a minute.3.7 Remote setup
With the application being properly structured into its modules, we can now finally distribute it as a true client-server application without any of the application code being touched at all. Only the the server adapter needs to be in place and the client starter needs to connect to the correct URL.The server adapter is a plain-old Servlet such that the code can run in any servlet container. It is as small as can be:public class TutorialServlet extends DolphinServlet{ @Override protected void registerApplicationActions(ServerDolphin serverDolphin) { serverDolphin.register(new TutorialDirector()); } }
<servlet> <display-name>TutorialServlet</display-name> <servlet-name>tutorial</servlet-name> <servlet-class>step_7.servlet.TutorialServlet</servlet-class> </servlet><servlet-mapping> <servlet-name>tutorial</servlet-name> <url-pattern>/tutorial/</url-pattern> </servlet-mapping>
public static void main(String[] args) throws Exception { ClientDolphin clientDolphin = new ClientDolphin(); clientDolphin.setClientModelStore(new ClientModelStore(clientDolphin)); HttpClientConnector connector = new HttpClientConnector(clientDolphin, "http://localhost:8080/myFirstDolphin/tutorial/"); connector.setCodec(new JsonCodec()); connector.setUiThreadHandler(new JavaFXUiThreadHandler()); clientDolphin.setClientConnector(connector); TutorialApplication.clientDolphin = clientDolphin; Application.launch(TutorialApplication.class); }
That is it!
We can now start the provided jetty server./gradlew :server-app:jettyRun
Some extra flexibility
You may have observed that we refactored the actual server-side printing into a service class with a service interface. This allows some extra flexibility when the server-side action depends on any technology that is only available on the server - say JEE, JPA, Spring, GORM, etc.Refactoring the access into an interface allows us to still use the same code with the in-memory mode for testing, debugging, profiling, and so on with a stub or mock implementation for the service interface.Final considerations
This has been a very small application to start with but we have touched all relevant bases from starting with a standalone view, through proper modularization, up to a remote client-server setup.We have used a "bare-bones" setup with 100% pure Java and a no dependencies beyond Java 7+ and OpenDolphin.This is to show that OpenDolphin is as "un-opinionated" as can be.In real life and in most of the demos that ship with OpenDolphin, we make additional use of Groovy, GroovyFX, and Grails. Note that you can use any client- and server-side framework and technology with OpenDolphin: Griffon, Eclipse RCP, Netbeans - JEE, Spring, Grails, Glassfish, JBoss, Hibernate, WebLogic, WebSphere, you name it.Remember: OpenDolphin is a library, not a framework.We don't lock you in, we are open.Of course, a full application has more use cases than managing a single text field.The use cases and demos chapter leads you through the typical use cases of master-detail views, form-based pages, collections of data, lazy loading, shared attributes, CRUD operations, and much more by describing the use case, explaining the OpenDolphin approach of solving it, and pointing to the respective demos.