In Search of the Holy Grail of Swing MVC (Part 4)

Designing the Module

Now that we have established how to connect the controller and it’s view, we should step back and think about the structure of the whole dialog.

At the Java One 2009, Kenn Orr delivered an interesting keynote, in which he discussed Component Oriented Design.

His main ideas regarding Component Oriented Design are as follows:

  1. Expose a small set of functionalities as a service. Only give the API consumer access to functionality in the component’s contract.
  2. The look and implementation of a user interface portion of the component is almost completely hidden from clients; the only exposure of the UI is via a getComponent method which returns a JComponent. Thus, the implementation of the visual representation of the component may change freely over time, as the developer has nothing to hook onto. Swing was intended to have this separation (think UI delegates and models), however, many widgets aggregate some of the UI and model methods within the widget API
  3. Components do not extend anything. Aggregation is the name of the game. To build a more complex component, developers can assemble pre-built components.

Here, he was basically adressing Swing components, but I believe the same idea also applies to dialogs.

What does a typical usage of a login dialog look like?

// Get the login module from somewhere
LoginModule module = ...
try {
    // Show the login dialog with a predefined username
    String username = module.login("john");
    LOG.info("Login successful: " + username);
} catch (final CanceledException ex) {
    // User canceled the login process
    LOG.error(ex.getMessage());
}

The module’s interface is clearly fairly small:

public interface LoginModule {

    // Login with a predefined username
    public String login(String username) throws CanceledException;

    // Login with empty dialog
    public String login() throws CanceledException;

}

There is one big problem with this design, namely that the controller cannot implement the interface directly because it forwards control to the user. The latter means that the “login(..)” method will show the window and then immediately return to the caller (without result). As you may remember, the login process is finished when the user presses OK or CANCEL and the appropriate controller method is called. No return value is available until then, meaning that the above usage example will not work!

Transforming the Module Interface

To avoid this problem, let’s transform the interface:

@ModuleRef(LoginModule.class)
public interface LoginModuleImplIntf {

    public void login(LoginListener listener, String username);

    public void login(LoginListener listener);

    // Listener to get informed about the result
    public interface LoginListener {

        public void success(String username);

        public void failure(CanceledException ex);

    }

}

This is an exact 1:1 transformation of the above code. You may have noticed the “@ModuleRef” annotation, which connects the two interfaces. I’ll return to this point later. For now, we will add additional annotations to the Module interface:

@ModulImplIntfRef(LoginModuleImplIntf.class)
public interface LoginModule {

    @WaitForUserInput
    public String login(String username) throws CanceledException;

    @WaitForUserInput
    public String login() throws CanceledException;

}

Both classes are now connected using two simple annotations @ModulImplIntfRef and @ModuleRef. But what is the purpose of the @WaitForUserInput annotation? It is simply a marker informing us that the control will be forwarded to the user by means of this method, and it may take some time before a result is generated. All three annotations will later be used by a code generator I created to connect the Module, Controller, and View.

Controller acts as Module

In this example, I would like the controller to act as the module, meaning that it must implement the new LoginModuleImplIntf:

/**
 * Example login module and controller implementation.
 */
public class LoginControllerImpl implements LoginController, LoginModuleImplIntf {

    :

    private LoginListener listener;

    private WindowManager windowManager;

    :

    /**
     * {@inheritDoc}
     */
    public final void cancel() {
        LOG.info("cancel");

        // Hide the UI
        windowManager.close();

        if (listener != null) {
            // Notify the listener that the login was canceled
            listener.failure(new CanceledException());
            listener = null;
        }

    }

    /**
     * {@inheritDoc}
     */
    public final void verify(final String username, final char[] password) {
        LOG.info("verify '" + username + "'");

        if (isPasswordCorrect(password)) {
            LOG.info("LOGIN OK!");

            // Hide the UI
            windowManager.close();

            // Notify the listener that the login was successful
            listener.success(username);
            listener = null;

        } else {
            final String message = "INVALID USERNAME OR PASSWORD";
            LOG.info(message);
            view.setMessage(message);
        }

    }

