January 31, 2012

How to implement MVC (Model View Controller) Pattern with Swing – Part 3

In my third blog about implementing MVC pattern in Swing I will look at how to implement global actions, context sensitive global actions, updating status bar and popup dialogs. But before delving into these issues and I would like again to repeat each class responsibility in the MVC pattern:

Model:
  1. Simple POJO model.
View:
  1. Layout out the Swing components.
  2. Responds to user action by sending request to Control.
  3. Responds to Controller responds.
Controller:
  1. Receives request from the View.
  2. Do logic.
  3. Sends Responds to View(s).
And a last reminder the Request and Responds to and from the Controller should be TOTALLY view technique neutral as the Model. The view specific code, in our case Swing code, ends in the view. Handling all Swing code is the responsibility of each view.

In my previous blog we have seen that each View is only responsible of its Swing component and what happens in other view is it totally unaware of. Try to think of separation of concern. It is the responsibility of the Controller to call the Response methods in each Views that should be updated.

Example:
    private class CreateDocumentAction extends AbstractAction {
        private static final long serialVersionUID = 1L;

        public CreateDocumentAction() {
            putValue(Action.NAME, "Create");
            putValue(Action.SHORT_DESCRIPTION, "Create Document");
            putValue(Action.SMALL_ICON, imageIcon("/org/tango-project/tango-icon-theme/16x16/actions/document-new.png"));
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            getDossierController().preCreateDocument(getModel());
        }
    }


And the corresponding Controller code:
    public void preCreateDocument(Dossier dossier) {
        // 1. do server logic
        Document document = new Document();
        document.setDossierId(dossier.getDossierId());
        // 2. do swing response
        getMainFrame().getView(DocumentModelView.class).setModel(document);
        getMainFrame().getView(DossierTreeModelView.class).addDocumentNode(document);
    }


The Toolbar is yet Another View

So now lets start with looking how to add a JToolBar to our Swing client. If you think of it for a while you will realize that the toolbar is yet another view. So lets write a new class that extends from our AbstractView.

package se.msc.mvcframework.demo.view;

import static se.msc.mvcframework.ComponentFactory.button;
import static se.msc.mvcframework.ComponentFactory.imageIcon;

import java.awt.event.ActionEvent;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JButton;
import javax.swing.JToolBar;

import se.msc.mvcframework.AbstractFrame;
import se.msc.mvcframework.AbstractView;
import se.msc.mvcframework.demo.controller.DossierController;

public class ToolBarView extends AbstractView<jtoolbar> {
    private JButton saveButton;
    private Action saveAction;

    public ToolBarView(AbstractFrame mainFrame) {
        super(mainFrame);
    }

    @Override
    protected JToolBar layout() {
        JToolBar toolBar = new JToolBar();
        toolBar.add(button(new OpenDossier()));
        toolBar.add(saveButton = button(saveAction = new SaveAction()));
        return toolBar;
    }

    // ---------- Request Code Goes Here

    public class OpenDossier extends AbstractAction {
        private static final long serialVersionUID = 1L;

        public OpenDossier() {
            putValue(Action.NAME, "Open");
            putValue(Action.SHORT_DESCRIPTION, "Open Dossier");
            putValue(Action.SMALL_ICON, imageIcon("/org/tango-project/tango-icon-theme/16x16/actions/document-open.png"));
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            getMainFrame().getController(DossierController.class).retrieveDossierView();
        }
    }

    public class SaveAction extends AbstractAction {
        private static final long serialVersionUID = 1L;

        public SaveAction() {
            putValue(Action.DEFAULT, AbstractFrame.GLOBAL_SAVE_ACTION);
            putValue(Action.NAME, "Save");
            putValue(Action.SHORT_DESCRIPTION, "Save");
            putValue(Action.SMALL_ICON, imageIcon("/org/tango-project/tango-icon-theme/16x16/actions/document-save.png"));
        }

        @Override
        public void actionPerformed(ActionEvent e) {
        }
    }

    // ---------- Response Code Goes Here

