January 20, 2012

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

In my previous blog I lay the ground of the definition of the MVC pattern and showed you have to implement it with Swing technology. I also defined a minimum set of classes that will help you to separate the client code into the MVC components – View, Controller and Model.
In this blog I will implement a more complex Swing client to show you that the minimum set of framework classes still holds for upholding the separation of concern between the View, Controller and Model. And the MVC pattern really deliver a component based architecture. That is even if you decide to rearrange the views, that will not impose the previous written code of the views, you merely need to rewire the code in the controller. And maybe foremost the code will be clean and what I hope most, will be easy read and follow.

But first lets repeat the responsibilities of each class in the MVC pattern

View
Layout the Swing components.
Sends user action request to the Controller.
Updates the View from Controller responses.
Controller
Do business logic.
Sends responses to views.
Model A POJO.

And a few warnings:

Don't do any logic in the Swing Action, e.g. open other dialogs or frames, call for update in other views. All these code will only destroy the maintainability of you application, because what it is breaking the law of seperation of concern. A specific View should not be aware and shoould not care what other Views are doing. What the View should do is merely pass the user action to the Controller and it is the job of the Controller to decide what to do with the request.

For example. The Controller X recieves a request, does some logic, such as calling the Server Facade and recieves the responds. Call for update on View Y, Z and E and open a new dialog.

By keeping the swing logic in the Controller and also keeping the Controller free from Swing code, it will be easy to go back later to understand the logic and flow in the client, but also for other to read and finally to maintain. It will only be in one place you need to change your code if the working flow or logic changes.

So now lets discuss the example. It is a internal frame solution, that only got one internal frame a dossier window. The dossier window contains document and present these structure in a tree. The detail of each tree node is shown in the same internal frame but in a detail panel. Here is a snapshot of the example.



Lets start with Main class.
1. It creates and saves all Views and Controllers in HashMap that the base class holds, so that all Views and Controller will be accessible through the entire client.
2. Lay out the JFrame with a default view.

package se.msc.mvcframework.demo.main;

import static se.msc.mvcframework.ComponentFactory.frame;

import javax.swing.JDesktopPane;
import javax.swing.JFrame;

import se.msc.mvcframework.AbstractFrame;
import se.msc.mvcframework.InternalFrameView;
import se.msc.mvcframework.demo.controller.DossierController;
import se.msc.mvcframework.demo.view.DocumentFormView;
import se.msc.mvcframework.demo.view.DossierFormView;
import se.msc.mvcframework.demo.view.DossierTreeView;
import se.msc.mvcframework.demo.view.DossierView;

public class MainInternalFrame extends AbstractFrame {
    private JDesktopPane desktopPane;

    @Override
    protected void registerAllViews() {
        desktopPane = new JDesktopPane();
        views.put(DossierFormView.class, new DossierFormView(this));
        views.put(DocumentFormView.class, new DocumentFormView(this));
        views.put(DossierTreeView.class, new DossierTreeView(this));
        views.put(DossierView.class, new InternalFrameView<dossierview>(this, desktopPane, new DossierView(this)));
    }

    @Override
    protected void registerAllControllers() {
        controllers.put(DossierController.class, new DossierController(this));
    }

    @Override
    protected JFrame layout() {
        return frame("Demo MVC Framework", desktopPane, new ToolBarView(this).getContentPane());
    }

    public static void main(String[] args) {
        javax.swing.SwingUtilities.invokeLater(new Runnable() {

            public void run() {
                new MainInternalFrame().show();
            }
        });
    }
}


We have two form views that have simply to to important methods that populates the view – getValues and setValues.

Now for the Tree View. This View is more complex but only because Swing is so verbose. I will leave the detail of how to implement a modifiable tree you can download the source at the end of the blog or google. I will in upcoming blog present a better ways to decorate the existing swing components, to take a POJO Model and populate from that. And to hold that POJO Model, so you can later ask the swing component for a POJO Model instead of making tiring calling of get and set from the model to the swing component and vice versa. But showing you simplifying handling of swing components is out of the scoop of this blog. The import thing is to show you how to seperate the concern of the Views and Controllers and the interaction between them.

    // ------------ Request Code Goes Here
    
    @SuppressWarnings("unchecked")
    private <b> BeanTreeNode<b> getSelectedNode(Class<b> nodeClass) {
        return (tree.getSelectionPath() != null) ? (BeanTreeNode<b>) tree.getSelectionPath().getLastPathComponent() : null;
    }
    
    public class TreeMouseListener extends MouseAdapter {
        
        @Override
        public void mousePressed(MouseEvent e) {
            BeanTreeNode<object> node = getSelectedNode(Object.class);
            if (node == null) return;            
            if (e.isPopupTrigger()) documentPopupMenu.show((JComponent) e.getSource(), e.getX(), e.getY());
        }
        
        @Override
        public void mouseReleased(MouseEvent e) {
            BeanTreeNode<object> node = getSelectedNode(Object.class);
            if (node == null) return;            
            if (e.isPopupTrigger()) documentPopupMenu.show((JComponent) e.getSource(), e.getX(), e.getY());
        }
        
        @Override
        public void mouseClicked(MouseEvent e) {
            BeanTreeNode<object> node = getSelectedNode(Object.class);
            if (node == null) return;
            if (node.getBean() instanceof Dossier) getDossierController().retrieveDossier((Dossier) node.getBean());
            if (node.getBean() instanceof Document) getDossierController().retrieveDocument((Document) node.getBean());
        }
    }

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

        public CreateDocument(String name) {
            super(name);
        }

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

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

        public DeleteDocument(String name) {
            super(name);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            BeanTreeNode<object> node = getSelectedNode(Object.class);
            if (node.getBean() instanceof Document) getDossierController().DeleteDocument((Document) node.getBean());
        }
    }

    // ------------ Response Code Goes Here
    
    public void setDossier(Dossier dossier) {
        dossierNode.setBean(dossier);
    }

    public void addDocumentNode(Document document) {
        dossierNode.getBean().addDocument(document); // update views internal cache    
        BeanTreeNode<document> documentNode = new BeanTreeNode<document>(document, "Name", null);
        dossierNode.add(documentNode);
        treeModel.nodeStructureChanged(dossierNode);
        tree.setSelectionPath(new TreePath(documentNode.getPath()));
    }

    public void updateDocumentNode(Document document) {
        BeanTreeNode<document> documentNode = getSelectedNode(Document.class);
        documentNode.setBean(document);
        treeModel.nodeStructureChanged(documentNode);
        tree.setSelectionPath(new TreePath(documentNode.getPath()));
    }

    public void deleteDocumentNode(Document document) {
        dossierNode.getBean().removeDocument(document); // update views internal cache
        dossierNode.remove(getSelectedNode(Document.class));
        treeModel.nodeStructureChanged(dossierNode);
        tree.setSelectionPath(new TreePath(dossierNode.getPath()));
    }


As you can see in the code above the the tree View does not do anything in theirs action, just merely call the correct Controller method.

And now the Controller where we wire everything together.

package se.msc.mvcframework.demo.controller;

import java.util.Random;
import java.util.UUID;

import se.msc.mvcframework.AbstractController;
import se.msc.mvcframework.AbstractFrame;
import se.msc.mvcframework.demo.model.Document;
import se.msc.mvcframework.demo.model.Dossier;
import se.msc.mvcframework.demo.view.DocumentFormView;
import se.msc.mvcframework.demo.view.DossierFormView;
import se.msc.mvcframework.demo.view.DossierTreeView;
import se.msc.mvcframework.demo.view.DossierView;

public class DossierController extends AbstractController {

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

    public void showDossierView() {
        // 1. do server logic
        Dossier dossier = new Dossier();
        dossier.setDossierId(new Random().nextLong());
        dossier.setDossierNumber(UUID.randomUUID().toString());
        dossier.setTitle("My Dossier");
        // 2. do swing response  
        getMainFrame().getInternalFrameView(DossierView.class).setTitle(dossier.getTitle());
        getMainFrame().getView(DossierTreeView.class).setDossier(dossier);
        getMainFrame().getInternalFrameView(DossierView.class).show();
    }

    public void retrieveDossier(Dossier dossier) {
        // 1. do server logic
        // 2. do swing response
        getMainFrame().getView(DossierFormView.class).setValues(dossier);
        getMainFrame().getInternalFrameView(DossierView.class).getView().showDossierFormView();
    }
    
    public void retrieveDocument(Document document) {
        // 1. do server logic
        // 2. do swing response
        getMainFrame().getView(DocumentFormView.class).setValues(document);
        getMainFrame().getInternalFrameView(DossierView.class).getView().showDocumentFormView();
    }
    
    public void PreCreateDocument() {
        // 1. do server logic
        Document document = new Document();
        // 2. do swing response
        getMainFrame().getView(DossierTreeView.class).addDocumentNode(document);
        getMainFrame().getView(DocumentFormView.class).setValues(document);
        getMainFrame().getInternalFrameView(DossierView.class).getView().showDocumentFormView();
    }

    public void cancelCreateDocument(Document document) {
        // 1. do server logic
        // 2. do swing response
        getMainFrame().getView(DossierTreeView.class).deleteDocumentNode(document);
        getMainFrame().getInternalFrameView(DossierView.class).getView().showDossierFormView();
    }
    
    public void CreateUpdateDocument(Document document) {
        // 1. do server logic
        if (document.getDocumentId() == null) {
            document.setDocumentId(new Random().nextLong());
            document.setDocumentNumber(UUID.randomUUID().toString());
        }
        // 2. do swing response        
        getMainFrame().getView(DocumentFormView.class).setValues(document);
        getMainFrame().getView(DossierTreeView.class).updateDocumentNode(document);
    }
    
    public void DeleteDocument(Document document) {
        // 1. do server logic
        // 2. do swing response
        getMainFrame().getView(DossierTreeView.class).deleteDocumentNode(document);
        getMainFrame().getInternalFrameView(DossierView.class).getView().showDossierFormView();
    }
}


I hope by just reading the code in the Controller you will get the feeling what will happen. But the best part. Everything is type safe! You can click on the method and directly go the code!


The complete source code from https://sourceforge.net/projects/swingframework/files/.

No comments: