• Table of contents
    1Introduction
    2Architecture
    3How to get started with OpenDolphin
    4OpenDolphin on the web
    5Use Cases and Demos
    6tbd Configuration and Setup
    7tbd Developer Zone
  • Quick Reference

Dolphin - where Enterprise Java meets Desktop Java

<< 2Architecture
(Quick Reference)
4OpenDolphin on the web >>

3 How to get started with OpenDolphin - Reference Documentation

Authors: The Dolphin team

Version: PRE-1.0

Table of Contents

3.1Adding nodes to a stage, registering an onAction handler
3.2Introducing a presentation model with one attribute and bind the value
3.3Logical separation between client and server
3.4Bind the "dirty" of presentation models to the view
3.5Split into modules/projects
3.6Enhanced view, let the "director" wire all application actions
3.7Remote setup

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:

This code illustrates a simple call to the JavaFX API. This code displays a window with the title "Dolphin Jump Start" displayed in the title of the window.

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); } }

You are free to also use any other Java-based widget toolkit at this point: Swing, AWT, SWT, Eclipse RCP, or any other UI toolkit that runs in a JVM (Java Virtual Machine). OpenDolphin doesn't care which UI toolkit is used.

Of course, the application needs some sensible content, which we will add right away. Note that we haven added any OpenDolphin specific code to our samples (yet).

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:

The code now contains references to the widgets

private TextField field;
private Button    button;

and an action handler

button.setOnAction(new EventHandler<ActionEvent>() {
    public void handle(ActionEvent actionEvent) {
        System.out.println("text field contains: "+field.getText());
    }
});

The printing of the field content is our "stand-in" for a real business logic. You can easily assume some persistence action at this point or "service" calls in general.

Now it is time to introduce OpenDolphin.

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

but the code has.

As an intermediate step, we have put the OpenDolphin setup and the usage in the same place. Don't worry if this looks ugly. We will clean it up in a minute.

Focus on these lines in the code:

We create a presentation model with the distinctive name "input" and an attribute for the "text" property.

PresentationModel input = clientDolphin.presentationModel("input", new ClientAttribute("text"));

Note that we do not define a "JumpStartPresentationModel" since presentation models in OpenDolphin are totally generic.

Behind the scenes (no pun intended) happens quite a lot:

  • 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.

While this happens, we bind the text property of the TextField (this is a JavaFX property) to the "text" attribute of the input presentation model

JFXBinder.bind("text").of(field).to("text").of(input);

Note the fluent API for setting up the binding.

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 as
bind "text" of field to "text" of input

Finally, 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.

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");
    }
});

When we now start the application we see in the log:

[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

The C prefixes illustrate statements that are logged from the Client. S prefixes denote statements that are logged from the Server.

The log statements are telling us that the presentation model has been created and the value changed from null to an empty String, the JavaFX default value for text fields.

Let's enter "abcd":

[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): []

Every single change is asynchronously sent to the server dolphin. Note that the user interface does not block.

Finally, we hit the button

[INFO] C: transmitting Command: PrintText
server text field contains: abcd
[INFO] S:     received Command: PrintText
[INFO] C: server responded with 0 command(s): []

Our server action performs its printing action asynchronously, particularly not in the UI thread! You can see the asynchronous behavior by the line ordering in the log above. If it were synchronous, lines 2 and 3 would never be in this order.

Note that even though all the logic runs in-memory, we have the first benefit from OpenDolphin:

All actions are executed asynchronously outside the UI thread.
We cannot accidentally block it by long-running or failed operations, which is a common error in UI development.

With the first dolphinized application running, let's clean up and add a bit more OpenDolphin goodness.

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";

The configuration setup now moves into the constructor:

public JumpStart() {
    config = new DefaultInMemoryConfig();
    textAttributeModel = config.getClientDolphin().presentationModel(MODEL_ID, new ClientAttribute(MODEL_ATTRIBUTE_ID, ""));
    config.getClientDolphin().getClientConnector().setUiThreadHandler(new JavaFXUiThreadHandler());
    config.registerDefaultActions();
}

This leaves the "start" method with "view" responsibilities only: the initial contruction and separate method calls for binding and registering actions.

@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(); }