    public void setGlobalActions(Action[] actions) {
        resetGlobalActions();
        for (Action action : actions) {
            if (AbstractFrame.GLOBAL_SAVE_ACTION.equals(action.getValue(Action.DEFAULT)))
                saveButton.setAction(action);
        }
    }

    public void resetGlobalActions() {
        saveButton.setAction(saveAction);
    }
}


I have here used javax.swing.Action for base class, since they contain all graphical properties such icon, label and tooltip. But also javax.swing.Action can be used for JButton and in JPopupMenu and JMenuItem.

And I have also deliberately not created a abstract class for our action, which could have loaded our images, since I do not believe that such extra abstraction class will not bring any extra to our code nor reduce the number of lines. It will only make our client code more less understandable. But what would justify an extra abstraction layer is if abstract action class called the Controller in thread safe ways and change cursor to busy and in case of failure show a generic exception dialog. That would really bring something to our code, but not loading icons, using Action is quite straightforward and I like to see directly what my code does.

Handling Context Sensitive Global Actions

So after realized that the toolbar is just another view, lets move onto how to make the toolbar context sensitive, i.e. when switching active panel/window the save actions is replaced with the active panel/window save action. But before we must first decide where the concrete Swing save action should be located. It clearly belong to the concrete view. Think again of separation of concern. But what we must do is to expose the global actions so we can send them to the toolbar view. And lastly where do we wire the views together? In the Controller of course.

Let first add an extra method in our abstract view:
    public Action[] getGlobalActions() { return new Action[0]; }


Then in our controller we wire the global actions from the view to the toolbar view.
    public Action[] getGlobalActions() { return new Action[0]; }


And in our toolbar view:

    public void preCreateDocument(Dossier dossier) {
        // 1. do server logic
        Document document = new Document();
        document.setDossierId(dossier.getDossierId());
        // 2. do swing response  
getMainFrame().getView(ToolBarView.class).setGlobalActions(getMainFrame().getView(DocumentModelView.class).getGlobalActions());
        getMainFrame().getView(DocumentModelView.class).setModel(document);
        getMainFrame().getView(DossierTreeModelView.class).addDocumentNode(document);
    }


The last thing we need is to do is to switch back to the default when changing to other views. Here it can be justified to introduce some kind of View lifecycle mechanism, so the programmer does not have to concern about setting the correct toolbar action each time a new View is retrieved.

Managing a Statusbar

After thinking the GUI as a composition of different views, it should not be surprising to think of the statusbar as yet another view as well. And where is the respond sent to update this view. In the Controller of course. Here is a simple example how to implement a statusbar with swinglabs JXStatusBar.

se.msc.mvcframework.demo.view.StatusBarView
package se.msc.mvcframework.demo.view;

import javax.swing.JLabel;

import org.jdesktop.swingx.JXStatusBar;

import se.msc.mvcframework.AbstractFrame;
import se.msc.mvcframework.AbstractView;

public class StatusBarView extends AbstractView<jxstatusbar> {
    private JLabel statusLabel1;
    private JLabel statusLabel2;

    public StatusBarView(AbstractFrame mainFrame) {
        super(mainFrame);
    }

    @Override
    protected JXStatusBar layout() {
        statusLabel1 = new JLabel("Ready1");
        statusLabel2 = new JLabel("Ready2");

        JXStatusBar statusBar = new JXStatusBar();
        statusBar.add(new JLabel(), new JXStatusBar.Constraint(JXStatusBar.Constraint.ResizeBehavior.FILL));
        statusBar.add(statusLabel1, new JXStatusBar.Constraint(100));
        statusBar.add(statusLabel2, new JXStatusBar.Constraint(100));
        return statusBar;
    }

    // ---------- Request Code Goes Here

    // ---------- Response Code Goes Here

    public void setStatusText1(String statusText1) {
        statusLabel1.setText(statusText1);
    }

    public void setStatusText2(String statusText2) {
        statusLabel2.setText(statusText2);
    }
}


Managing Modular Popup Windows

The modular popup windows is also yet another view. The only difference it needs a parent JFrame to show from. And the solution to that is simple. Each View has a reference to the AbstractFrame that holds the JFrame via getFrame().

No comments: