Agenda

This tutorial describes how to leverage different MVC patterns, such as

  • Model - View - Controller

  • Model - View - Presenter

  • Model - View - ViewModel

  • PresentationModel - View - Controller

The goal is to build the same application using different patterns.

1. Definitions

1.1. MVC: Model-View-Controller

Perhaps the most well known pattern, also the one that many will get wrong and implement in an even worse way. The MVC pattern arose as a solution to keep 3 concerns separate from each other: visuals (View), data (Model), and logic (Controller). The pattern is easy to understand but hard to implement given that its generic description: a triangle where all parts can communicate with each other. Problem is, the links between some parts may be passive, for example some claim the View may only read the Model but do not update it directly, where as others claim data flows equally both ways.

The following diagram presents one of the many ways in which this pattern may be implemented; the dotted lines represent passive links.

mvc
Figure 1. MVC

Top

1.2. MVP: Model-View-Presenter

This pattern emerged as a counterpoint for MVC, trying to solve the triangle problem. In this pattern the View is passive and only reacts to data sent by the Presenter, which came from the Model. The View can route user events to the Presenter but does not know the Model. The Model in turn receives updates from the Presenter and notifies it of any state changes, resulting in an isolated Model. Only the Presenter knows the other two members of the pattern.

mvp
Figure 2. MVP

Top

1.3. MVVM: Model-View-ViewModel

The MVVM pattern is a weird variation if you ask me, as it puts the logic between the View and the ViewModel. The ViewModel is responsible for abstracting all View input/outputs while providing behavior at the same time. This simplifies (in theory) testing of an application built with this pattern as the ViewModel has the lion’s share of data and behavior, and it’s separated from the View. Perhaps the most interesting feature coming from this pattern is the availability of a Binder, used to synchronize data between View and ViewModel.

mvvm
Figure 3. MVVM

Top

1.4. PMVC: PresentationModel-View-Controller

The PMVC pattern is the ultimate variation of MVC, where we’re back to the triangle but this time we’ve got clear links between members. The PresentationModel is responsible for holding information used to display the application’s data and hints to how said data should be visualized, such as colors, fonts, etc. Similarly to MVVM, the View benefits from a Binder that can tie data from the PresentationModel to the View's UI elements. The Controller is now content to manipulate the PresentationModel directly, completely oblivious about a specific View. This leads to a much more testable outcome, as the PresentationModel and Controller are completely separate from the View.

pmvc
Figure 4. PMVC

Top

2. Implementations

It’s time to get down with business. The following sections describe the implementation of each pattern using the Griffon framework. All applications use JavaFX as the toolkit of choice.

2.1. Common Files

Regardless of the pattern all UIs rely on FXML to describe the View in a declarative approach as much as possible. The following FXML file is thus shared by all applications

griffon-app/resources/org/example/sample.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane id="AnchorPane" maxHeight="-Infinity" maxWidth="-Infinity"
            minHeight="-Infinity" minWidth="-Infinity"
            prefHeight="80.0" prefWidth="384.0"
            xmlns:fx="http://javafx.com/fxml"
            fx:controller="org.example.SampleView">
    <Label layoutX="14.0" layoutY="14.0" text="Please enter your name:"/>
    <TextField fx:id="input" layoutX="172.0" layoutY="11.0"
               prefWidth="200.0"/>
    <Button layoutX="172.0" layoutY="45.0"
            mnemonicParsing="false"
            prefWidth="200.0"
            text="Say hello!"
            styleClass="btn, btn-primary"
            fx:id="sayHelloActionTarget"/>
    <Label layoutX="14.0" layoutY="80.0" prefWidth="360.0" fx:id="output"/>
</AnchorPane>

Input data is transformed by a simple service class, also shared by all applications

griffon-app/services/org/example/SampleService.java
package org.example;

import griffon.core.artifact.GriffonService;
import griffon.core.i18n.MessageSource;
import griffon.metadata.ArtifactProviderFor;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonService;

import static griffon.util.GriffonNameUtils.isBlank;
import static java.util.Arrays.asList;

