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

Now, let’s create some code example for the described design.

We’re going to create a small login dialog:

The Interfaces

First, we create the interface for the view:

/**
 * Login view used by the controller.
 */
public interface LoginView extends View<LoginController> {

	/**
	 * Sets the username in the view.
	 *
	 * @param username Username to set.
	 */
	public void setUsername(String username);

	/**
	 * Sets a message in the view.
	 *
	 * @param message Message to set.
	 */
	public void setMessage(String message);

}

As you can see, the view involves only two methods. The first is to set the username to a default value, and the second, to set an error message, should the username or password prove incorrect. The view extends a View interface that contains standard methods shared by all view implementations.

Next, we turn to the interface for the controller:

/**
 * Example login controller used by the view.
 */
public interface LoginController extends Controller<LoginView> {

	/**
	 * Try to login with the username and password.
	 *
	 * @param username Username.
	 * @param password Password.
	 */
	public void login(String username, char[] password);

	/**
	 * Cancel the login process.
	 */
	public void cancel();

}

The two methods correspond to the two buttons on the form. The controller extends a Controller interface that contains standard methods shared by all controller implementations.

The Implementation

At this point, we will create implementations for the two interfaces.

A standard JPanel, implementing the View:

/**
 * Example login panel.
 */
public class LoginPanel extends JPanel implements LoginView {

	private static final Logger LOG = LoggerFactory.getLogger(LoginPanel.class);

	private LoginController ctrl;

	/**
	 * {@inheritDoc}
	 */
	public JPanel getViewUI() {
		return this;
	}

	/**
	 * {@inheritDoc}
	 */
	public LoginController getController() {
		return ctrl;
	}

	/**
	 * {@inheritDoc}
	 */
	public void setController(LoginController ctrl) {
		this.ctrl = ctrl;
	}

	/**
	 * {@inheritDoc}
	 */
	public void block() {
		// TODO We don't want to do this for now to keep the example simple
	}

	/**
	 * {@inheritDoc}
	 */
	public void unblock() {
		// TODO Implement this later!
	}

	/**
	 * {@inheritDoc}
	 */
	public void setUsername(String username) {
		LOG.info("SET USERNAME '" + username + "'");
		textFieldUsername.setText(username);
	}

	/**
	 * {@inheritDoc}
	 */
	public void setMessage(String message) {
		LOG.info("SET MESSAGE '" + message + "'");
		labelMessage.setText(message);
	}

	:

	/**
	 * Connect buttons with controller.
	 */
	private void initHandler() {
		buttonOK.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				// Call the controller to handle the login process
				ctrl.login(textFieldUsername.getText(), textFieldPassword
						.getPassword());
			}
		});
		buttonCancel.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				// Call the controller to cancel the login
				ctrl.cancel();
			}
		});
		setPreferredSize(new Dimension(300, 200));
	}

	:

}

A dummy Controller implementation:

/**
 * Example login controller implementation.
 */
public class LoginControllerImpl implements LoginController {

	private static final Logger LOG = LoggerFactory.getLogger(LoginControllerImpl.class);

	// This is basically NOT a good idea! The
	// controller should NOT contain a direct
	// reference to the UI framework (Swing)!
	// This is only done to keep the example simple.
	private JFrame frame;

	private LoginView view;

	/**
	 * Constructor with frame.
	 *
	 * @param frame
	 *            Frame.
	 */
	public LoginControllerImpl(final JFrame frame) {
		super();
		this.frame = frame;
		this.frame.addWindowListener(new WindowAdapter() {
			@Override
			public void windowClosing(final WindowEvent e) {
				cancel();
			}
		});

	}

	/**
	 * Current thread sleeps for some time.
	 *
	 * @param millis
	 *            Milliseconds.
	 */
	private void sleep(final long millis) {
		try {
			Thread.sleep(millis);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Checks if the password is "test".
	 *
	 * @param input
	 *            Password to check.
	 *
	 * @return If the password was "test" <code>true</code> else
	 *         <code>false</code>
	 */
	private boolean isPasswordCorrect(final char[] input) {

		// Simulate long user/pw check
		sleep(5000);

		boolean isCorrect = true;
		final char[] correctPassword = { 't', 'e', 's', 't' };

		if (input.length != correctPassword.length) {
			isCorrect = false;
		} else {
			isCorrect = Arrays.equals(input, correctPassword);
		}
		// Zero out the password.
		Arrays.fill(correctPassword, '0');
		return isCorrect;
	}

	/**
	 * Sets the view.
	 *
	 * @param view
	 *            View.
	 */
	public final void setView(final LoginView view) {
		this.view = view;
		this.view.setUsername("test");
		this.view.setMessage("");
	}

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

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

		if (isPasswordCorrect(password)) {
			LOG.info("LOGIN OK!");
			System.exit(0);
		} else {
			final String message = "INVALID USERNAME OR PASSWORD";
			LOG.info(message);
			view.setMessage(message);
		}

	}

}

The Naïve Approach

Things have been easy up to this point. Now, let’s connect the view of the controller with a Naïve Approach:

/**
 * Naive approach to connect controller and view.
 */
public final class LoginDialogNaiveApproach {