    /**
     * {@inheritDoc}
     */
    public final void login(final LoginListener listener, final String username) {
        LOG.info("login '" + username + "'");

        // Show the UI
        windowManager.open();

        // Set values in view
        this.view.setUsername(username);
        this.view.setMessage("");

        // Keep the listener to inform on "cancel()" or "verify(..)"
        this.listener = listener;

    }

    /**
     * {@inheritDoc}
     */
    public final void login(final LoginListener listener) {
        LOG.info("login");

        // Show the UI
        windowManager.open();

        // Set values in view
        this.view.setUsername("");
        this.view.setMessage("");

        // Keep the listener to inform on "cancel()" or "verify(..)"
        this.listener = listener;

    }

    :

}

(Only new or changed methods are included in the above code; the rest is identical to the Dummy Controller Implementation in Part 3.)

The WindowManager is a simple interface that makes it possible for the controller to display or hide its window.

Redesigned Example

The “LoginDialogBetterApproach” example now looks like this:

/**
 * Better approach using an easy framework to connect controller and view.
 */
public final class LoginDialogBetterApproach {

    :

    /**
     * Start the application in calling (main) thread.
     */
    public final void start() {

        LOG.info("start()");

        try {

            // Initialize Look and Feel
            Utils4Swing.initSystemLookAndFeel();

            // Create view (=panel)
            final LoginPanel view = new LoginPanel();

            // Create controller
            final LoginController ctrl = new LoginControllerImpl();

            // The package we'll use for the generated byte code
            final String packageName = "org.fuin.examples.apps4swing.generated";

            // Connect module, controller and view
            final ModuleControllerViewConnector<LoginModule, LoginController, LoginView> cvc =
                new ModuleControllerViewConnector<LoginModule, LoginController, LoginView>(
                    "Login", // Basic name for the classes
                    packageName, // Controller package
                    LoginController.class, // Controller interface
                    ctrl, // Controller implementation
                    packageName, // View package
                    LoginView.class, // View interface
                    view, // View implementation
                    packageName, // Module package
                    LoginModule.class, // Module interface
                    createExecutorService(),
                    4);

            final LoginModule module = cvc.getModule();
            try {
                final String username = module.login("john");
                LOG.info("Login successful: " + username);
            } catch (final CanceledException ex) {
                LOG.error(ex.getMessage());
            }
            LOG.info("exit(0)");
            System.exit(0);
        } catch (final Throwable t) {
            LOG.error("exit(1)", t);
            System.exit(1);
        }

    }

    :

    /**
     * Starts the example.
     *
     * @param args
     *            Not used.
     */
    public static void main(final String[] args) {
        configureLog4J();
        new LoginDialogBetterApproach().start();
    }

}

Entering the password ‘test’ results in the following output:


[main] - start()
[pool-1-thread-1] - login 'john'
[AWT-EventQueue-0] - SET USERNAME 'john'
[AWT-EventQueue-0] - SET MESSAGE ''
[pool-1-thread-2] - verify 'john'
[pool-1-thread-2] - LOGIN OK!
[main] - Login successful: john
[main] - exit(0)

As you can see, the main threads will wait until the login result is made available.

Summary

Defining a new dialog now involves the follwing steps:

  1. Create the Controller interface (to be used by the View).
  2. Create the View interface (to be used by the Controller).
  3. Create the Module interface (for external use).
  4. Transform the Module interface into an Implementation Module interface (to be used by the module implementation).
  5. Annotate the two module interfaces with @ModulImplIntfRef, @ModuleRef, and @WaitForUserInput.
  6. Create a controller that implements the Module Implementation Interface and the Controller Interface.
  7. Create a view that implements the View interface (such as a JPanel).
  8. Use the ModuleControllerViewConnector to connect the Module, Controller, and View.

Please note that steps 1-6 are completely independent of the underlying UI framework (like Swing).

Here is a short UML diagram portraying the current implementation:

UML diagram

As usual, you can find the code for the example here:

(The Apps4J and Apps4Swing libraries have not yet been officially published.)

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s