@ArtifactProviderFor(GriffonService.class)
public class SampleService extends AbstractGriffonService {
    public String sayHello(String input) {
        MessageSource messageSource = getApplication().getMessageSource();
        if (isBlank(input)) {
            return messageSource.getMessage("greeting.default");
        } else {
            return messageSource.getMessage("greeting.parameterized", asList(input));
        }
    }
}

Finally the i18n messages required by the service

griffon-app/i18n/messages.properties
name.label = Please enter your name
greeting.default = Howdy stranger!
greeting.parameterized = Hello {0}

Alright, we can now have a look at each specific pattern.

Top

2.2. MVC

Let’s begin with the View, as this is the pattern member the user will interact. The View in this case is pretty straight forward, as it only needs to load the FXML file that contains the descriptive UI. However it also exposes two UI elements (input and output) in order for the Controller and Model to read data and supply updates.

griffon-app/views/org/example/SampleView.java
package org.example;

import griffon.core.artifact.GriffonView;
import griffon.inject.MVCMember;
import griffon.metadata.ArtifactProviderFor;
import javafx.fxml.FXML;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import org.codehaus.griffon.runtime.javafx.artifact.AbstractJavaFXGriffonView;

import javax.annotation.Nonnull;
import java.util.Collections;

@ArtifactProviderFor(GriffonView.class)
public class SampleView extends AbstractJavaFXGriffonView {
    private SampleController controller;

    @FXML private TextField input;
    @FXML private Label output;

    @MVCMember
    public void setController(@Nonnull SampleController controller) {
        this.controller = controller;
    }

    @Override
    public void initUI() {
        Stage stage = (Stage) getApplication()
            .createApplicationContainer(Collections.<String, Object>emptyMap());
        stage.setTitle(getApplication().getConfiguration().getAsString("application.title"));
        stage.setWidth(400);
        stage.setHeight(120);
        stage.setScene(init());
        getApplication().getWindowManager().attach("mainWindow", stage);
    }

    // build the UI
    private Scene init() {
        Scene scene = new Scene(new Group());
        scene.setFill(Color.WHITE);
        scene.getStylesheets().add("bootstrapfx.css");

        Node node = loadFromFXML();
        ((Group) scene.getRoot()).getChildren().addAll(node);
        connectActions(node, controller);                                      (1)

        return scene;
    }

    @Nonnull
    public TextField getInput() {                                              (2)
        return input;
    }

    @Nonnull
    public Label getOutput() {                                                 (2)
        return output;
    }
}
1 Connects user events to Controller
2 Exposes UI elements

Next comes the Model, responsible for holding the data required by the View. Notice that this particular implementation notifies the View using proper threading concerns, that is, data is pushed to the View only inside the UI thread.

griffon-app/models/org/example/SampleModel.java
package org.example;

import griffon.core.artifact.GriffonModel;
import griffon.inject.MVCMember;
import griffon.metadata.ArtifactProviderFor;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonModel;

import javax.annotation.Nonnull;
import java.util.Map;

@ArtifactProviderFor(GriffonModel.class)
public class SampleModel extends AbstractGriffonModel {
    private StringProperty output;
    private SampleView view;

    @Nonnull
    public final StringProperty outputProperty() {
        if (output == null) {
            output = new SimpleStringProperty(this, "output");
        }
        return output;
    }

    public void setOutput(String output) {
        outputProperty().set(output);
    }

    public String getOutput() {
        return output == null ? null : outputProperty().get();
    }

    @MVCMember
    public void setView(@Nonnull SampleView view) {
        this.view = view;
    }

    @Override
    public void mvcGroupInit(@Nonnull Map<String, Object> args) {
        outputProperty().addListener((observable, oldValue, newValue) -> {
            runInsideUIAsync(() -> view.getOutput().setText(newValue));        (1)
        });
    }
}
1 Notifies View

Finally we get to the Controller, where we can see how it reacts to user events (the click on a button), reads data from the View, transforms said data, then sends it to the Model. As we saw earlier, the Model closes the circuit by updating the View. You may be wondering, how is the connection established between Controller and View? If you look back at the FXML file you’ll notice that the button has an id equal to sayHelloActionTarget. That name is really close to one of the Controller's public methods. Also, in the View there’s a call to connectActions that takes a controller argument. Here Griffon expects a naming convention (the name of the action method plus the name of the button’s id), which if followed correctly, will make the a connection between Controller and View.

griffon-app/controllers/org/example/SampleController.java
package org.example;

import griffon.core.artifact.GriffonController;
import griffon.inject.MVCMember;
import griffon.metadata.ArtifactProviderFor;
import griffon.transform.Threading;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonController;

import javax.annotation.Nonnull;
import javax.inject.Inject;

@ArtifactProviderFor(GriffonController.class)
public class SampleController extends AbstractGriffonController {
    private SampleModel model;
    private SampleView view;

    @Inject
    private SampleService sampleService;

    @MVCMember
    public void setModel(@Nonnull SampleModel model) {
        this.model = model;
    }

    @MVCMember
    public void setView(@Nonnull SampleView view) {
        this.view = view;
    }

    @Threading(Threading.Policy.INSIDE_UITHREAD_ASYNC)
    public void sayHello() {
        String input = view.getInput().getText();                              (1)
        String output = sampleService.sayHello(input);                         (2)
        model.setOutput(output);                                               (3)
    }
}
1 Read input
2 Transform data
3 Set as output

Given the trivial nature of this application you may be wondering if it’s worthwhile having a Model at all. And you may be right, however the Model is shown here to compare the different implementations that follow.

Top

2.3. MVP

The View in this pattern is a passive one. Curiously enough we can implement it in the same way as we did with the MVC pattern. The resulting View turns out to be exactly the same as before.

griffon-app/views/org/example/SampleView.java
package org.example;

import griffon.core.artifact.GriffonView;
import griffon.inject.MVCMember;
import griffon.metadata.ArtifactProviderFor;
import javafx.fxml.FXML;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import org.codehaus.griffon.runtime.javafx.artifact.AbstractJavaFXGriffonView;

import javax.annotation.Nonnull;
import java.util.Collections;

@ArtifactProviderFor(GriffonView.class)
public class SampleView extends AbstractJavaFXGriffonView {
    private SamplePresenter presenter;

    @FXML private TextField input;
    @FXML private Label output;

    @MVCMember
    public void setPresenter(@Nonnull SamplePresenter presenter) {
        this.presenter = presenter;
    }

    @Override
    public void initUI() {
        Stage stage = (Stage) getApplication()
            .createApplicationContainer(Collections.<String, Object>emptyMap());
        stage.setTitle(getApplication().getConfiguration().getAsString("application.title"));
        stage.setWidth(400);
        stage.setHeight(120);
        stage.setScene(init());
        getApplication().getWindowManager().attach("mainWindow", stage);
    }

    // build the UI
    private Scene init() {
        Scene scene = new Scene(new Group());
        scene.setFill(Color.WHITE);
        scene.getStylesheets().add("bootstrapfx.css");

        Node node = loadFromFXML();
        ((Group) scene.getRoot()).getChildren().addAll(node);
        connectActions(node, presenter);                                       (1)

        return scene;
    }

    @Nonnull
    public TextField getInput() {                                              (2)
        return input;
    }

    @Nonnull
    public Label getOutput() {                                                 (2)
        return output;
    }
}
1 Connects user events to Presenter
2 Exposes UI elements

We’ll see some changes in the remaining pattern members. The Model is again just a data holder, with just a single property that takes care of the output to be displayed. There’s nothing more to it given that this class uses JavaFX properties, and as such we’ll use event listeners to let the Model notify the Presenter when a state change occurs.

griffon-app/models/org/example/SampleModel.java
package org.example;

import griffon.core.artifact.GriffonModel;
import griffon.metadata.ArtifactProviderFor;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonModel;

import javax.annotation.Nonnull;

@ArtifactProviderFor(GriffonModel.class)
public class SampleModel extends AbstractGriffonModel {
    private StringProperty output;

    @Nonnull
    public final StringProperty outputProperty() {
        if (output == null) {
            output = new SimpleStringProperty(this, "output");
        }
        return output;
    }

    public void setOutput(String output) {
        outputProperty().set(output);
    }

    public String getOutput() {
        return output == null ? null : outputProperty().get();
    }
}

Next we get to the Presenter. Griffon allows MVC members to have any name you deem fit, however their responsibilities must be clearly stated, using the @ArtifactProviderFor annotation and a given MVC interface. At the moment of writing there’s no GriffonPresenter interface, and it’s likely there’ll never be, as this example shows the current types are more than enough to provide the required behavior

griffon-app/controllers/org/example/SamplePresenter.java
package org.example;

import griffon.core.artifact.GriffonController;
import griffon.inject.MVCMember;
import griffon.metadata.ArtifactProviderFor;
import griffon.transform.Threading;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonController;

import javax.annotation.Nonnull;
import javax.inject.Inject;
import java.util.Map;

@ArtifactProviderFor(GriffonController.class)
public class SamplePresenter extends AbstractGriffonController {
    private SampleModel model;
    private SampleView view;

    @Inject
    private SampleService sampleService;

    @MVCMember
    public void setModel(@Nonnull SampleModel model) {
        this.model = model;
    }

    @MVCMember
    public void setView(@Nonnull SampleView view) {
        this.view = view;
    }

    @Override
    public void mvcGroupInit(@Nonnull Map<String, Object> args) {
        model.outputProperty().addListener((observable, oldValue, newValue) -> {
            runInsideUIAsync(() -> view.getOutput().setText(newValue));        (4)
        });
    }

    @Threading(Threading.Policy.INSIDE_UITHREAD_ASYNC)
    public void sayHello() {
        String input = view.getInput().getText();                              (1)
        String output = sampleService.sayHello(input);                         (2)
        model.setOutput(output);                                               (3)
    }
}
1 Read input
2 Transform data
3 Set as output
4 Notifies View

Last but not least, we have to cover the application’s configuration in terms of MVC definitions, given that we’re using presenter instead of controller.

griffon-app/conf/Config.java
import griffon.util.AbstractMapResourceBundle;

import javax.annotation.Nonnull;
import java.util.Map;

import static griffon.util.CollectionUtils.map;
import static java.util.Arrays.asList;

public class Config extends AbstractMapResourceBundle {
    @Override
    protected void initialize(@Nonnull Map<String, Object> entries) {
        map(entries)
            .e("application", map()
                .e("title", "sample")
                .e("startupGroups", asList("sample"))
                .e("autoShutdown", true)
            )
            .e("mvcGroups", map()
                .e("sample", map()
                    .e("model", "org.example.SampleModel")
                    .e("view", "org.example.SampleView")
                    .e("presenter", "org.example.SamplePresenter")
                )
            );
    }
}

The configuration is quite flexible to allow us a simple rename.

Top

2.4. MVVM

On to the third pattern, MVVM. The View is again a passive one. A Binder makes it easy to keep data in sync between the View and the ViewModel. I opted to make use of the Binder in the View, rendering it almost exactly the same as in the previous two patterns.

griffon-app/views/org/example/SampleView.java
package org.example;

import griffon.core.artifact.GriffonView;
import griffon.inject.MVCMember;
import griffon.metadata.ArtifactProviderFor;
import javafx.fxml.FXML;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import org.codehaus.griffon.runtime.javafx.artifact.AbstractJavaFXGriffonView;

import javax.annotation.Nonnull;
import java.util.Collections;

@ArtifactProviderFor(GriffonView.class)
public class SampleView extends AbstractJavaFXGriffonView {
    private SampleViewModel viewModel;

    @FXML private TextField input;
    @FXML private Label output;

    @MVCMember
    public void setViewModel(@Nonnull SampleViewModel viewModel) {
        this.viewModel = viewModel;
    }

    @Override
    public void initUI() {
        Stage stage = (Stage) getApplication()
            .createApplicationContainer(Collections.<String, Object>emptyMap());
        stage.setTitle(getApplication().getConfiguration().getAsString("application.title"));
        stage.setWidth(400);
        stage.setHeight(120);
        stage.setScene(init());
        getApplication().getWindowManager().attach("mainWindow", stage);
    }

    // build the UI
    private Scene init() {
        Scene scene = new Scene(new Group());
        scene.setFill(Color.WHITE);
        scene.getStylesheets().add("bootstrapfx.css");

        Node node = loadFromFXML();
        ((Group) scene.getRoot()).getChildren().addAll(node);
        connectActions(node, viewModel);                                       (1)
        input.textProperty().bindBidirectional(viewModel.inputProperty());     (2)
        output.textProperty().bindBidirectional(viewModel.outputProperty());   (2)

        return scene;
    }
}
1 Connects user events to ViewModel
2 Binds ViewModel and View

Now, the ViewModel contains data and behavior, essentially, a combination of Model and Controller/Presenter as seen in previous patterns

griffon-app/controllers/org/example/SampleViewModel.java
package org.example;

import griffon.core.artifact.GriffonController;
import griffon.inject.MVCMember;
import griffon.metadata.ArtifactProviderFor;
import griffon.transform.Threading;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonController;

import javax.annotation.Nonnull;
import javax.inject.Inject;

@ArtifactProviderFor(GriffonController.class)
public class SampleViewModel extends AbstractGriffonController {
    private SampleView view;
    private StringProperty input;
    private StringProperty output;

    @Nonnull
    public final StringProperty inputProperty() {
        if (input == null) {
            input = new SimpleStringProperty(this, "input");
        }
        return input;
    }

    public void setInput(String input) {
        inputProperty().set(input);
    }

    public String getInput() {
        return input == null ? null : inputProperty().get();
    }

    @Nonnull
    public final StringProperty outputProperty() {
        if (output == null) {
            output = new SimpleStringProperty(this, "output");
        }
        return output;
    }

    public void setOutput(String output) {
        outputProperty().set(output);
    }

    public String getOutput() {
        return output == null ? null : outputProperty().get();
    }

    @Inject
    private SampleService sampleService;

    @MVCMember
    public void setView(@Nonnull SampleView view) {
        this.view = view;
    }

    @Threading(Threading.Policy.INSIDE_UITHREAD_ASYNC)
    public void sayHello() {
        String input = getInput();                                             (1)
        String output = sampleService.sayHello(input);                         (2)
        setOutput(output);                                                     (3)
    }
}
1 Read input
2 Transform data
3 Set as output

The configuration is shown next.

griffon-app/conf/Config.java
import griffon.util.AbstractMapResourceBundle;

import javax.annotation.Nonnull;
import java.util.Map;

import static griffon.util.CollectionUtils.map;
import static java.util.Arrays.asList;

public class Config extends AbstractMapResourceBundle {
    @Override
    protected void initialize(@Nonnull Map<String, Object> entries) {
        map(entries)
            .e("application", map()
                .e("title", "sample")
                .e("startupGroups", asList("sample"))
                .e("autoShutdown", true)
            )
            .e("mvcGroups", map()
                .e("sample", map()
                    .e("view", "org.example.SampleView")
                    .e("viewModel", "org.example.SampleViewModel")
                )
            );
    }
}

In truth we could have had a Model member too, just like in the other pattern implementations. However MVVM prefers the Model to wrap a domain object or provide direct access to the data layer. As that’s not the case for this trivial application the Model disappears and the data properties are moved to the ViewModel. This also servers to showcase that MVC configurations can have less members than the stereotypical three.

Top

2.5. PMVC

Finally we meet the last pattern. We begin again with the View.

griffon-app/views/org/example/SampleView.java
package org.example;

import griffon.core.artifact.GriffonView;
import griffon.inject.MVCMember;
import griffon.metadata.ArtifactProviderFor;
import javafx.fxml.FXML;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import org.codehaus.griffon.runtime.javafx.artifact.AbstractJavaFXGriffonView;

import javax.annotation.Nonnull;
import java.util.Collections;

@ArtifactProviderFor(GriffonView.class)
public class SampleView extends AbstractJavaFXGriffonView {
    private SampleController controller;
    private SampleModel model;

    @FXML private TextField input;
    @FXML private Label output;

    @MVCMember
    public void setController(@Nonnull SampleController controller) {
        this.controller = controller;
    }

    @MVCMember
    public void setModel(@Nonnull SampleModel model) {
        this.model = model;
    }

    @Override
    public void initUI() {
        Stage stage = (Stage) getApplication()
            .createApplicationContainer(Collections.<String, Object>emptyMap());
        stage.setTitle(getApplication().getConfiguration().getAsString("application.title"));
        stage.setWidth(400);
        stage.setHeight(120);
        stage.setScene(init());
        getApplication().getWindowManager().attach("mainWindow", stage);
    }

    // build the UI
    private Scene init() {
        Scene scene = new Scene(new Group());
        scene.setFill(Color.WHITE);
        scene.getStylesheets().add("bootstrapfx.css");

        Node node = loadFromFXML();
        ((Group) scene.getRoot()).getChildren().addAll(node);
        model.inputProperty().bindBidirectional(input.textProperty());         (2)
        model.outputProperty().bindBidirectional(output.textProperty());       (2)
        connectActions(node, controller);                                      (1)

        return scene;
    }
}
1 Connects user events to Controller
2 Binds Model and View

Opposed to the other implementations, this View makes direct use of the Binder, thus foregoing the need to expose UI elements to other MVC members. In this way, the View is self contained and all of its links are properly established.

The Model goes back to being a simple data holder, but this time it contains properties for both input and output, as those define all the required data points.

griffon-app/models/org/example/SampleModel.java
package org.example;

import griffon.core.artifact.GriffonModel;
import griffon.metadata.ArtifactProviderFor;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonModel;

import javax.annotation.Nonnull;

@ArtifactProviderFor(GriffonModel.class)
public class SampleModel extends AbstractGriffonModel {
    private StringProperty input;
    private StringProperty output;

    @Nonnull
    public final StringProperty inputProperty() {
        if (input == null) {
            input = new SimpleStringProperty(this, "input");
        }
        return input;
    }

    public void setInput(String input) {
        inputProperty().set(input);
    }

    public String getInput() {
        return input == null ? null : inputProperty().get();
    }

    @Nonnull
    public final StringProperty outputProperty() {
        if (output == null) {
            output = new SimpleStringProperty(this, "output");
        }
        return output;
    }

    public void setOutput(String output) {
        outputProperty().set(output);
    }

    public String getOutput() {
        return output == null ? null : outputProperty().get();
    }
}

The last member we must discuss is the Controller itself.

griffon-app/controllers/org/example/SampleController.java
package org.example;

import griffon.core.artifact.GriffonController;
import griffon.inject.MVCMember;
import griffon.metadata.ArtifactProviderFor;
import griffon.transform.Threading;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonController;

import javax.annotation.Nonnull;
import javax.inject.Inject;

@ArtifactProviderFor(GriffonController.class)
public class SampleController extends AbstractGriffonController {
    private SampleModel model;

    @Inject
    private SampleService sampleService;

    @MVCMember
    public void setModel(@Nonnull SampleModel model) {
        this.model = model;
    }

    @Threading(Threading.Policy.INSIDE_UITHREAD_ASYNC)
    public void sayHello() {
        String input = model.getInput();                                       (1)
        String output = sampleService.sayHello(input);                         (2)
        model.setOutput(output);                                               (3)
    }
}
1 Read input
2 Transform data
3 Set as output

This Controller has no need for a particular View, it only needs a PresentationModel that delivers the required inputs and outputs. This greatly simplifies testing such components.

Top

3. Conclusions

As you can see, Griffon is flexible enough and can adapt itself to your preferred MVC pattern. There are other MVC variants, such as the Hierarchical model–view–controller, which is also automatically supported, given that Griffon enables hierarchical MVC groups and per-group contexts.

The preferred pattern is PMVC, as that’s the one suggested by the standard project templates, but as we’ve shown, it’s easy to migrate from one pattern to the next.

The full code for these applications can be found here.

Top