Implementing The MVC Workbench

The MVCWorkbench

The MVCWorkbench is my main class (i.e., the class that contains main). An instance of this class—called a workbench-- is a desktop window.

One design problem I faced trying to implement the Model-View-Controller pattern was that "view" duties seem to be split between the desktop window (which contains the menus) and the internal windows (which contain buttons, text fields, trees, tables, graphs, etc.) Both seem to need controllers, both need a reference to the current model, the controllers need to know about them, and both need to know about each other.

Of course the menus, workbench model, an the workbench should be singletons (we shouldn't have more that one of each of these), so I decided to store references to these objects in public static fields that will be visible throughout the program. Note: This may not be a very good design.

public class MVCWorkbench extends JFrame
implements Runnable, ActionListener {
    private static final long serialVersionUID = 1L;
    punlic static JDesktopPane desktop;
    public static JMenuBar menuBar;
    public static JMenu viewMenu;
    public static WorkbenchModel wbModel;
     public static Model currentModel;
    private static int openFrameCount = 0;
    public static MVCWorkbench theWorkbench;

    public MVCWorkbench() {
         setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
         setDefaultLookAndFeelDecorated(true);
         desktop = new JDesktopPane(); //a specialized layered pane
         setContentPane(desktop);
         menuBar = new JMenuBar();
         setJMenuBar(menuBar);
         //Make dragging a little faster but perhaps uglier.
         desktop.setDragMode(JDesktopPane.OUTLINE_DRAG_MODE);
         wbModel = new WorkbenchModel();
         theWorkbench = this;
    }
    // display a view in an internal window
    public static void addView(View view) {
        JInternalFrame frame = new
            JInternalFrame("Document #" + (++openFrameCount),
                true, //resizable
                true, //closable
                true, //maximizable
                true); // iconifiable
        frame.setContentPane(view);
        frame.setSize(300,300);
        desktop.add(frame);
        frame.setVisible(true); //necessary as of 1.3
        try {
            frame.setSelected(true);
        } catch (java.beans.PropertyVetoException e) {}
    }
     public void actionPerformed(ActionEvent e) {    }

     public void run() {
       pack();
       setVisible(true);
    }

    public void display() {
      SwingUtilities.invokeLater(this);
    }

    public static void main(String[] args) {
       MVCWorkbench app = new MVCWorkbench();
       app.setTitle("MVC Workbench");
       app.display();
    }
}

 

The Model

Another design problem I faced: A model may need to inherit features from a Swing model (such as DefaultTableModel) and the Model class used by the workbench. Of course Java doesn't permit multiple inheritance. To solve this problem I turned the workbench model into an interface.

public interface Model extends java.io.Serializable {
   String getFileName();
   void setFileName(String newName);
   boolean getUnsavedChanges();
   void setUnsavedChanges(boolean flag);
   String getComponentName();
   void setComponentName(String newName);
   public void notifyViews();
}

The workbench provides the following default implementation. It implements view notification through the machinery inherited from the Observable class. (This is Java's version of the Publisher-Subscriber Design Pattern.)  The rest of the implementation is simple. Of course a model that inherits from one of the Swing models won't need to worry about notifying views because that functionality is built into the Swing models.

public class DefaultModel extends Observable implements Model {
   private String fileName;
   private String componentName;
   private boolean unsavedChanges;
   public DefaultModel() {
      fileName = null;
      componentName = null;
      unsavedChanges = false;
   }
   public String getFileName() { return fileName; }
   public void setFileName(String newName) { fileName = newName; }
   public boolean getUnsavedChanges() { return unsavedChanges; }
   public void setUnsavedChanges(boolean flag) {
      unsavedChanges = flag;
    }
   public String getComponentName() { return componentName; }
   public void setComponentName(String newName) {
      componentName = newName;
    }
   public void notifyViews() {
      setChanged();
      notifyObservers();
      clearChanged();
   }
}

There are, of course, two models: the current component model and the workbench model. (After all, the workbench follows the Model-View-Controller architecture just like the components do.) Here's a sketch of that model:

class WorkbenchModel extends DefaultModel {

   private Map<String, MVCComponent> components;
   private Model currentModel;
   public WorkbenchModel() {
      components = new Hashtable<String, MVCComponent>();
   }
   // application-specific logic:
   public void add(MVCComponent mvc) {
      components.put(mvc.getName(), mvc);
      setUnsavedChanges(true);
      notifyViews();

   }
   public void remove(String componentName) {
      components.remove(componentName);
      setUnsavedChanges(true);
      notifyViews();

   }
   public MVCComponent get(String componentName) {
      return components.get(componentName);
   }
   public Model getCurrentModel() { return currentModel; }
   public void setCurrentModel(Model model) {
      currentModel = model;
   }
   public Set<String> getComponentNames() {
      return components.keySet();
   }
}

Discovery

"Discovery" refers to the ability of a component container like MVCWorkbench to discover the capabilities of components that are added to it at runtime. Java provides reflection for just this purpose.

The Deploy Component use case

A component developer will create as many .class files as he likes. These files must be deployed on the user's machine in such a way that the default class loader can find them. For convenience, the .class files can be bundled into a .jar file.

Only the .class file containing the MVCComponent extension is special. Somehow this file must be easy to recognize for both humans and the workbench. One possible convention we could adopt is insisting that the file name must end with "Component.class".

Account.java

Accounts are bank account models:

public class Account extends DefaultModel {
   private double balance = 0;
   public void deposit(double amt) {
      balance += amt;
      setUnsavedChanges(true);
      notifyViews();

   }
   public void withdraw(double amt) {
      balance -= amt;
      setUnsavedChanges(true);
      notifyViews();

   }
   public double getBalance() { return balance; }
}

AccountView.java

An account view lives in an internal window and consists of one text field labeled balance. It also has two buttons labeled deposit and withdraw. Clicking on the deposit button causes a dialog box to pop up asking the user for the amount to be deposited. (Hint: Try using JOptionPane for this.) The balance is incremented by the specified amount. The balance fields of all open views are updated to show the new balance.

public class AccountView extends View {
   public static AccountController controller;
   private TextField balanceField;
   public AccountView(Account model) {
      super(model);
      controller = new AccountController(model);
      // add buttons and text fields
      JButton button1 = new JButton("Withdraw");
      button1.addActionListener(controller);
      // etc.
   }
   public void update(Observable o, Object arg) {
      Account myAccount = (Account)getModel();
      double newBalance = myAccount.getBalance();
      balanceField.setText("$" + newBalance);
   }
}

In this example we can use one controller for both menus and views:

class AccountController extends ActionListener {
   private Account model;
   public AccountController(Account model) {
      this.model = model;
   }
   public void setModel(Account model) {
      this.model = model;
   }
   public void actionPerformed(ActionEvent ae) {
      String cmmd = ae.getActionCommand();
      if (cmmd.equals("deposit")) {
         // get amount and
         model.deposit(amt);
      } else if ...
   }
}

AccountComponent.java

Here is the *Component class. It tells the workbench which class is the model class and which classes are the view classes. It also

public class AccountComponent implements MVCComponent {
   public String getName() { return "Account Component"; }
   public Class getModelClass() { return Account.class; }
   public Set<Class> getViewClasses() {
      Set<Class> result = new HashSet<Class>();
      result.add(AccountView.class);
      return result;
   }
   public Set<JMenu> getMenus(Account model) {
      Set<JMenu> result = new HashSet<Class>();
      JMenu menu1 = new JMenu("Manage Account");
      JMenuItem withdraw = new JMenuItem("Withdraw");
      AccountController controller = new AccountController(model);
      // or: AccountController controller = AccountView.controller;
      withdraw.addActionListener(controller);
      menu1.add(withdraw);
      // same for deposit
      result.add(menu1);
      // more menus may be added here
      return result;
   }
}

Implementing the Load Component use case

The listener for the "Component/Load Component" menu item prompts the user for the name of the component class:

String componentName = "AccountComponent";

The component is loaded and added to the component table in the workbench model:

Class componentClass = Class.forName(componentName);
MVCComponent component = componentClass.newInstance();
MVCWorkbench.wbModel.add(component);

Implementing the New Model use case

The listener for the "File/New Model" use case displays a list of installed components:

Set<String> choices = MVCWorkbench.wbModel.getComponentNames();

The user selects a component:

String componentName = "AccountComponent";

The component is fetched from the table:

MVCComponent component = MVCWorkbench.wbModel.get(componentName);

A new model is created:

MVCWorkbench.wbModel.setCurrentModel(
   component.getModelClass().newInstance());

New menus are added to the menu bar:

Model model = MVCWorkbench.wbModel.getCurrentModel();
for(JMenu menu: component.getMenus(model))) {
   MVCWorkbench.menuBar.add(menu);
}

Implementing the Open View use case

String componentName = MVCWorkbench.currentModel.getComponentName();
Set<Class> choices =    MVCWorkbench.wbModel.get(componentName).getViewClasses();
List<String> choiceList;
for (Class choice: choices) {
   choiceList.add(choice.getName());
}
display(choiceList);

Implementing the Select View use case

Class viewClass = Class.forName(viewType);
View view = (View)viewClass.newInstance();
MVCWorkbench.display(view);

P.S. I have no idea if any of this code will even compile!