7. Presentation and Control
The Model-View-Controller Architecture
One of the features of object-oriented development is that objects in the application domain can be directly represented by objects in the solution domain. Some authors call these objects proxies, entities, or semantic objects. For example:
The loading docks, aisles, and shipping orders in a warehouse might be represented by loading dock, aisle, and shipping order objects in an inventory control system.
The brushes, pens, canvases, and palettes in an artist's studio might be represented by brush, pen, canvas, and palette objects in a paint program.
The elevators, rudder, and ailerons on an airplane might be represented by elevator, rudder, and aileron objects in a flight simulator.
We represent application domain objects by solution domain objects, but what about the application domain itself? Does it make sense to have an object representing the warehouse, the artist's studio, or the airplane? What responsibilities would typically be assigned to such an object?
Many desktop applications instantiate the Model-View-Controller architecture, which is named after its three most important classes. Instances of the Model class often represent the application domain itself. Just as the application domain aggregates or is composed of application domain objects, the model aggregates or is composed of application domain proxies. The model controls how its proxies are created, manipulated, and destroyed. The model is also responsible for saving and restoring its proxies.
The Controller and View classes comprise the application's user interface. Instances of the controller class are responsible for handling messages sent by the user or by other objects. Typically, a message is a command to update the model in some way. For example, a flight simulator might provide users with a controller associated with the mouse and a controller associated with an on-screen control panel filled with buttons. These controllers translate mouse movements and button clicks into commands that control the altitude, attitude, and speed of the airplane model:
Instances of the View class are responsible for displaying the model or its components. In addition to a view window displaying the airplane in flight, the simulator could provide users with a view window displaying the current altitude, attitude, heading, and air speed. A special window might display information about the temperature, thrust, and fuel consumption rate of the airplane's engine.
While the model is a relatively stable component, the user interface is very volatile. In other words, changes to the user interface are more common than changes in the application data and logic. For this reason, it is desirable to keep the coupling between the model and the user interface (i.e., the views and controllers) as loose as possible. This prevents changes in the user interface from propagating to the model.
Independence of presentation logic and application logic is even important at the level of individual functions. For example, consider the following definition:
double cube(double num) {
double result = num * num * num;
System.out.println("cube = " + result); // bad idea!
return result;
}
While the "cube = ..." message might be considered user-friendly when cube() is called by a human, it becomes annoying screen clutter when it is called by another function, which is by far the more common case. This severely limits the reusability of this function. For example, we would never want to use this function in an application with a GUI. To put it another way, cube() tries to do too much. It not only computes the cube of num (application logic), it also communicates with the user (presentation logic). It would be more reusable if it left presentation decisions to its callers.
The Model-View-Controller design pattern formalizes the Model-View-Controller architecture and the presentation-application independence principle:
Model-View-Controller
[POSE]Other Names
MVC, Model-View-Controller architecture, Model-View architecture, Model-View Separation pattern, Document-View architecture.
Problem
The user interface is the component most susceptible to change. These changes shouldn't propagate to application logic and data.
Solution
Encapsulate application logic and data in a model component. View components are responsible for displaying application data, while controller components are responsible for updating application data. There are no direct links from the model to the view-controllers.
Static Structure
Note that navigation from views and controllers to their associated model is allowed, but not the reverse. A model is independent of its views and controllers, because it doesn't know about them:
For example, a model for a spread sheet might be a two dimensional array of cells containing data and equations for a family's budget:
Budget
The user can choose to view this data in many ways: bar charts, pie graphs, work sheets, etc. In addition, sub windows such as menus and tool bars contain controllers that allow the user to modify the budget's data. Here's a possible object diagram:
To take another example, a model component for a word processor might be a document containing a chapter of a book. The user can view the document as an outline (outline view), as separate pages (page layout view), or as a continuous page (normal view):
GUI Toolkits
A GUI toolkit is a library of components for building graphical user interfaces: windows, menus, toolbars, etc. MFC is a C++ library for building GUIs on Windows platforms. JFC is a library for building platform independent GUIs. The reader should consult Appendix 2 for a discussion of JFC.
Views and View Notification
Unlike other user interface components, views don't have pre-defined drawComponent() functions. Instead, it is the responsibility of the application developer to implement the drawComponent() function in classes derived from View. Typically, the implementation of drawComponent() uses a reference to the model (inherited from the View class) to navigate to the model, fetch relevant application data, then draw a picture of this data on its canvas.
Of course a view must redraw itself every time the application data encapsulated by the model changes. This raises a problem with the Model-View-Architecture. If the model doesn't know about the open views, how will it notify them that there has been a change they should record?
This is a classic event notification problem. As it did in Chapter ?, the Publisher-Subscriber pattern comes to our rescue. If models are publishers (Observables), then views can be subscribers (Observers):
Recall that subscribers must provide an update() function, which will be called by the publisher's notifyObservers() function. In the case of a view, the update() function calls the repaint() function implemented in various classes derived from View:
public class MyView extends AppView {
public void update(Observable o, Object m) {
repaint();
}
public void drawComponent(Graphics g) { ... }
// etc.
}
Scenario
When a new view is created, it subscribes to the model that is currently open for editing. If a controller subsequently changes the model's data, the inherited notify() function is called. This calls the update() function of each registered subscriber. In the pull variant, the update() function calls the draw() function, which fetches the model's modified data, then uses this information to redraw itself:
Example: MFC's Document-View Architeture
Frameworks were introduced in Chapter 3. An application framework is a horizontal framework that can be customized into any interactive application.
Microsoft Foundation Classes (MFC) is an application framework for building desktop applications for the MS Windows platforms (Windows NT, Windows 9x, Windows CE, Windows 2000). All MFC applications are based on a variant of the Model-View-Controller architecture called the Document-View architecture. (The document is the model.)
Suppose we want to use MFC to develop a primitive word processor. The skeleton of an MFC application is created by a code generator called the App Wizard. App Wizard asks the user a few questions about the application to be built, such as "what is the name of the application?", then generates several header and source files. We could begin by using the App Wizard to create an application called "WP" (for "Word Processor").
If we accept all default settings proposed by the App Wizard, then a number of class declarations will be generated for us, including CWPDoc and CWPView:
class CWPDoc : public CDocument { ... }; // in WPDoc.h
class CWPView : public CView { ... }; // in WPView.h
We can see the principle relationships and members of these four classes in a class diagram:
CDocument and CView are predefined MFC framework classes analogous to our Publisher and Subscriber classes, respectively. CWPDoc and CWPView are newly generated customization classes analogous to our Model and View classes, respectively. Of course the key member functions in the derived classes are stubs that must be filled in by the programmer. (Code entered by the programmer will be shown in boldface type.)
Word processor documents will be represented by instances of the CWPDoc class. We might edit the CWPDoc class declaration by adding a member variable representing the text of the document, and member functions for inserting, appending, and erasing text:
class CWPDoc : public CDocument
{
private:
CString m_text;
public:
CString GetText() { return m_text; }
void Erase(int start, int end);
void Insert(int pos, CString text);
void Append(CString text);
// etc.
};,
A document can notify each view in its view list (m_viewList) of data member changes by calling UpdateAllViews(0). This is typically called after a call to SetModifiedFlag(true), which sets the inherited m_bModified flag. If this flag is set to true when the user attempts to quit the application before saving the application data, a "Save changes?" dialog box will automatically appear. For example, here's how we might implement the CWPDoc's Append() member function:
void CWPDoc::Append(CString text)
{
m_text += text; // append text
SetModifiedFlag(true);
UpdateAllViews(0);
}
Serializing and deserializing application data is the job of the Serialize() member function in the CWPDoc class. This function is automatically called when "Open" or "Save" is selected from the application's File menu. MFC's CArchive class is similar to our ObjectStream class. Here's the code we would add to this stub:
void CWPDoc::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
ar << m_text;
}
else
{
ar >> m_text;
}
}
CView is an abstract class containing virtual member functions called OnUpdate() and OnDraw(). These functions are analogous to our update() and draw() functions, respectively. Implementations for these functions must be provided in the CWPView class.
The OnDraw() function receives a pointer to a graphical context, which is called a device context in MFC and is represented by instances of the CDC class. The view finds its document (hence the data to be displayed) by following the inherited m_pDocument pointer returned by CWPView::GetDocument().
Here's our implementation of OnDraw(), which simply draws the document's text within the rectangular region (an MFC CRect object) of the device context's canvas that is currently being displayed through the view's window (this region is called the client rectangle in MFC):
void CWPView::OnDraw(CDC* pDC)
{
CWPDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
CRect region; // = some rectangular region
GetClientRect(®ion); // region = client rectangle
CString text = pDoc->GetText(); // = text to draw
pDC->DrawText(text, ®ion, DT_WORDBREAK);
}
The document notifies all open views of changes to its text by calling UpdateAllViews(). This function traverses the document's view list, calling each view's OnUpdate() function. The CView class implements OnUpdate() as a virtual function with an empty body, but we can redefine OnUpdate() in the CWPView class. Obviously, OnUpdate() should call OnDraw(), but how can we make a valid device context argument to pass to OnDraw()? All of this is done automatically by CView::Invalidate():
void CWPView::OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint)
{
Invalidate(); // calls OnDraw(new CDC())
}
Finally, we can add a keyboard handler that will be automatically called each time the user presses a key. The handler simply appends the character typed to the document's text:
void CWPView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
CWPDoc* pDoc = GetDocument();
pDoc->Append(nChar);
}
Objects that can receive messages (we call these objects controllers, but in MFC they are called command targets) are instances of MFC classes with associated message maps. A message map is simply a table that lists each message a target might be interested in, and which member function (i.e., handler) to call if and when the message is received.
MFC programmers rarely make manual entries in message maps. Instead, they use a powerful Visual C++ tool called the Class Wizard, which allows a programmer to select a message, then automatically generates a stub for the corresponding handler and makes an entry in the message map:
We can now build an run our word processor. Note that we can edit several documents simultaneously by repeatedly selecting "New" or "Open ..." from the File menu. We can also open multiple views of any given document by repeatedly selecting "New Window" from the Window menu:
Building an Application Framework (AFW)
Recall that an application framework is a horizontal framework than can be customized into any desktop application that employs the services of the underlying platform. We present several version of a working application framework called AFW. Version 1.0 provides a console user interfaces. Subsequent versions provide graphical user interfaces.
AFW 1.0: A CUI Application Framework
Separation of user interface from application data and logic even applies to programs that employ console-based user interfaces (CUIs).
Design
AFW 1.0 consists of a Console class, which combines control and view responsibilities, a ConsoleModel class that serves as a super class for custom model classes, and an AppError class for reporting application-specific errors.
Implementation
Console Class
Consoles interact with users through platform-sensitive character streams. Consoles collaborate with the framework's ConsoleModel class to provide application-independent implementations of the following commands: help, about, quit, save, save as, load, and new.
public class Console {
protected BufferedReader stdin;
protected PrintWriter stdout;
protected PrintWriter stderr;
protected String prompt = "-> ";
protected ConsoleModel model;
public Console(ConsoleModel m) { ... }
public void controlLoop() { ... }
protected void help() { ... }
protected void about() { ... }
protected boolean handle(AppError exp) { ... }
private void save() throws IOException { ... }
private void saveChanges() throws IOException { ... }
private void newModel() throws Exception { ... }
private void load() throws IOException, ClassNotFoundException {
...
}
}
Console Constructor
The constructor creates the character streams:
public Console(ConsoleModel m) {
stdout = new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(System.out)), true);
stderr = new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(System.out)), true);
stdin = new BufferedReader(
new InputStreamReader(System.in));
model = m;
}
Control Loop
The control loop drives the application. It perpetually prompts the user for a command, executes the command if it is application-independent, or passes the command to the model if it is not, then displays the result:
public void controlLoop() {
boolean more = true;
String cmmd = " ";
String result = " ";
String done = "done";
stdout.println("type \"help\" for commands");
while(more) {
try {
stdout.print(prompt);
stdout.flush(); // force the write
cmmd = stdin.readLine();
if (cmmd.equals("quit")) {
saveChanges();
more = false;
} else if (cmmd.equals("help")) {
help();
} else if (cmmd.equals("about")) {
about();
} else if (cmmd.equals("save")) {
save();
stdout.println(done);
} else if (cmmd.equals("load")) {
load();
stdout.println(done);
} else if (cmmd.equals("new")) {
newModel();
stdout.println(done);
} else if (cmmd.equals("save as")) {
saveChanges();
model.setUnsavedChanges(true);
stdout.println(done);
} else { // an application-specific command?
result = model.execute(cmmd);
stdout.println(result);
}
} catch(AppError exp) {
more = handle(exp);
} catch (IOException exp) {
stderr.println("IO error, " + exp);
} catch (Exception exp) {
stderr.println("Serious error, " + exp);
more = false;
}
} // while
stdout.println("bye");
} // controlLoop
Help and About
After providing application-independent information, the model is given the opportunity to add application-dependent information:
protected void help() {
stdout.println("Console Help Menu:");
stdout.println(" about: displays application information");
stdout.println(" help: displays this message");
stdout.println(" load: load a saved model");
stdout.println(" new: create a new model");
stdout.println(" quit: terminate application");
stdout.println(" save: save model");
stdout.println(" save as: save model with a new name");
stdout.println("Application-specific Help Menu:");
model.help(stdout);
}
protected void about() {
stdout.println("Console Framework");
stdout.println("copyright (c) 2001, all rights reserved\n");
model.about(stdout);
}
Handling Application Errors
The default for handling application-specific errors is simply to print the error message and continue. Programmers who are unhappy with this simplistic strategy can redefine handle() in a Console subclass.
protected boolean handle(AppError exp) {
stderr.println("Application error, " + exp);
return true;
}
AppError.java
AppError provides a base class for all application-specific errors.
public class AppError extends Exception {
private String gripe;
public String toString() {
return gripe;
}
public AppError(String g) {
super(g);
gripe = g;
}
public AppError() {
super("unknown");
gripe = "unknown";
}
}
Saving Models
The first time a model is saved, the user is prompted for a file name. After that the model remembers its file name, which is used to create an object stream into which the model is written.
private void save() throws IOException {
String fname = model.getFname();
if (fname == null) { // first save
stdout.print("enter file name: ");
stdout.flush(); // force the write
fname = stdin.readLine();
model.setFname(fname);
}
if (model.getUnsavedChanges()) {
ObjectOutputStream obstream =
new ObjectOutputStream(
new FileOutputStream(fname));
model.setUnsavedChanges(false);
obstream.writeObject(model);
obstream.flush();
obstream.close();
}
}
If the user enters the load, new, save as, or quit commands, and if the current model has been modified since the last save command was issued, then the following function prompts the user to save the current model.
private void saveChanges() throws IOException {
if (model.getUnsavedChanges()) {
stdout.print("Save changes?(y/n): ");
stdout.flush(); // force the write
String response = stdin.readLine();
if (response.equals("y"))
save();
else
stdout.println("changes discarded");
}
}
Loading Models
This method prompts the user for the name of the file, uses the file name to create an object input stream, then reads the model from the stream.
private void load() throws IOException, ClassNotFoundException {
saveChanges();
stdout.print("enter file name: ");
stdout.flush(); // force the write
String fname = stdin.readLine();
ObjectInputStream ois =
new ObjectInputStream(new FileInputStream(fname));
model = (ConsoleModel) ois.readObject();
ois.close();
model.setUnsavedChanges(false); // necessary?
model.setFname(fname); // necessary?
}
Creating New Models
Potentially the most interesting feature of AFW is its ability to be customized dynamically. A customization of a more traditional application framework, like MFC, forces the programmer to specify some fixed number of model types at compile time. AFW uses reflection to allow the programmer to create a new model of any type from the console's prompt.
private void newModel() throws Exception {
saveChanges();
stdout.print("enter class name: ");
stdout.flush(); // force the write
String cname = stdin.readLine();
Class c = Class.forName(cname);
model = (ConsoleModel)c.newInstance();
}
Console Model Class
Naturally, the ConsoleModel implements the Serializable interface. It also provides a flag for indicating unsaved changes and storage for the name of the file where the model is stored.
public class ConsoleModel implements Serializable {
protected String unrecognized = "Unrecognized command: ";
private String fname = null;
private boolean unsavedChanges = false;
public boolean getUnsavedChanges() { return unsavedChanges; }
public void setUnsavedChanges(boolean flag) {
unsavedChanges = flag;
}
public String getFname() { return fname; }
public void setFname(String name) { fname = name; }
public String execute(String cmmd) throws AppError { ... }
public void help(PrintWriter str) {
str.println(
" Sorry, no application-specific help available");
}
public void about(PrintWriter str) {
str.println(
"Sorry, no application-specific info available");
}
// a test harness
public static void main(String[] args) {
Console cui = new Console(new ConsoleModel());
cui.controlLoop();
}
}
Scanning Commands
The simplest AFW customization extends the ConsoleModel class with a class that provides an implementation of the execute() method. Our default implementation tests the error handling mechanism and demonstrates how commands can be broken into token streams.
public String execute(String cmmd) throws AppError {
if (cmmd.equals("throw")) {
throw new AppError(unrecognized + cmmd);
}
StringTokenizer tokens = new StringTokenizer(cmmd);
String result = "You entered " +
tokens.countTokens() + " token(s): ";
while (tokens.hasMoreTokens()) {
result += tokens.nextToken() + ' ';
}
return result;
}
Test Harness Demo
Here is the output produced by the ConsoleModel's test harness:
-> help
Console Help Menu:
about: displays application information
help: displays this message
load: load a saved model
new: create a new model
quit: terminate application
save: save model
save as: save model with a new name
Application-specific Help Menu:
Sorry, no application-specific help available
-> about
Console Framework
copyright (c) 2001, all rights reserved
Sorry, no application-specific info available
-> throw
Application error, Unrecognized command: throw
-> 46 + 19 * 23
You entered 5 token(s): 46 + 19 * 23
->
Example: Account Manager
Programs like Quicken allow users to manage their accounts. Our version only allows users to manage a single account. To run the account manager, the user doesn't need to start a different console. We can continue the session begun with the ConsoleModel test harness, but change models dynamically using the new command:
-> new
enter class name: Account
done
-> help
Console Help Menu:
about: displays application information
help: displays this message
load: load a saved model
new: create a new model
quit: terminate application
save: save model
save as: save model with a new name
Application-specific Help Menu:
deposit AMT balance += AMT
withdraw AMT balance -= AMT
balance display balance
->
Here are a few examples of application-specific commands and errors:
-> deposit 250
done
-> balance
balance = $250.0
-> withdraw 900
Application error, insufficient funds
-> save
enter file name: acct1
done
->
After the model has been saved into the "acct1" file, a subsequent deposit is made. The console prompts us to save this change when the load command is issued. The user declines, the change is discarded, the account stored in "acct1" is loaded, and the old balance is restored:
-> deposit 50
done
-> balance
balance = $300.0
-> load
Save changes?(y/n): n
changes discarded
enter file name: acct1
done
-> balance
balance = $250.0
->
Account Class
The Account class demonstrates a typical customization of AFW 1.0. Note how little work is involved for the customizer.
public class Account extends ConsoleModel {
private double balance = 0.0;
public String execute(String cmmd) throws AppError { ... }
public void help(PrintWriter str) {
str.println(" deposit AMT balance += AMT");
str.println(" withdraw AMT balance -= AMT");
str.println(" balance display balance");
}
// test harness:
public static void main(String[] args) {
Console cui = new Console(new Account());
cui.controlLoop();
}
}
Executing Account Management Commands
The hardest part, also the only required part, is implementing the execute() method. A set of application-specific commands must be identified and executed. A few pre-defined exceptions are caught, converted into application-specific errors, and are re-thrown. Also note that after each deposit or withdrawl, the unsaved-changes flag is set.
public String execute(String cmmd) throws AppError {
try {
StringTokenizer tokens = new StringTokenizer(cmmd);
String op = tokens.nextToken();
if (op.equals("deposit")) {
double amt =
Double.valueOf(tokens.nextToken()).doubleValue();
if (amt < 0) {
throw new AppError("negative deposit: " + amt);
}
balance += amt;
setUnsavedChanges(true);
return "done";
} else if (op.equals("withdraw")) {
double amt =
Double.valueOf(tokens.nextToken()).doubleValue();
if (balance < amt) {
throw new AppError("insufficient funds");
}
balance -= amt;
setUnsavedChanges(true);
return "done";
} else if (op.equals("balance")) {
return "balance = $" + balance;
} else {
throw new AppError(unrecognized + cmmd);
}
} catch (NumberFormatException e) {
throw new AppError("Amount must be a number");
} catch (NoSuchElementException e) {
throw new AppError("usage: deposit/withdraw AMOUNT");
}
}
AFW 2.0: A GUI Application Framework
AFW 2.0 provides a graphical user interface with multiple views of the model. Customization is more difficult because programmers must extend the framework's model class and its view class.
Readers should consult the Polygon Viewer section below to see an example of an AFW 2.0 customization. (Actually, the Polygon Viewer is an AFW 3.0 customization, but many of the features are supported by AFW 2.0.)
Design
As with MFC, the Publisher-Subscriber pattern is used to handle the problem of notifying views of changes in the application data stored by the model. The framework provides an application window with a menu bar. Users could also place other controls in this window, which makes this window the application controller. Views are modeless dialog boxes that connect to the model.
Implementation
The AppWindow Class
The AppWindow class is a JFrame with a menu bar. It demonstrates a common technique for creating and handling menus.
public class AppWindow extends MainJFrame
implements ActionListener, MenuListener {
protected AppModel model;
private File currentDirectory = new File(".");
protected JMenu fileMenu, editMenu, viewMenu, helpMenu;
public JMenu makeMenu(String name, String[] items) { ... }
public AppWindow(AppModel m) { ... }
public void menuSelected(MenuEvent e) { ... }
public void menuDeselected(MenuEvent e) { }
public void menuCanceled(MenuEvent e) { }
public void actionPerformed(ActionEvent e) { ... }
protected void error(String gripe) { ... }
protected void handleCreateView() throws Exception { ... }
protected void handleNew() throws Exception { ... }
protected void handleLoad()
throws IOException, ClassNotFoundException { ... }
protected void handleSave() throws IOException { ... }
protected void handleSaveAs() throws IOException { ... }
protected void handleQuit() throws IOException { ... }
protected void handleCopy() { ... }
protected void handleCut() { ... }
protected void handlePaste() { ... }
protected void handleUndo() { ... }
protected void handleRedo() { ... }
protected void handleAbout() { ... }
protected void handleHelp() { ... }
private void saveChanges() throws IOException { ... }
}
The AppWindow Constructor
The constructor creates four menus, then adds them to the menu bar:
public AppWindow(AppModel m) {
model = m;
setTitle(model.getClass().getName() + " Control");
JMenuBar menuBar = new JMenuBar();
setJMenuBar(menuBar);
fileMenu = makeMenu("&File", new String[]
{"&New", "&Save", "Sa&ve As", "&Load", "&Quit"});
editMenu = makeMenu("&Edit", new String[]
{"&Copy", "Cu&t", "&Paste", null, "&Undo", "&Redo"});
viewMenu = makeMenu("&View", new String[] {"Create"});
helpMenu = makeMenu("&Help", new String[] {"&About", "&Help"});
menuBar.add(fileMenu);
menuBar.add(editMenu);
menuBar.add(viewMenu);
menuBar.add(helpMenu);
}
The Menu Maker Utility
Menus are created using the following (reusable) factory method. This method has two parameters, the name of the menu and an array of names of the menu items the menu will contain. Each name in the array is converted into a menu item with the AppWindow as its listener. We borrow a trick from MFC by allowing programmers to place the '&' character in front of the letter in the items name that will be used as the keyboard shortcut for the item. Otherwise the first letter is assumed to be the shortcut key.
public JMenu makeMenu(String name, String[] items) {
JMenu result;
int j = name.indexOf('&');
if ( j != -1) {
char c = name.charAt(j + 1);
String s = name.substring(0, j) + name.substring(j + 1);
result = new JMenu(s);
result.setMnemonic(c);
} else {
result = new JMenu(name);
}
for(int i = 0; i < items.length; i++) {
if (items[i] == null) {
result.addSeparator();
} else {
j = items[i].indexOf('&');
JMenuItem item;
if ( j != -1) {
char c = items[i].charAt(j + 1);
String s = items[i].substring(0, j) +
items[i].substring(j + 1);
item = new JMenuItem(s, items[i].charAt(j + 1));
item.setAccelerator(
KeyStroke.getKeyStroke(c, InputEvent.CTRL_MASK));
} else { // no accelerator or shortcut key
item = new JMenuItem(items[i]);
}
item.addActionListener(this);
result.add(item);
}
result.addMenuListener(this);
}
return result;
}
Disabling Menu Items
There are two techniques for dealing with unimplemented menu items. Either the item can be disabled or selecting the item can generate an error message. Here is a simple example of how the items on the edit menu are disabled.
public void menuSelected(MenuEvent e) {
int j = editMenu.getItemCount();
for(int i = 0; i < j; i++) {
JMenuItem mi = editMenu.getItem(i);
String arg2 = null;
if (mi != null)
arg2 = mi.getActionCommand();
if (arg2 != null &&
(arg2.equals("Copy") ||
arg2.equals("Cut") ||
arg2.equals("Paste"))) {
mi.setEnabled(false);
}
}
}
Handling Menu Selections
A simple-- but not particularly object-oriented-- way to handle menus is to provide a single handler in the AppWindow class. This handler contains a giant, multi-way conditional that transfers control to a protected, item-specific handler. Because these lower level handlers are protected, they can be easily redefined in an extension class.
public void actionPerformed(ActionEvent e) {
try {
if (e.getSource() instanceof JMenuItem) {
String arg = e.getActionCommand();
if (arg.equals("New")) handleNew();
else if (arg.equals("Load")) handleLoad();
else if (arg.equals("Save")) handleSave();
else if (arg.equals("Save As")) handleSaveAs();
else if (arg.equals("Quit")) handleQuit();
else if (arg.equals("Copy")) handleCopy();
else if (arg.equals("Cut")) handleCut();
else if (arg.equals("Paste")) handlePaste();
else if (arg.equals("Undo")) handleUndo();
else if (arg.equals("Redo")) handleRedo();
else if (arg.equals("Create")) handleCreateView();
else if (arg.equals("About")) handleAbout();
else if (arg.equals("Help")) handleHelp();
}
repaint(); // make the menu disappear
} catch (Exception x) {
error("Exception thrown: " + x);
}
}
An Error Handling Utility
AFW 2.0 deals with errors by displaying an error message dialog box. The JOptionPane class provides a very useful static method called showMessageDialog() that can be used to create and display 90% of all modal dialogs a programmer ever needs:
protected void error(String gripe) {
JOptionPane.showMessageDialog(null,
gripe,
"OOPS!",
JOptionPane.ERROR_MESSAGE);
}
Save Changes Utility
As in AFW 1.0, the user is warned when he is in danger of losing unsaved changes to the model. In AFW 2.0 the warning takes the form of a dialog box:
private void saveChanges() throws IOException {
if (model.getUnsavedChanges()) {
int response =
JOptionPane.showConfirmDialog(
null,
"Save changes?",
"Save Changes?",
JOptionPane.YES_NO_OPTION,
JOptionPane.QUESTION_MESSAGE);
if (response == 0) handleSave();
}
}
File/New Handler
As in AFW 1.0, users may ask the framework to dynamically create new models of arbitrary types:
protected void handleNew() throws Exception {
saveChanges();
String cname =
(String) JOptionPane.showInputDialog(null,
"Enter class name",
"New Model",
JOptionPane.QUESTION_MESSAGE);
if (cname != null) {
Class c = Class.forName(cname);
model = (AppModel)c.newInstance();
setTitle(model.getClass().getName() + " Control");
}
}
File/Load Handler
The pre-defined file chooser dialog is used by the "Load", "Save", and "Save As" items on the "File" menu.
protected void handleLoad()
throws IOException, ClassNotFoundException {
saveChanges();
JFileChooser fd = new JFileChooser();
fd.setCurrentDirectory(currentDirectory);
fd.showOpenDialog(this);
currentDirectory = fd.getCurrentDirectory();
File ff = fd.getSelectedFile();
String fname = null;
if (ff != null)
fname = ff.getName();
if (fname != null) {
ObjectInputStream ois =
new ObjectInputStream(new FileInputStream(fname));
model = (AppModel) ois.readObject();
ois.close();
model.setUnsavedChanges(false); // necessary?
model.setFname(fname); // necessary?
}
}
File/Save Handler
protected void handleSave() throws IOException {
String fname = model.getFname();
if (fname == null) { // first save
JFileChooser fd = new JFileChooser();
fd.setCurrentDirectory(currentDirectory);
fd.showSaveDialog(this);
currentDirectory = fd.getCurrentDirectory();
File ff = fd.getSelectedFile();
if (ff != null)
fname = ff.getName();
else
fname = null;
model.setFname(fname);
}
if (fname != null && model.getUnsavedChanges()) {
ObjectOutputStream obstream =
new ObjectOutputStream(
new FileOutputStream(fname));
model.setUnsavedChanges(false);
obstream.writeObject(model);
obstream.flush();
obstream.close();
}
}
File/Save As Handler
protected void handleSaveAs() throws IOException {
saveChanges();
model.setUnsavedChanges(true);
model.setFname(null);
handleSave();
}
File/Quit Handler
protected void handleQuit() throws IOException {
saveChanges();
System.exit(0);
}
Edit Menu Handlers
Item-specific handlers that must be redefined in a subclass can generate error messages in the framework:
protected void handleCopy() {
error("Sorry, this item isn't implemented");
}
Help Menu Handlers
protected void handleAbout() {
JOptionPane.showMessageDialog(null,
new String[] {"Application Framework",
"Copyright(c) 2001",
"All rights reserved"},
"About",
JOptionPane.INFORMATION_MESSAGE);
}
protected void handleHelp() {
error("Sorry, this item isn't implemented");
}
Create/View Handler
The "View" menu contains a single item: "Create". Like the File/New command, users can ask the framework to create a variety of types of views:
protected void handleCreateView() throws Exception {
AppView av = null;
String cname =
(String) JOptionPane.showInputDialog(null,
"Enter class name",
"New Model",
JOptionPane.QUESTION_MESSAGE);
if (cname != null) {
Class c = Class.forName(cname);
Constructor cons = c.getConstructor(
new Class[] {this.getClass(), model.getClass()});
av = (AppView) cons.newInstance(new Object[] {this, model});
}
av.show();
}
Our handler makes heavy use of reflection. We can't simply call the createInstance() method of the Class object, because this invokes the class' default constructor. Instead, we must locate and invoke the object representing the constructor that takes a model as a parameter.
A more usable View menu would list each known type of view as a menu item. This way users wouldn't need to remember the names of the types of views.
The AppView Class
Views are both model observers and closeable modeless dialogs.
abstract public class AppView extends JDialog implements Observer {
protected AppModel model;
protected AppWindow window;
public AppView(AppWindow parent, AppModel m) {
super(parent, m.getClass().getName() + " View", false);
window = parent; // necessary?
setSize(250, 250);
model = m;
model.addObserver(this);
addWindowListener(new Terminator());
}
private class Terminator extends WindowAdapter {
public void WindowClosing(WindowEvent e) {
setVisible(false);
AppWindow ap = (AppWindow)getOwner();
//ap.remView(this);
model.deleteObserver(AppView.this);
}
}
}
The AppModel Class
An AppModel is serializable and observable. As in AFW 1.0, it flags unsaved changes and it holds the name of the file in which it is stored. No execute() method is required. Model methods must either be invoked from controls in the AppWindow or by mouse clicks in a view window.
public class AppModel extends Observable implements Serializable {
private String fname = null;
private boolean unsavedChanges = false;
public boolean getUnsavedChanges() { return unsavedChanges; }
public void setUnsavedChanges(boolean flag) {
unsavedChanges = flag;
}
public String getFname() { return fname; }
public void setFname(String name) { fname = name; }
public String help(PrintWriter str) { return
"Sorry, no application-specific help available";
}
public String about(PrintWriter str) { return
"Sorry, no application-specific info available";
}
// a test harness
public static void main(String[] args) {
AppWindow w = new AppWindow(new AppModel());
w.setVisible(true);
}
}
Commands and Command Processors
Instead of directly modifying the model in response to user inputs, controllers create commands and forward them to a centralized command processor. It is the job of the command processor to execute the commands.
One advantage of this arrangement is that it makes it easy to provide several types of controllers that do the same thing. For example, most menu selections have corresponding tool bar buttons and hot key combinations that perform the same action. More advanced users prefer the tool bar or keyboard because they are faster to use, while beginners can make the GUI simpler by hiding the tool bar. We can avoid coding redundancy by having multiple controllers create the same type of command. Thus a menu selection and its corresponding toll bar button can create the same type of command object:
Controllers send the commands they create to a centralized command processor object. It is the job of the command processor to execute the commands it receives, but it can do a lot more. For example, by keeping track of all commands, the command processor can implement application-independent commands such as undo, redo, and rollback. All of this is summarized in the command processor pattern:
Command Processor
[POSA] [Go4]Other Names
Commands are also called actions and transactions.
Problem
A framework wishes to provide an undo/redo mechanism, and perhaps other features such as scheduling, concurrency, roll back, history mechanisms, or the ability to associate the same action to different types of controllers (e.g. for novice and advanced user).
Solution
Create an abstract base class for all commands in the framework. This base class specifies the interface all concrete command types must implement.
Commands are created by controllers such as menu selections, buttons, text boxes, and consoles. The commands are forwarded to a command processor, which can store, execute, undo, redo, and schedule them.
In the smart command variant of the pattern, commands know how to execute themselves. In the dumb command variant commands are simply tokens, and the command processor must know how to execute them.
Scenario
When a user clicks a button, selects a menu item, or activates some other type of controller, the controller creates an appropriate command object, then asks the command processor to execute it. If we are using dumb commands, then either the command processor must know how to execute the command, which implies some knowledge of the application, or the command processor must ask some other component, such as the model, to execute the command. In the smart command variant, the commands are objects that know how to execute and undo themselves:
AFW 3.0: A GUI Application Framework with a Command Processor
AFW 3.0 enhances AFW 2.0 by adding a command processor. See Polygon Viewer (below) for an example of an AFW 3.0 customization.
Design
Instead of adding a separate command processor class, AFW 3.0 simply assigns command processor duties to the AppWindow. The AppWindow maintains two stacks of commands: the stack of commands that can be undone, and the stack of commands that can be redone.
The Command class is the abstract base class for all application-specific commands. It extends the AbstractAction class, which is a limited version of the Command Processor pattern that's already provided by Swing.
The AFW Command Hierarchy
AFW 3.0 menu item handlers create commands and pass them to the command processor for execution. Each item has an associated Command class extention:
Implementation
The AppView and AppModel classes are unchanged from AFW 2.0.
The Command Class
Each command maintains a reference to the model it will alter when executed and a reference to the command processor that will execute it. Not all commands are undoable (for example: "Print"). A flag indicates if a particular command is undoable. Each command must provide an execute() method. Undoable commands must also provide an undo() method.
public abstract class Command extends AbstractAction {
protected AppModel model;
protected AppWindow window;
private boolean undoable;
public ActionEvent ae = null;
public boolean getUndoable() { return undoable; }
public void setUndoable(boolean b) { undoable = b; }
public Command(AppWindow w) {
undoable = true;
window = w;
model = window.getModel();
}
protected void error(String gripe) { // used?
JOptionPane.showMessageDialog(null,
gripe,
"OOPS!",
JOptionPane.ERROR_MESSAGE);
}
public void undo() throws AppError {
throw new AppError("this command can't be undone");
}
public void execute() throws AppError {
throw new AppError(
"Sorry, this command isn't implemented yet");
}
public void actionPerformed(ActionEvent evt) {
ae = evt;
window.execute(this);
}
}
Swing components can fire abstract actions which provide their own actionPerformed() methods. We can think of abstract actions as a general case of our more specific notion of an AFW 3.0 command. To preserve compatibility with Swing's abstract actions, we subclass the AbstractAction class and provide an actionPerformed() method that asks the command processor to execute this command.
The AppWindow Class
AFW 3.0 application windows are different from their AFW 2.0 ancestors in several ways. First, they provide the command processor machinery: an undo stack, a redo stack, and undo(), redo(), and execute() methods. Second, menu items create commands and pass them to the execute() method. Third, an AFW 3.0 application window has a toolbar with buttons that duplicate popular menu items. These buttons simply create the same commands objects their menu item counterparts create.
public class AppWindow extends MainJFrame
implements MenuListener {
protected AppModel model;
private File currentDirectory = new File(".");
protected JMenu fileMenu, editMenu, viewMenu, helpMenu;
private Stack undo = new Stack();
private Stack redo = new Stack();
public AppWindow(AppModel m) { ... }
protected void error(String gripe) { ... }
public AppModel getModel() { return model; }
// utilities for making menus & toolbars:
public JMenu makeMenu(String name, Command[] actions) { ... }
public JToolBar makeToolBar(Command[] actions) { ... }
// command processor methods:
public void undo() { ... }
public void redo() { ... }
public void execute(Command c) { ... }
// MenuListener methods:
public void menuSelected(MenuEvent e) { ... }
public void menuDeselected(MenuEvent e) { }
public void menuCanceled(MenuEvent e) { }
}
The AppWindow Constructor
The constructor creates a menu bar:
public AppWindow(AppModel m) {
model = m;
setTitle(model.getClass().getName() + " Control");
JMenuBar menuBar = new JMenuBar();
setJMenuBar(menuBar);
Creating Command Arrays
Next the constructor declares five arrays of command objects. The first four arrays are the command objects that will be fired by the File, Edit, Help, and View menus. The last array are the commands that will be fired by the toolbar. (Note the interesting syntax.)
Command[] fileCmmds = new Command[] {
new FileNewCommand(this),
new FileSaveCommand(this, false),
new FileSaveCommand(this, true),
new FileLoadCommand(this),
new FileQuitCommand(this)
};
Command[] editCommands = new Command[] {
new EditCommand(this, "Copy"),
new EditCommand(this, "Cut"),
new EditCommand(this, "Paste"),
null,
new EditUndoRedoCommand(this, true),
new EditUndoRedoCommand(this, false)
};
Command[] helpCommands = new Command[] {
new AboutCommand(this),
new HelpCommand(this, "help")
};
Command[] viewCommands = new Command[] {
new ViewCommand(this)
};
Command[] toolBarCommands = new Command[] {
new FileSaveCommand(this, false),
new FileLoadCommand(this),
null,
new EditUndoRedoCommand(this, true),
new EditUndoRedoCommand(this, false)
};
Creating Menus and the Toolbar
The first four arrays are then passed to a new menu factory method. The fifth array is passed to a toolbar factory method:
fileMenu = makeMenu("&File", fileCmmds);
editMenu = makeMenu("&Edit", editCommands);
viewMenu = makeMenu("&View", viewCommands);
helpMenu = makeMenu("&Help", helpCommands);
menuBar.add(fileMenu);
menuBar.add(editMenu);
menuBar.add(viewMenu);
menuBar.add(helpMenu);
JToolBar toolBar = makeToolBar(toolBarCommands);
Container pane = getContentPane();
pane.setLayout(new BorderLayout());
pane.add(toolBar, BorderLayout.NORTH);
} // AppWindow()
A Revised Menu Factory Method
The new menu factory method creates a menu containing items that "fire" the corresponding commands in the command array:
public JMenu makeMenu(String name, Command[] actions) {
JMenu result = null;
String s = name;
int j = name.indexOf('&');
if ( j != -1) {
char c = name.charAt(j + 1);
s = name.substring(0, j) + name.substring(j + 1);
result = new JMenu(s);
result.setMnemonic(c);
} else {
result = new JMenu(s);
}
for(int i = 0; i < actions.length; i++) {
if (actions[i] == null) {
result.addSeparator();
} else {
result.add(actions[i]);
}
}
return result;
}
A Toolbar Factory Method
The toolbar factory method creates a toolbar containing buttons that "fire" the corresponding item in the command array:
public JToolBar makeToolBar(Command[] actions) {
JToolBar result = new JToolBar(JToolBar.HORIZONTAL);
result.setFloatable(true);
for(int i = 0; i < actions.length; i++) {
if (actions[i] == null) {
result.addSeparator();
} else {
result.add(actions[i]);
}
}
return result;
}
Both factory methods work because our Command class extends the AbstractAction class.
The Command Processor Methods
The undo method attempts to pop the last command off of the undo stack and call its undo() method. If the stack is empty, the exception is caught, and the error dialog is displayed:
public void undo() {
try {
Command cmmd = (Command)undo.pop();
cmmd.undo();
redo.push(cmmd);
} catch(Exception e) {
error(e.toString());
}
}
The redo method attempts to pop the last command off of the redo stack and call its execute() method.
public void redo() {
try {
Command cmmd = (Command)redo.pop();
cmmd.execute();
undo.push(cmmd);
} catch(AppError e) {
error(e.toString());
} catch (Exception e) {
error(e.toString());
}
}
The execute() method executes its command parameter. If the command is undoable, it is pushed onto the undo stack:
public void execute(Command c) {
try {
c.execute();
if (c.getUndoable())undo.push(c);
repaint();
} catch(AppError e) {
error(e.toString());
} catch (Exception e) {
error(e.toString());
}
}
The FileCommand Base Class
The commands fired by all File menu items extend the FileCommand base class.
class FileCommand extends Command {
static protected File currentDirectory = new File(".");
public FileCommand(AppWindow w) {
super(w);
setUndoable(false);
}
protected void saveChanges() throws IOException { ... }
protected void handleSave() throws IOException { ... }
public void execute() {
try {
saveChanges();
} catch(Exception e) {
throw new AppError(e.toString());
}
}
}
Save Changes Utility
This saveChanges() method prompts the user to save unsaved model modifications:
protected void saveChanges() throws IOException {
if (model.getUnsavedChanges()) {
int response =
JOptionPane.showConfirmDialog(
null,
"Save changes?",
"Save Changes?",
JOptionPane.YES_NO_OPTION,
JOptionPane.QUESTION_MESSAGE);
if (response == 0) handleSave();
}
}
File/Save Handler
protected void handleSave() throws IOException {
String fname = model.getFname();
if (fname == null) { // first save
JFileChooser fd = new JFileChooser();
fd.setCurrentDirectory(currentDirectory);
fd.showSaveDialog(null);
currentDirectory = fd.getCurrentDirectory();
File ff = fd.getSelectedFile();
if (ff != null)
fname = ff.getName();
else
fname = null;
model.setFname(fname);
}
if (fname != null && model.getUnsavedChanges()) {
ObjectOutputStream obstream =
new ObjectOutputStream(
new FileOutputStream(fname));
model.setUnsavedChanges(false);
obstream.writeObject(model);
obstream.flush();
obstream.close();
}
}
The File/Load Command Class
class FileLoadCommand extends FileCommand {
public FileLoadCommand(AppWindow w) {
super(w);
putValue(Action.NAME, "Load");
putValue(Action.SHORT_DESCRIPTION, "Load model file");
setUndoable(false);
}
protected void handleLoad()
throws IOException, ClassNotFoundException {
saveChanges();
JFileChooser fd = new JFileChooser();
fd.setCurrentDirectory(currentDirectory);
fd.showOpenDialog(null);
currentDirectory = fd.getCurrentDirectory();
File ff = fd.getSelectedFile();
String fname = null;
if (ff != null)
fname = ff.getName();
if (fname != null) {
ObjectInputStream ois =
new ObjectInputStream(new FileInputStream(fname));
model = (AppModel) ois.readObject();
ois.close();
model.setUnsavedChanges(false); // necessary?
model.setFname(fname); // necessary?
}
}
public void execute() {
try {
System.out.println("Load selected");
handleLoad();
} catch (Exception e) {
throw new AppError(e.toString());
}
}
}
The File/Save Command Class
class FileSaveCommand extends FileCommand {
private boolean saveAsFlag = false;
public FileSaveCommand(AppWindow w, boolean sa) {
super(w);
saveAsFlag = sa;
if (!saveAsFlag) {
putValue(Action.NAME, "Save");
putValue(Action.SHORT_DESCRIPTION, "Save model to a file");
} else {
putValue(Action.NAME, "Save As");
putValue(Action.SHORT_DESCRIPTION,
"Save model to a new file");
}
setUndoable(false);
}
public void execute() {
try {
if (!saveAsFlag) {
handleSave();
} else {
saveChanges();
model.setUnsavedChanges(true);
model.setFname(null);
handleSave();
}
} catch (Exception e) {
throw new AppError(e.toString());
}
}
}
The File/New Command Class
class FileNewCommand extends FileCommand {
public FileNewCommand(AppWindow w) {
super(w);
putValue(Action.NAME, "New");
putValue(Action.SHORT_DESCRIPTION, "Create a new model");
setUndoable(false);
}
protected void handleNew() throws Exception {
saveChanges();
String cname =
(String) JOptionPane.showInputDialog(null,
"Enter class name",
"New Model",
JOptionPane.QUESTION_MESSAGE);
if (cname != null) {
Class c = Class.forName(cname);
model = (AppModel)c.newInstance();
window.setTitle(model.getClass().getName() + " Control");
}
}
public void execute() {
try {
handleNew();
} catch (Exception e) {
throw new AppError(e.toString());
}
}
}
The File/Quit Command Class
class FileQuitCommand extends FileCommand {
public FileQuitCommand(AppWindow w) {
super(w);
putValue(Action.NAME, "Quit");
putValue(Action.SHORT_DESCRIPTION, "Quit application");
setUndoable(false);
}
public void execute() {
try {
saveChanges();
System.exit(0);
} catch (Exception e) {
throw new AppError(e.toString());
}
}
}
The Edit Command Base Class
The commands fired by all Edit menu items extend the EditCommand base class:
class EditCommand extends Command {
static protected Object clipBoard;
public EditCommand(AppWindow w) {
super(w);
setUndoable(false);
}
public EditCommand(AppWindow w, String name) {
super(w);
setUndoable(false);
putValue(Action.NAME, name);
}
}
The Edit/Undo and Edit/Redo Command Class
The "Undo" and "Redo" items on the "Edit" menu both fire the same type of command. Executing this command simply calls the application window's undo() or redo() methods.
class EditUndoRedoCommand extends EditCommand {
private boolean redoCommand;
public EditUndoRedoCommand(AppWindow w, boolean redoFlag) {
super(w);
redoCommand = redoFlag;
if (!redoCommand) {
putValue(Action.NAME, "Undo");
putValue(Action.SHORT_DESCRIPTION, "Undoes last undoable");
} else {
putValue(Action.NAME, "Redo");
putValue(Action.SHORT_DESCRIPTION,
"Redoes last undone command");
}
setUndoable(false);
}
public void execute() {
if (redoCommand)
window.redo();
else
window.undo();
}
}
The Help Command Base Class
class HelpCommand extends Command {
public HelpCommand(AppWindow w) {
super(w);
setUndoable(false);
}
public HelpCommand(AppWindow w, String name) {
super(w);
setUndoable(false);
putValue(Action.NAME, name);
}
}
The Help/About Command Class
class AboutCommand extends HelpCommand {
public AboutCommand(AppWindow w) {
super(w);
putValue(Action.NAME, "About");
putValue(Action.SHORT_DESCRIPTION, "About this application");
}
protected void handleAbout() {
JOptionPane.showMessageDialog(null,
new String[] {"Application Framework",
"Copyright(c) 2001",
"All rights reserved"},
"About",
JOptionPane.INFORMATION_MESSAGE);
}
public void execute() {
handleAbout();
}
}
The View Command Base Class
class ViewCommand extends Command {
public ViewCommand(AppWindow w) {
super(w);
putValue(Action.NAME, "Create");
putValue(Action.SHORT_DESCRIPTION,
"Create a new view of the model");
setUndoable(false);
}
protected void handleCreateView() throws Exception {
AppView av = null;
String cname =
(String) JOptionPane.showInputDialog(null,
"Enter class name",
"New Model",
JOptionPane.QUESTION_MESSAGE);
if (cname != null) {
Class c = Class.forName(cname);
Constructor cons = c.getConstructor(
new Class[] {window.getClass(), model.getClass()});
av = (AppView) cons.newInstance(
new Object[] {window, model});
}
av.show();
}
public void execute() throws AppError {
try {
handleCreateView();
} catch(Exception e) {
throw new AppError(e.toString());
}
}
}
Example: Polygon Viewer
The polygon viewer is a customization of version 3.0 of AFW, but some of its features are also supported by version 2.0.
The application window has a menu bar and, in the version 3.0 customization, a toolbar with buttons that duplicate popular menu items:
Selection the Help/About menu item displays the modal About dialog box:
Selecting the File/Save, File/Save As, or the File/Load menu items displays variants of the File Finder dialog box:
Some menu items are unimplemented (the implementations are left as an exercise to the reader or to the customizer). These items are either disabled or display the Error dialog when selected:
Selecting the View/Create menu item displays the New View dialog box:
The user types the name of the view class, and the application uses the class loader to load the appropriate view class, then dynamically instantiates the class.
Here is a screen snapshot after two views of a five-sided polygon model have been created:
Clicking on either view with the left mouse button causes the number of sides to increment by one. This changes is made in the underlying polygon mode. All open views immediately redraw themselves using the updated number of sides:
Naturally, if the user attempts to quit the application or load a new model, the program prompts the user to save any modifications:
Design
Implementation
The Polygon Model
class Poly extends AppModel {
private int sides = 5;
public int getSides() { return sides; }
public void incSides() {
sides = Math.max((sides + 1) % 20, 3);
setUnsavedChanges(true);
setChanged();
notifyObservers();
clearChanged();
}
public void decSides() {
sides = sides - 1;
if (sides < 3) sides = 20;
setUnsavedChanges(true);
setChanged();
notifyObservers();
clearChanged();
}
// test harness:
public static void main(String[] args) {
AppWindow aw = new AppWindow(new Poly());
aw.setVisible(true);
}
}
The Increment Sides Command Class
class IncSidesCommand extends Command {
public IncSidesCommand(AppWindow w) {
super(w);
}
public void execute() {
Poly p = (Poly) model;
p.incSides();
}
public void undo() {
Poly p = (Poly) model;
p.decSides();
}
}
The PolyGraph View Class
public class PolyGraphView extends AppView {
PolyPanel myPanel;
public void update(Observable o, Object m) {
myPanel.repaint();
}
class PolyPanel extends JPanel { ... }
class MouseHandler extends MouseAdapter {
public void mouseClicked(MouseEvent e) {
window.execute(new IncSidesCommand(window));
}
}
public PolyGraphView(AppWindow aw, Poly p) {
super(aw, p);
Container contentPane = getContentPane();
myPanel = new PolyPanel();
contentPane.add(myPanel);
addMouseListener(new MouseHandler());
}
}
The PolyPanel Class
class PolyPanel extends JPanel {
public void paintComponent(Graphics g) {
super.paintComponent(g);
Poly pol = (Poly) PolyGraphView.this.model;
int sides = pol.getSides();
int red = 255;
int blue = 175;
int green = 0;
g.setColor(new Color(red, green, blue));
// calculate client area dimensions:
Dimension d = getSize();
Insets in = getInsets();
int clientWidth = d.width - in.right - in.left;
int clientHeight = d.height - in.bottom - in.top;
int xUpperLeft = in.right;
int yUpperLeft = in.top;
int xLowerRight = xUpperLeft + clientWidth;
int yLowerRight = yUpperLeft + clientHeight;
int xCenter = xUpperLeft + clientWidth/2;
int yCenter = yUpperLeft + clientHeight/2;
// draw polygon:
Polygon p = new Polygon();
double radius = Math.min(clientHeight, clientWidth)/2;
double angle = 2 * Math.PI/sides;
for(int i = 0; i < sides; i++)
p.addPoint(
(int)(xCenter + radius * Math.cos(i * angle)),
(int)(yCenter + radius * Math.sin(i * angle)));
g.drawPolygon(p);
// draw "sides = ..." string
g.setColor(Color.black);
String s = "# sides = " + sides;
FontMetrics fm = g.getFontMetrics();
int w = fm.stringWidth(s);
int h = fm.getHeight();
g.drawString(s, xCenter - w/2, yCenter - h/2);
}
} // paintComponent()
Mementos
AFW 3.0 customizers must implement execute() and undo() for each type of command. This can be a lot of work if there are many types of commands. If models are small, or if the amount of data encapsulated by the model that can't be re-calculated is small, then it may make sense to simply push the model's data onto the undo stack before each command is executed. Then, the undo operation simply restores the data into the model. Much of this work can be done in the framework. But there is a problem: how does the framework know what type of data is encapsulated by the model? The framework could simply make a copy of the entire model, but then all of the data is being saved, not just the non re-computable data. The Memento pattern solves this problem:
Memento
[Go4]Other Names
Token
Problem
The command processor pattern assumes commands know how to undo themselves. Using this pattern in a framework can mean a lot of work for customizers who must provide implementations of undo() in each of their concrete command classes. Alternatively, the framework's command processor or command base class could simply save the non re-computable state of the model before each command is executed. If the command is undone, then the former state of the model can easily be restored. Unfortunately, the internal state of a model is usually private. Allowing the framework to access this data would violate encapsulation.
Solution
A memento is an object created by a model that encapsulates some or all of its internal state at a given moment in time. The memento can be stored inside of a command or command processor without allowing access to its encapsulated data. The object that stores the memento is called a caretaker. The undo() function simply passes the memento back to the model, where the encapsulated data is extracted and used to restore the model's state to what it was at the time the memento was created.
Static Structure
When a command is executed, it first asks its model to make a memento encapsulating its state information. Although the command has no idea what's inside the memento, it keeps the memento until the command is undone:
Resource Managers
Programs normally have complete control over the objects they create. This is fine provided these objects are not shared or sensitive resources such as threads, windows, and databases. Giving a program complete control over such a resource could be risky. What would happen, for example, if a program created a fake desktop, modified a database while another program was querying it, or created a run away thread that couldn't be interrupted?
One way to prevent such problems is to associate a resource manager to each resource class (resources are instances of resource classes). The resource manager alone is responsible for creating, manipulating, and destroying instances of that class. Resource managers provide a layer of indirection between programs and the resources they use:
Resource Manager
[ROG]Other Names
Object manager, lifecycle manager
Problem
In some situations we may need to hide from clients the details how certain resources are allocated, deallocated, manipulated, serialized, and deserialized. In general, we may need to control client access to these objects.
Solution
A resource manager is responsible for allocating, deallocating, manipulating, tracking, serializing, and deserializing certain types of resource objects. A resource manager adds a layer of indirection between clients and resources. This layer may be used to detect illegal or risky client requests. In addition, the resource manager may provide operations that can be applied to all instances of the resource such as "save all", statistical information such as "get count", or meta-level information such as "get properties".
Static Structure
A resource manager is a singleton that maintains a table of all instances of the resource that are currently open for use:
class Manager {
public int open() { ... } // resource factory method
public bool close(int i) { ... }
public bool serviceA(int i) { ... }
public bool serviceB(int i) { ... }
// etc.
private Map open = new Hashtable();
private bool authorized(...); // various parameters
}
If the caller is authorized, the open() function creates a new resource object, places it in the table, then returns the index to the caller. Clients must use this index number when subsequently referring to the resource. For example, here's how an authorized client invokes the serviceA() method of a previously allocated resource:
bool serviceA(int i) {
if (!authorized(...)) return false; // fail
Resource r = (Resource)open.get(Integer(i));
r.serviceA();
return true; // success
}
Authorization can have a variety of meanings: Does the requested resource exist? Does the client have access rights to it? Is it currently available?Is the proposed operation legal?
An operating system provides resource managers for most classes of system resources:
Window Managers
Although applications may have objects representing user interface components, these are usually just handles that wrap references to system-level bodies that directly represent user interface components (recall the handle-body idioms discussed in Chapter ?). Like files, threads, and memory, user interface components are resources that are owned and managed by the operating system. A handle representing a user interface component in an application program merely delegates requests such as move, minimize, close, etc. through the operating system's window manager to its associated body:
For example, a user interface component in an MFC application is an instance of the CWnd class (which is analogous to our UIComponent class):
class CWnd: public CCmdTarget
{
public:
HWND m_hWnd; // "points" to a system UI component
// etc.
};
m_hWnd is an index into a table of open windows. Most CWnd member functions simply pass this index to the operating system along with the requested operation.
A window manager is a resource manager that manages the lifecycle, appearance, size, position, and state of all user interface components on the desktop. For example, when the mouse button is clicked, the operating system might consult the window manager to determine which component was under the mouse cursor at the time of the click. Creating, destroying, hiding, moving, selecting, resizing, and repainting components can also be window manager jobs. The window manager gives a GUI its "look and feel". There are many well known window managers that are commonly used by the X Windows system: Motif window manager (mwm), OPEN LOOK window manager (olwm), and Tom's window manager (twm). X Windows programmers can even create their own window managers. The Macintosh window manager manages all open Macintosh windows. The NT object manager manages NT windows, as well as other resources such as files, processes, and threads.
View Handlers
A view handler is an application-level window manager that creates, manages, and destroys application views. The view handler also implements view commands. View commands are application-independent commands that are applied to one or more of an application's open views. For example, all MFC applications provide a Window menu containing the window commands: "New Window", "Cascade", "Tile", and "Arrange Icons". Operations such as tiling and cascading must be done by a view handler rather than views, because they require knowledge of the size and position of all open views. There is even a view handler design pattern:
View Handler
[POSA]Other Names
Window Manager, View Manager
Problem
Applications that allow multiple views often need to impose some uniformity on the appearance, attributes, and behavior of these views.
Certain meta operations such as tiling and cascading require the ability to query and manipulate all open views.
In some situations it may not make sense to have model-view-controller models be publishers.
Solution
Uniformity can be imposed by requiring all views to be derived from an abstract view base class. Introduce a component called a view handler that maintains a list of all open views. The view handler implements all view meta operations. The view handler can also notify views when they need to repaint themselves, thus models don't need to be publishers if they maintain a link to the view handler.
AFW 4.0: A GUI Application Framework with a View Handler
AFW 4.0 enhances AFW 3.0 by replacing the Publisher-Subscriber event notification mechanism with a centralized view handler.
Design
Implementation