	/**
	 * Runs in the Event Dispatch Thread (EDT).
	 */
	private void startIntern() {

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

		// Create view (=panel) and show it in a frame
		final LoginPanel view = new LoginPanel();
		final JFrame frame = Utils4Swing.createShowAndPosition("Password Example Dialog", view,
				false, new ScreenCenterPositioner());
		frame.getRootPane().setDefaultButton(view.getDefaultButton());

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

		// Connect controller and view
		ctrl.setView(view);
		view.setController(ctrl);

	}

	/**
	 * Start the application in EDT thread.
	 */
	public final void start() {
		if (SwingUtilities.isEventDispatchThread()) {
			startIntern();
		} else {
			SwingUtilities.invokeLater(new Runnable() {
				public void run() {
					startIntern();
				}
			});
		}
	}

	private static void configureLog4J() {
		:
	}

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

}

Running the example and simply pressing OK results in the following console output:

[AWT-EventQueue-0 ] -SET USERNAME ‘test’
[AWT-EventQueue-0 ] -SET MESSAGE ”
[AWT-EventQueue-0 ] – LOGIN USER test
[AWT-EventQueue-0 ] – INVALID USERNAME OR PASSWORD
[AWT-EventQueue-0 ] – SET MESSAGE ‘INVALID USERNAME OR PASSWORD’

Everything is carried out within the Event Dispatch Thread… When the example is executed and the OK button is pressed, you will notice that it locks up, that is to say, that it will remain pressed until to the controller method returns, a situation wish to have no part of, as this is not what we want!

The Superior Approach

If the design described in the previous articles is used, it’s quite simple to get things done the right way:

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

	private static ExecutorService createExecutorService() {
		final ExecutorService executorService = Executors.newCachedThreadPool();
		Runtime.getRuntime().addShutdownHook(new Thread() {
			@Override
			public void run() {
				// shutdown thread pool
				executorService.shutdown();
			}
		});
		return executorService;
	}

	/**
	 * Runs in the Event Dispatch Thread (EDT).
	 */
	private void startIntern() {

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

		// Create view (=panel) and show it in a frame
		final LoginPanel view = new LoginPanel();
		final JFrame frame = Utils4Swing.createShowAndPosition("Password Example Dialog", view,
				false, new ScreenCenterPositioner());
		frame.getRootPane().setDefaultButton(view.getDefaultButton());

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

		// Connect controller and view
		new ControllerViewConnector<LoginController, LoginView>("Login", 	"org.fuin.apps4swing.example.controller.swing", LoginController.class, ctrl, "org.fuin.apps4swing.example.view.swing", LoginView.class, view, createExecutorService(), 4);

	}

	/**
	 * Start the application in EDT thread.
	 */
	public final void start() {
		if (SwingUtilities.isEventDispatchThread()) {
			startIntern();
		} else {
			SwingUtilities.invokeLater(new Runnable() {
				public void run() {
					startIntern();
				}
			});
		}
	}

	private static void configureLog4J() {
		:
	}

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

}

The output will be as follows:

[AWT-EventQueue-0 ] – SET USERNAME ‘test’
[AWT-EventQueue-0 ] – SET MESSAGE ”
[pool-1-thread-2 ] – LOGIN USER test
[pool-1-thread-2 ] – INVALID USERNAME OR PASSWORD
[AWT-EventQueue-0 ] – SET MESSAGE ‘INVALID USERNAME OR PASSWORD’

Now, the controller code is executed in a separate thread, while the methods of the panel are executed in the EDT thread.

The piece of code working its magic is as follows:

// Connect controller and view
new ControllerViewConnector<LoginController, LoginView>(
		"Login",
		"org.fuin.apps4swing.example.controller.swing",
		LoginController.class,
		ctrl,
		"org.fuin.apps4swing.example.view.swing",
		LoginView.class,
		view,
		createExecutorService(),
		4);

With this code, the ControllerQueue, ToControllerDispatcher, and ToViewDispatcher implementations are created on-the-fly and appropriately linked.

The complete code for the example can be found here:

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

As of yet, the path looks promising, but the quest continues.

Advertisements

2 thoughts on “In Search of the Holy Grail of Swing MVC (Part 3)

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