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();
}
}
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" 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.
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".
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; }
}
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 ...
}
}
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;
}
}
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);
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);
}
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);
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!