We add an additional labeled checkbox to visualize the status: whether the text field - or more precisely the dolphin attribute that backs it - is considered "dirty".

As soon as we change the content of the text field, this checkbox will become selected (checked). If we remove our edits, it should become unselected (unchecked) again!

Here is how the binding for that requirement looks like:

JFXBinder.bind("text").of(textField).to(MODEL_ATTRIBUTE_ID).of(textAttributeModel);
JFXBinder.bindInfo("dirty").of(textAttributeModel.getAt(MODEL_ATTRIBUTE_ID)).to("selected").of(status);

At this point we see the next benefit of presentation model and attribute abstractions: they can provide more information about themselves and can carry additional state that is automatically updated and available for binding.

Each attribute has a "base" value. When the current value differs from that base value, it is considered "dirty". A presentation model is dirty, if and only if any of its attributes are dirty.

With this knowledge, we can even do a little more.

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);

Second, we also want the button to be enabled only when there is something reasonable to do, i.e. when there is some value change in the form. This is a very common requirement in business applications.

Now, JavaFX buttons do not have an "enabled" state, only a "disabled" state with the opposite logic. Luckily, our binding facilities are perfectly able to handle this with a converter:

Converter converter = new Converter<Boolean,Boolean>() {
    public Boolean convert(Boolean value) {
        return !value;
    }
};
JFXBinder.bindInfo("dirty").of(textAttributeModel).using(converter).to("disabled").of(button);

You probably guessed that this code looks nicer in Groovy. Yes, it does:

bindInfo "dirty" of textAttributeModel using { state -> !state } to "disabled" of button

Once the code is nicely broken into independent parts we can put the various parts into separate modules for better dependency management.

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 an onFinished handler:

public void handle(ActionEvent actionEvent) {
    clientDolphin.send(CMD_LOG, new OnFinishedHandlerAdapter() {
        @Override
        public void onFinished(List<ClientPresentationModel> presentationModels) {
            textAttributeModel.getAt(ATT_FIRSTNAME).rebase();
        }
    });
}

Please note that the onFinished handler will be called asynchronously inside the UI thread. It may trigger changes in the model store, which may lead to changes in the display and any such changes must occur in the UI thread.

The server module (or "controller" module if you wish) contains the step5.TutorialAction controller.

Both, client and server depend on the shared module, which makes the step5.TutorialConstants known to both parties. The shared module itself does not depend on anything.

The code that contains the shared constants now also cares for the uniqueness of certain Strings, particularly of IDs used to retrieve presentation models and named commands.

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; }

Splitting four classes into four different modules may look a bit over-engineered at this point but it is an indispensible step before we can go into true remoting and before we can instantly switch between in-memory- and client-server-mode.

If you fear that this is too much work for setting up the directory structure or the build-time dependencies: simply unzip one of the project templates and you are good to go.

We get the following benefits:

  • 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

The true value of the change is not visible in a screenshot, though, since it is in the behavior. The modified background color of the text field appears as soon as it becomes dirty and is set back to the original state when the dirty state is set back.

To make this happen, we enhance the binding with a little trick in the converter that adds the "dirty" style class to the text field when needed and removes it otherwise.

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);

The tutorial.css contains the definition of that style, which makes the code very flexible should we later decide to visualize the dirty state differently.

.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;
}

Once we have the view code so nicely refactored to be free of any other responsibility, we can spend some extra brain cycles on improving the look and feel, both when visualizing state but also for emphasizing state transitions.

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(); } });

Note that the transition is only created once but played as often as you click the button.

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());
    }
}

As with any servlet, you need to register it in the web.xml:

<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>

The step7.TutorialStarter can now move to the "client" module since it is no longer dependent on the combination of client and server. It can be cleaned from setting up the in-memory server and must of course point to the server URL:

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
and then as many TutorialStarter clients as we want.

Alternatively, we can now create a WAR file via Maven or Gradle and deploy it on any server you fancy.

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.

<< 2Architecture
4OpenDolphin on the web >>
Quick Reference (hide)

Action

Usage

Attribute

Usage
dirty
id
qualifier

Binding

Usage

Command

Usage

Dolphin

Usage

EventBus

Usage

Presentation Model

Usage
create
Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically.