User interfaces are assemblies built out of presentation and control components. These components depend on lower level components such as graphical contexts, event notification mechanisms, and I/O streams. In this chapter we will introduce mechanisms, services, and design patterns that are important for user interfaces and application frameworks. We begin by introducing the Model-View-Controller pattern. Next, views and controllers are placed in the larger context of user interface components and graphics. Although we can't implement these classes without using non-standard, platform-dependent libraries, we can simulate them.
Next, after a brief introduction to Microsoft Foundation Classes (MFC), we define several versions of our own application framework, AFW, which really works. AFW provides architecture, a console user interface, persistence, help, exception handling, and undo/redo to its customizations. In addition to Model-View-Controller, AFW introduces several other patterns including View Handler, Command Processor, and Memento.
double cube(double num)
{
double result = num * num * num;
cout << "cube = "
<< result << endl; // 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.
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):
Controllers are objects that are able to respond to messages sent from the user. A message may be a command such as "add x y" or an event such as "mouse clicked". Of course all messages are instances of some message class:
struct Message
{
MsgType type; // e.g. QUIT, ADD,
MOUSE_MOVE
Controller* target;
string content;
// etc.
};
A controller provides a specific member function that expects a message as input. Objects pass messages to the controller by calling this function. The function may or may not return a result. In our examples we call this function handle(), and we provide an abstract base class for all controllers:
class Model; // forward reference
class Controller
{
public:
Controller(Model* m = 0) { theModel =
m; }
virtual ~Controller() {}
virtual Result handle(Message msg) = 0;
protected:
Model* theModel;
};
For now, results are simply strings:
typedef string Result; // for now
After determining the type of its message argument, the handle() function may respond to the message by executing it, by forwarding it to another controller, or by sending a new, "higher level" message to another controller.
For example, clicking a dialog box button labeled "OK" may send a "mouse clicked" message to a button controller. The controller may respond by sending an "OK button clicked" message to the controller associated with a control panel that contains the button (event routing) or to controllers that are specifically "listening" to this button (event delegation). Alternatively, the button controller may simply execute the "mouse clicked" message by performing the required actions (update the model, hide the control panel, etc.) all by itself.
Button controllers are passive. Like most objects, a passive controller does nothing unless another object calls its handle() function. By contrast, active controllers have control loops that perpetually "listen" for incoming messages. Clearly a single-threaded program can have at most one active controller. An active controller is the engine that drives the application.
Generally speaking, there are two types of active controllers. Applications with console user interfaces (CUIs) are driven by interpreters (also called shells or consoles) that perpetually read messages (i.e. user commands) from the keyboard:
while (more)
{
cout << prompt;
cin >> message;
if (message.type == QUIT)
more = false;
else
{
result = handle(message);
cout << result << endl;
}
}
Applications with graphical user interfaces (GUIs) are driven by message brokers (see Chapter 7) that perpetually extract messages from a message queue:
while (more)
{
message = msgQueue.dequeue(); // idles
if queue is empty
if (message.type == QUIT)
more = false;
else
handle(message);
}
Low level messages such as "mouse clicked" or "key pressed" are placed in the message queue by the operating system. High level messages such as "OK button clicked" or "File/Open menu item selected" are placed in the message queue by application-level objects. If the queue is empty, the broker enters a blocked state until a message arrives.
The message handler of an interpreter might use a table or a multi-way conditional to route the message to a more specialized handler:
switch(msg.type)
{
case (ADD)
handleAdd(msg);
break;
case (MUL)
handleMul(msg);
break;
// etc.
default: defaultHandler(msg);
}
A broker simply forwards the message to its target:
msg.target->handle(msg);
Messages (hence control) flows through an application like balls rolling through a pinball machine. The active controller perpetually fires off new messages. A message bounces from one passive controller to the next. Lights flash, buzzers buzz, and points accumulate. Occasionally a passive controller consumes its messages and replaces it with a new one. Eventually the message rolls down the drain and either the game is over, or it's time for the player to fire a new message.
A graphical user interface is an assembly of GUI components— controls (buttons, text and list boxes, menu items, etc.), views, and control panels (dialogs, menu bars, tool bars, etc.) —contained in an application window.
Application windows and control panels are GUI component containers. Thus, a GUI has a logical, tree-like structure in which the root node is the application window, parent nodes are control panels, child nodes are GUI components contained by their parents, and leaf nodes are views and controls:[1]
At the lowest level of abstraction, a user interface component is simply a rectangular region of the desktop. As such, it has a size (height and width), a position (location of its upper left corner), and resize() and move() member functions for changing these attributes. A virtual draw() member function draws the component in its associated region. The default implementation simply draws the component's border. The Button class re-implements this function to draw a picture of a button, the MenuItem class re-implements this function to draw a picture of a menu item, and the View class re-implements this function to draw a picture of the model.
The following declaration is only intended to simulate the important features of user interface components. In a real GUI our UI components would be handles that delegate to corresponding system-level user interface components (i.e., handle bodies):
class UIComponent: public Controller
{
public:
virtual Result handle(Message m);
virtual void draw(GC* gc = 0);
void move(const Point& p);
void resize(int h, int w);
// etc.
protected:
Point corner; // upper left corner
int height, width; // size
UIContainer* parent;
// UIComponentOID peer; // id of system
component
};
By deriving our UIComponent base class from the Controller class, we imbue user interface components with the ability to respond to user inputs such as mouse clicks or key presses. A default implementation of the handle() function means that derived classes can ignore this ability if they choose. Our default implementation simply delegates the message to its parent in the user interface component tree:
virtual Result UIComponent::handle(Message m)
{
if (!parent) throw
runtime_error("unhandled message");
return parent->handle(m);
}
When drawing a component on the desktop, we want to avoid introducing dependencies on a particular type of monitor or graphics subsystem into our code. This is achieved by defining an interface called a graphical context (GC) that decouples the graphics code in our application from device-dependent implementations:
class GC // interface
{
public:
virtual void display() = 0;
virtual void clear() = 0;
virtual void plot(const Point& x) =
0;
virtual void plot(const Line& x) =
0;
virtual void plot(const Text& x) =
0;
void plot(const Box& x);
void plot(const Polygon& x);
void plot(const Ellipse& x);
// etc.
protected:
Brush brush; // fill color &
texture
Pen pen; // line color & style
// etc.
};
The plot() functions plot various geometric objects onto a rectangular region of the desktop called a canvas. The canvas is equipped with a co-ordinate system. Typically, the origin of the co-ordinate system is the upper left corner of the canvas. The positive x-axis runs across the top of the canvas, the positive y-axis runs down the left side of the canvas, and units are pixels:
Points encapsulate the positive x any y co-ordinates of points on the canvas:
struct Point
{
int xc, yc;
// etc.
};
A line segment connects two points:
struct Line
{
Point start, end;
// etc.
};
A polygon is a vector of points representing vertices:
struct Polygon: public vector<Point>
{
Polygon(int numVerts, int radius, Point
center);
};
Boxes (i.e., rectangles) are such common polygons that they are often treated separately:
struct Box
{
Point corner; // upper left corner
int height, width;
// etc.
};
An ellipse is completely specified by its minimum bounding box:
struct Ellipse
{
Box box; // minimum bounding box
// etc.
};
A text object (i.e., a label) encapsulates a string, a font, and a starting position on the canvas:
struct Text
{
string text;
Point start;
Font font;
// etc.
};
Of course there are also objects representing fonts, brushes, pens, colors, and other drawing tools:
class Font;
class Brush;
class Pen;
class Color;
We separate the action of plotting geometric objects from the action of displaying the canvas, which is done by the graphical context's display() member function. The clear() member function erases all plotted objects.
When is the draw() function of a user interface component called? Who calls it? Where does its graphical context argument come from? When is the graphical context displayed?
Component containers provide a partial answer to some of these questions. A component container manages a list of child components. Its implementation of draw first draws the container's border, which is achieved by calling its inherited draw() function, then passes its graphical context to the draw() function of each child:
class UIContainer: public UIComponent
{
public:
void add(UIComponent* c)
{
children.push_back(c);
c->setParent(this);
}
void draw(GC* gc)
{
UIComponent::draw(gc); // draw
border
list<UIComponent*>::iterator p
= children.begin();
for( ; p != children.end(); p++)
(*p)->draw(gc);
}
// etc.
protected:
list<UIComponent*> children;
};
The root of the user interface component tree is a special type of component container called an application window. In our implementation the application window creates, plots, and displays the graphical context:
class AppWindow: public UIContainer
{
public:
void draw(GC* gc = 0)
{
GC* theGC = gc? gc:
theSystem->makeGC();
UIContainer::draw(theGC);
theGC->display();
}
};
In a real GUI AppWindow::draw() is called by the operating system each time it feels an application window and its contents need to be redrawn. (For example, when the application window is uncovered.) It is the job of the operating system to create a graphical context and pass it to AppWindow::draw(). To simulate this we assume the operating system is explicitly represented in our application by a global object called theSystem, which is equipped with a factory method called makeGC(). In the next chapter we will see that this idea is a great way to reduce the coupling between our applications and their host platforms.
Unlike other user interface components, views don't have pre-defined draw() functions. Instead, it is the responsibility of the application developer to implement the draw() function in classes derived from View. Typically, the implementation of draw() uses a pointer 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 2, the Publisher-Subscriber pattern comes to our rescue. If models are publishers, then views can be subscribers:
Recall that subscribers must provide an update() function, which will be called by the publisher's notify() function. In the case of a view, the update() function calls the draw() function implemented in various classes derived from View:
class View: public Subscriber, public UIComponent
{
public:
View(Model* m = 0)
{
theModel = m;
theModel->subscribe(this);
}
virtual ~View()
{
theModel->unsubscribe(this);
}
void update(Publisher *who, void* what =
0)
{
draw();
}
void setModel(Model* m)
{
if (theModel)
theModel->unsubscribe(this);
theModel = m;
theModel->subscribe(this);
}
protected:
Model* theModel;
};
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:
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.
};[2],[3]
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:
For the rest of the chapter we will be discussing several versions of our own application framework, called AFW. Although we appropriate terminology and architecture from graphical user interfaces, AFW is intended as an application framework for programs with console user interfaces. All versions of AFW provide customizable mechanisms for persistence, exception handling, and help; all versions handle application-independent commands such as "save", "saveAs", "open", "help", "quit", and "about"; and all versions are based on the Model-View-Controller architecture. Like MFC, the first version uses the Publisher-Subscriber pattern to solve the view notification problem.
AFW is intended as a framework for CUI-based applications. This allows us to make a number of simplifications in our earlier discussion. First, messages (more properly called "commands" by CUI-based applications) can simply be identified with strings:
typedef string Message;
Unfortunately, the standard C++ library doesn't include support for graphics and GUI components such as buttons, boxes, and menu items. We can keep some of our GUI terminology in AFW by simply identifying graphical contexts with output streams:
typedef ostream GC;
In particular, cout will be our principal graphical context:
#define theGC cout
We also make a few small changes to our UIComponent base class, passing graphical contexts by reference instead of by pointer, and using theGC as the default argument:
class UIComponent
{
public:
virtual void draw(ostream& gc =
cout) = 0;
};
Now the View class defined earlier can be used without changes.
AFW provides a special class of exceptions that can be thrown by framework customizations to report application-level errors:
class AFWError: public runtime_error
{
public:
AFWError(string gripe =
"unknown"): runtime_error(gripe) {}
};
AFW provides model, view, and controller base classes. Persistence and view notification are built into the framework. Customizations must provide model, view, and controller derived classes:
The customization's model-derived class encapsulates application-dependent data and logic. The controller-derived class provides a handler for application-dependent commands. View-derived classes (there can be many) provide different ways to display application-dependent data.
class View: public Subscriber
{
public:
View(Model* m = 0)
{
theModel = m;
theModel->subscribe(this);
}
virtual ~View()
{
theModel->unsubscribe(this);
}
void update(Publisher *who, void* what
= 0)
{
draw();
}
virtual void draw(ostream& gc =
cout) = 0;
void setModel(Model* m)
{
if (theModel)
theModel->unsubscribe(this);
theModel = m;
theModel->subscribe(this);
}
protected:
Model* theModel;
};
AFW's Model class multiply inherits from the Publisher and Persistent classes presented in chapters 2 and 5, respectively:
class Model: public Publisher, public Persistent
{
public:
Model(string fn = NO_NAME) { modified =
false; fname = fn; }
virtual ~Model() {}
bool getModified() { return modified; }
void setModified(bool flag = true) {
modified = flag; }
string getFileName() { return fname; }
void setFileName(string fn) { fname =
fn; }
virtual void about(); // re-define in
derived class
virtual void help(); // re-define in derived class
protected:
string fname; // save model here
bool modified; // unsaved changes?
};
Each model encapsulates the name of the file it uses for serialization and deserialization. If a file name has not yet been assigned to a model, then this variable contains a special string:
#define NO_NAME "***undefined***"
If a user attempts to save a model that has not yet been given a name, the framework will prompt him for one. Subsequent save operations can then be done quietly, without additional user input. Users can use the "saveAs" command to save a model into a different file.
Each model also encapsulates a special flag that is set to true if there have been changes to the application data since the last time serialization occurred. This flag is consulted by the "quit" and "open" message handlers. If it is true, then the user is prompted to save unsaved modifications. The responsibility of setting this flag falls on the customization's Model-derived class.
The model's virtual help() and about() functions simply print apologies for the lack of better application-dependent help and about services. These functions may be redefined by the customization's Model-derived class. Of course application-independent help and about service is built into the framework.
AFW's ActiveController class is derived from the Controller class defined earlier, although it doesn't define the pure virtual message handling function inherited from this class. This job belongs to the customization's ActiveController-derived class. The active controller does provide functions that handle the six application-independent commands: "save", "saveAs", "help", "about", "open", and "quit". In addition, the active controller provides re-definable functions for handling exceptions. The active controller also maintains a list of all open views. This is simply a housekeeping measure in AFW version 1.0. In subsequent versions we will make some use of this list.
class ActiveController: public Controller
{
public:
ActiveController(Model* m = 0):
Controller(m){}
virtual ~ActiveController() {}
void controlLoop();
void addView(View* v);
protected:
virtual void handleExcpt(AFWError e);
virtual void terminate(string s =
"unknown");
void save();
void saveChanges();
void saveAs();
void open();
void about();
void help();
list<View*> views;
};
The active controller's control loop drives the entire application. It displays a prompt, then waits for the user to enter a command (i.e., a message). The application-independent commands are handled by special member functions. Application-dependent commands are passed to the handle() function defined by the customization. All messages are handled inside a try block. A sequence of catch blocks first handles application-level errors using the re-definable handleExcpt() function. More serious errors are handled by the re-definable terminate() function.
void ActiveController::controlLoop()
{
bool more = true;
Message msg;
Result res;
while(more)
try
{
cout << "-> ";
cin.sync(); // flush cin's buffer[4]
cin >> msg;
if (msg == "quit")
{
saveChanges();
more = false;
cout <<
"bye\n";
}
else if (msg == "save")
save();
else if (msg ==
"saveAs") saveAs();
else if (msg == "open")
open();
else if (msg == "help")
help();
else if (msg ==
"about") about();
else // an application-dependent
command
{
res = handle(msg);
cout << res <<
endl;
}
}
catch(AFWError e) { handleExcpt(e);
}
catch(runtime_error e) {
terminate(e.what()); }
catch(...) { terminate(); }
}
The application-independent message handlers are straight forward and most are left as exercises. As an example, we present an implementation of the save() function, which is called in response to the "save" command.
The save() function does nothing if there is no model or if there are no unsaved modifications. If the model doesn't have an associated file name, then the user is prompted for one. Next, an object stream is created and associated with the file name. The model is serialized into the object stream, the modified flag is cleared, and a confirmation is displayed:
void ActiveController::save()
{
if (theModel &&
theModel->getModified())
{
string fn;
if (theModel->getFileName() ==
NO_NAME)
{
cout << "enter a file
name: ";
cin >> fn;
theModel->setFileName(fn);
}
ObjectStream os;
fn = theModel->getFileName();
os.open(fn.c_str(), ios::out);
if (!os) throw AFWError("can't
open file");
theModel->serialize(os);
os.close();
theModel->setModified(false);
cout << "save
done\n";
}
else
cout << "No data or data
already saved.\n";
}
AFW's Model class inherits pure virtual serialize(), deserialize(), and clone() functions from the Persistent base class, and although framework functions like save() freely call these functions, their definitions aren't provided by the framework. Obviously, these functions can only be defined in Model-derived classes that know what application-dependent data needs to be serialized, deserialied, and cloned.
The default error handling functions display an error message, then attempt to repair cin:
void ActiveController::handleExcpt(AFWError e)
{
cerr << "Error: "
<< e.what() << endl;
cin.clear(); // cin.state = good
cin.sync(); // flush cin's buffer
}
The terminate() function, called to handle more serious errors, prompts the user to save changes, then shuts down the application:
void ActiveController::terminate(string gripe)
{
cerr << "Error: "
<< gripe << endl;
cerr << "Application
quitting ... \n";
cin.clear();
cin.sync();
saveChanges();
exit(1);
}
CAD/CAM stands for "Computer Aided Design/Computer Aided Manufacturing". CAD/CAM systems are used by engineers to design everything from spark plugs to skyscrapers. In this context the object being designed is the model. Engineers can create and modify different types of views of the model, such as two dimensional cross section views, three dimensional wire frame views, three dimensional solid surface views, statistical views, schematics, blueprints, even views of individual model components.
Let's build a CAD/CAM system by customizing our application framework, AFW 1.0. Our application will be called Brick CAD, version 1.0, because it will be used to design bricks. Yes, this is a joke, no one would really use a CAD/CAM system to design bricks, but our goal is simply to demonstrate how the application framework is customized; we don't want to be distracted by the details of a complicated application domain such as designing jumbo jets. On the other hand, a CAD/CAM system for designing jumbo jets could be developed by customizing a framework that employed a design similar to AFW.
Bricks are pretty simple; their main attributes are height, width, length, weight, and volume. For now, we provide three types of views: a top view that shows the length and width of a brick, a side view that shows the width and height, and a front view that shows the height and length:
A special active controller will handle messages that allow users to alter the brick's height, width, and length.
The following diagram hides some of the framework classes and focuses instead on the customization classes:
We begin with a demonstration of Brick CAD. Our test harness creates a brick, a controller, and four views, then starts the control loop:
BCController* bc = new BCController(new Brick());
bc->addView(new TopView());
bc->addView(new TopView());
bc->addView(new SideView());
bc->addView(new FrontView());
bc->controlLoop();
After starting Brick CAD we use the "about" and "help" commands to learn more about the application:
-> about
AFW, version 1.0
Cyberdellic Designs presents: Brick CAD
-> help
General commands:
about: about this application
help: print this message
open: read application data from a file
quit: terminate application
save: save application data to current file
saveAs: save application data to new file
Application specific commands:
setHeight AMT: to set height
setWidth AMT:
to set width
setLength AMT: to set length
show: display all properties
Notice that both commands produce outputs divided into two sections: application-independent information provided by the framework, and application-dependent information provided by the customization.
We can use Brick CAD's "show" command to discover the default properties of our brick model:
-> show
height = 5 inches
width = 5 inches
length = 5 inches
volume = 125 inches^3
weight = 5 pounds
done
Next, we use the Brick CAD's "setHeight" command to alter the brick's height. Notice the view notification mechanism at work here. After changing the height, all four views automatically display themselves:
-> setHeight 20
*** TOP VIEW ***
width = 5 inches
length = 5 inches
*** TOP VIEW ***
width = 5 inches
length = 5 inches
*** SIDE VIEW ***
height = 20 inches
length = 5 inches
*** FRONT VIEW ***
height = 20 inches
width = 5 inches
done
When we quit the application, we are prompted to save our modifications:
-> quit
save modifications? n
bye
The Brick class is derived from AFW's Model class. Instances of the class encapsulate height, width, and length member variables, which can be initialized by constructor parameters. Additional weight and volume members can be computed from height, width, and length parameters using the private updateProps() member function:
class Brick: public Model
{
public:
Brick(int h = 5, int w = 5, int l = 5);
void setHeight(int h);
void setLength(int l);
void setWidth(int w);
int getHeight() const { return height;
}
int getWidth() const { return width; }
int getLength() const { return length;
}
float getWeight() const { return
weight; }
int getVolume() const { return volume;
}
void help();
void about();
Persistent* clone() { return new
Brick(*this); }
void serialize(ObjectStream& os);
void deserialize(ObjectStream&
ins);
private:
void updateProps();
int height, width, length; // inches
int volume; // inches^3
float weight; // lbs.
static Persistent* myPrototype;
};
Of course we can't forget to create a prototype brick and add it to the static prototype table associated with the Persistent base class:
Persistent* Brick::myPrototype =
Persistent::addPrototype(new Brick());
As always, make sure this statement is executed after the prototype table has been created.
If it has a non-positive parameter, the constructor throws an exception that will be caught by the framework. Otherwise, the height, width, and length are initialized, and updateProps() is called to compute the weight and volume:
Brick::Brick(int h, int w, int l)
{
if (h <= 0 || w <= 0 || l <=
0)
throw AFWError("dimensions must
be positive");
height = h;
width = w;
length = l;
updateProps();
}
The setDIM() functions are all similar. If the argument is non-positive, an exception is thrown to the framework. Otherwise, the dimension is changed, the volume and weight are updated, the inherited modified flag is set, and the registered views are notified:
void Brick::setHeight(int h)
{
if (h <= 0)
throw AFWError("dimension must
be positive");
height = h;
updateProps();
setModified(true);
notify();
}
We weren't able to implement serialize(), deserialize(), and clone() in the Model class, so now this must be done in the Brick class. Fortunately, the implementations are easy. The clone() function passes its dereferenced implicit parameter to the Brick copy constructor. The serialize() function inserts newline-separated height, width, and length into an object stream. We don't need to save volume and weight, because these can be recomputed. This saves a lot of file space if a model has many attributes that can be recomputed:
void Brick::serialize(ObjectStream& os)
{
os << height << endl;
os << width << endl;
os << length << endl;
}
Brick CAD's controller derives from AFW's ActiveController class. This class only needs to provide a handler for Brick CAD related commands:
class BCController: public ActiveController
{
public:
BCController(Model* m = 0):
ActiveController(m) {}
Result handle(Message msg);
};
The handler begins by down casting the model pointer inherited from the Model base class to a brick pointer. (Using the dynamic_cast<>() operator would provide an extra measure of safety, here.) This step is necessary because the compiler won't allow us to call brick member functions using a Model pointer.
In the message is "setHeight", the dimension is extracted from cin. A non-numeric input will break cin. We can test this by comparing cin to 0, the null pointer. If cin is broken, we throw an exception to the framework, where cin will be repaired. We don't need to test for non-positive inputs, because the brick's setHeight() function already does this.
Result BCController::handle(Message msg)
{
Brick* b = (Brick*)theModel;
double arg = 0;
if (msg == "setHeight")
{
cin >> arg;
if (!cin) throw
AFWError("amount must be a number");
b->setHeight(arg);
}
else if (msg == "setWidth")
// similar
else if (msg == "setLength")
// similar
else if (msg == "show")
// display b's properties
else
throw
AFWError(string("unrecognized command: ") + msg);
return "done";
}
The Brick CAD view classes are all derived from AFW's View class. Each simply provides an implementation of the draw() function that down casts the protected model pointer inherited from the View base class to a Brick pointer, then calls the brick's getDIM() functions to fetch only those dimensions that are relevant to the view's perspective:
class FrontView: public View
{
public:
FrontView(Brick* b = 0): View(b) {}
void draw(GC& gc = theGC)
{
Brick* b = (Brick*)theModel;
gc << "*** FRONT VIEW
***\n";
gc << "height = "
<< b->getHeight() << " inches\n";
gc << "width = "
<< b->getWidth() << " inches\n";
}
};
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
bool close(int i);
bool serviceA(int i);
bool serviceB(int i);
// etc.
private:
map<int, Resource*> open;
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 Manager::serviceA(int i)
{
if (!authorized(...)) return false; //
fail
open[i]->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:
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.
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.
Version 2.0 of our application framework replaces the Publisher-Subscriber pattern with the View Handler pattern. Models don't need to be publishers anymore. Instead, each model maintains a reference to the view handler. When the model changes state, it calls the view handler's notifyAllViews() function, which calls the draw() function of each open view.
Both the active controller and the model need pointers to the view handler. The model needs to know where the view handler is so that it can notify open views of changes in the application data. The active controller needs to know where the view handler is so that it can delegate view commands:
One of the features of AFW 2.0 is that views are created dynamically. The user types the "view" command followed by the type of view to be created:
-> view ViewTYPE
OID = 506
done
The controller forwards the command to the view handler, which creates the view, then returns the assigned OID to the controller. The controller displays the OID to the user for future reference. Of course dynamic instantiation requires the prototype pattern (see Chapter 5). In AFW 2.0 the responsibilities of the prototype pattern's Product base class are divided between views (the products) and the view handler (the factory).
View constructors are protected. This prevents unauthorized creation of views (only the view handler can create a view). Views are also equipped with a unique object identifier (OID), which will subsequently be used by clients to refer to them. As required by the prototype pattern, prototypes of View-derived class must know how to clone themselves, so we add a pure virtual clone() function to the View base class. We must also be able to query a prototype about its type, so we add a getType() function that uses RTTI in its implementation (see Chapter 5). Notice that in AFW 2.0 the View class no longer derives from the Subscriber class:
class View: public UIComponent
{
public:
virtual ~View() {}
virtual View* clone() const = 0;
void setModel(Model* m) { theModel = m;
}
string getType() const;
ViewOID getOID() const { return oid; }
void setOID(ViewOID h) {oid = h; }
protected:
Model* theModel;
ViewOID oid;
View(Model* m = 0, ViewOID h = -1);
};
View object identifiers (View OID's) are integers:
typedef int ViewOID;
The view handler is a singleton (see Chapter 3) that maintains a table (i.e., map) of all open views. A factory method called openView() is used to create new views, closeView() destroys an existing view, and notifyAllViews() forces each open view to draw itself:
class ViewHandler
{
public:
static ViewHandler* makeViewHandler()
{
if (!theViewHandler)
theViewHandler = new
ViewHandler();
return theViewHandler;
}
typedef map<string, View*>
ProtoTable;
static View* addPrototype(View* p);
void notifyAllViews(GC& gc =
theGC);
ViewOID openView(string type, Model* m
= 0);
void closeView(ViewOID h);
// etc.
private:
map<ViewOID, View*> openViews;
ViewHandler() {}
ViewHandler(const ViewHandler& vh)
{}
~ViewHandler() {}
static ProtoTable protoTable;
static ViewOID nextOID; // OID
generator
static ViewHandler* theViewHandler; //
the singleton
};
We can't forget to define the view handler's static members (this would normally be done in afw2.cpp):
ViewHandler* ViewHandler::theViewHandler = 0;
ViewHandler::ProtoTable ViewHandler::protoTable;
ViewOID ViewHandler::nextOID = 500;
The view notifier uses an iterator to traverse the table of open views, passing a graphical context to each view's draw() function:
void ViewHandler::notifyAllViews(GC& gc)
{
map<ViewOID, View*>::iterator p;
for(p = openViews.begin(); p !=
openViews.end(); p++)
((*p).second)->draw(gc);
}
The openView() function is the view handler's factory method. Given the type of view to be opened (a string), it searches the prototype table, generates a new OID, clones the prototype, adds the clone to the table of open views, then sets the view's model and OID:
ViewOID ViewHandler::openView(string type, Model* m)
{
View *proto, *v = 0;
if (!find(type, proto, protoTable))
throw AFWError("View type
unknown");
ViewOID oid = nextOID++;
v = proto->clone();
openViews[oid] = v;
v->setModel(m);
v->setOID(oid);
cout << "OID = "
<< oid << endl;
return oid;
}
The Model class in AFW 2.0 is almost identical to the Model class in AFW 1.0. We drop the Publisher base class and we add a pointer to the view handler:
class Model: public Persistent
{
public:
void setViewHandler(ViewHandler* vh) {
theViewHandler = vh; }
void notify()
{
if (theViewHandler)
theViewHandler->notifyAllViews();
}
// etc.
protected:
ViewHandler* theViewHandler;
// etc.
};
The ActiveController class declaration in AFW 2.0 is virtually identical to the corresponding declaration in AFW 1.0. We only need to add a pointer to the view handler:
class ActiveController: public Controller
{
ViewHandler* theViewHandler;
// etc.
};
The ActiveController constructor creates the singleton view handler:
ActiveController::ActiveController(Model* m)
: Controller(m)
{
theViewHandler =
ViewHandler::makeViewHandler();
if (theModel)
theModel->setViewHandler(theViewHandler);
}
We can now handle additional application-independent view commands in the active controller's control loop:
// view commands:
else if (msg == "tile") theViewHandler->tileViews();
else if (msg == "cascade") theViewHandler->cascadeViews();
else if (msg == "view")
{
cin >> arg; // read view type
theViewHandler->openView(arg,
theModel);
cout << "done\n";
}
else if (msg == "closeView")
{
cin >> oid; // read view OID
theViewHandler->closeView(oid);
cout << "done\n";
}
// etc.
Version 2.0 of Brick CAD, our CAD/CAM system for designing bricks, is almost identical to version 1.0, except it customizes AFW 2.0 instead of AFW 1.0.
Our Brick CAD test harness no longer needs to statically create views:
BCController* bc = new BCController(new Brick());
bc->controlLoop();
We start Brick CAD and modify the model. Notice the lack of feedback. This is because no views have been created:
-> setHeight 10
done
Next, we use the "view" command to create a couple of views:
-> view TopView
OID = 500
done
-> view SideView
OID = 501
done
Now, modifying the model causes the newly created views to draw themselves:
-> setHeight 12
*** TOP VIEW ***
width = 5 inches
length = 5 inches
*** SIDE VIEW ***
height = 12 inches
length = 5 inches
done
We can use the "closeView" command to delete a view. We need to supply the view's OID as an argument:
-> closeView 500
deleting view #500
done
We can prove the view is gone by modifying the model once again:
-> setHeight 15
*** SIDE VIEW ***
height = 15 inches
length = 5 inches
done
No changes need to be made from version 1.0 in the definitions of Brick and BCController:
class Brick: public Model { /* as before */ };
class BCController: public ActiveController { /* as before */ };
Only slight changes need to be made to the View-derived classes. In particular, we need to add the machinery required by the Prototype pattern: a clone() function and a prototype holder:
class FrontView: public View
{
public:
FrontView(Brick* b = 0): View(b) {}
void draw(GC& gc);
View* clone() const { return new
FrontView(*this); }
private:
static View* myPrototype;
};
Of course we must also create prototype views:
View* FrontView::myPrototype =
ViewHandler::addPrototype(new
FrontView());
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.
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:
Version 3.0 of our application framework enhances version 2.0 with a command processor and a command base class. The new framework will now be able to handle "undo" and "redo" commands. Instead of depending on a derived class to implement a message handler, the new active controller depends on a derived class to implement a virtual factory method for creating commands.
Each command is equipped with a pointer to the model. The active controller passes the commands it creates to the command processor, which maintains two stacks of command pointers: the undo stack, and the redo stack.
All application-dependent commands are derived from AFW's command base class, which contains a pointer to the model, a pure virtual execute() function, and a virtual undo() function that must be redefined in derived classes that represent undoable commands:
class Command
{
public:
Command(const string& nm =
"?", Model* m = 0)
{
theModel = m;
name = nm;
undoable = true;
}
virtual ~Command() {}
string getName() { return name; }
bool getUndoable() { return undoable; }
virtual void execute() = 0;
virtual void undo() {}; // override if
undoable
protected:
Model* theModel;
string name;
bool undoable; // can clear in derived
classes
};
Also notice that each command encapsulates a flag indicating if it is undoable (commands like "print" and "quit" are not undoable) and a name. The name will be used to remind users which command they are undoing or redoing.
Command processors are singletons (see Chapter 3). A command processor has two command stacks. One holds pointers to commands that can be undone, the other to commands that can be redone:
class CommandProcessor
{
public:
static CommandProcessor*
makeCommandProcessor()
{
if (!theCommandProcessor)
theCommandProcessor = new
CommandProcessor();
return theCommandProcessor;
}
void execute(Command* cmmd);
Result undo();
Result redo();
private:
static CommandProcessor*
theCommandProcessor;
CommandProcessor() {}
CommandProcessor(const
CommandProcessor& cp) {}
~CommandProcessor() {}
stack<Command*> undoStack,
redoStack;
};
Don't forget to define the static pointer to the one and only command processor:
CommandProcessor* CommandProcessor::theCommandProcessor = 0;
If commands are smart, then the command processor's execute() function is simple. It asks its command argument to execute itself, then, if the command is undoable, it pushes it onto the undo stack:
void CommandProcessor::execute(Command* cmmd)
{
cmmd->execute();
if (cmmd->getUndoable())
undoStack.push(cmmd);
}
The command processor's undo() function removes the top command from the undo stack (commands must be undone in the reverse order in which they were executed), calls its undo() function, then pushes it onto the redo stack:
Result CommandProcessor::undo()
{
if (undoStack.empty())
throw AFWError("Nothing left to
undo.");
Command* cmmd = undoStack.top();
undoStack.pop();
cmmd->undo();
redoStack.push(cmmd);
return cmmd->getName();
}
The command processor's redo() function is almost the mirror image of the undo() function, except redoing a command simply means calling its execute() function again:
string CommandProcessor::redo()
{
if (redoStack.empty())
throw AFWError("Nothing left to
redo.");
Command* cmmd = redoStack.top();
redoStack.pop();
cmmd->execute();
undoStack.push(cmmd);
return cmmd->getName();
}
The active controller in AFW 3.0 is constructed from the AFW 2.0 active controller by adding a pointer to the command processor, a virtual factory method for creating commands, and an implementation of the pure virtual handle() function inherited from the Controller base class:
class ActiveController: public Controller
{
public:
ActiveController(Model* m = 0);
void controlLoop();
protected:
virtual Command* makeCommand(Message
msg) = 0;
CommandProcessor* theCommandProcessor;
Result handle(Message msg);
// etc.
};
The controller's constructor creates the view handler and command processor singletons:
ActiveController::ActiveController(Model* m)
: Controller(m)
{
theViewHandler =
ViewHandler::makeViewHandler();
if (theModel)
theModel->setViewHandler(theViewHandler);
theCommandProcessor =
CommandProcessor::makeCommandProcessor();
}
The handle() function no longer needs to be implemented in framework customizations (the virtual factory method will need to be implemented instead). The handle() function uses the virtual factory method to convert the message parameter into a command object, which is then passed to the command processor's execute() function:
Result ActiveController::handle(Message msg)
{
Command *cmmd = makeCommand(msg);
theCommandProcessor->execute(cmmd);
return "done";
}
We can now add clauses for handling "undo" and "redo" commands to the active controller's control loop:
else if (msg == "undo")
{
res = theCommandProcessor->undo();
cout << res << "
undone\n";
}
else if (msg == "redo")
{
res = theCommandProcessor->redo();
cout << res << "
redone\n";
}
// etc.
Version 3.0 of Brick CAD, our CAD/CAM system for designing bricks, customizes AFW 3.0 instead of AFW 2.0. Fortunately, version 3.0 can re-use the version 2.0 Model and View derived classes:
class Brick: public Model { /* as before */ };
class FrontView: public View { /* as before */ };
class SideView: public View { /* as before */ };
class TopView: public View { /* as before */ };
We will also need to derive a class from the ActiveController class (this will be different from the 2.0 version). We will also need to create command-derived classes for each application-dependent command:
Initially, Brick CAD 3.0 edits a brick with default dimensions:
-> show
height = 5 inches
width = 5 inches
length = 5 inches
volume = 125 inches^3
weight = 5 pounds
done
We create top and side views, which are notified when we subsequently alter the brick's length and height:
-> setLength 20
*** TOP VIEW ***
width = 5 inches
length = 20 inches
*** SIDE VIEW ***
height = 5 inches
length = 20 inches
done
-> setHeight 15
*** TOP VIEW ***
width = 5 inches
length = 20 inches
*** SIDE VIEW ***
height = 15 inches
length = 20 inches
done
Let's display the model's properties once again:
-> show
height = 15 inches
width = 5 inches
length = 20 inches
volume = 1500 inches^3
weight = 60 pounds
done
Notice that the "undo" command undoes the "setHeight" command, not the previous, undoable "show" command:
-> undo
*** TOP VIEW ***
width = 5 inches
length = 20 inches
*** SIDE VIEW ***
height = 5 inches
length = 20 inches
setHeight undone
Typing "undo" a second time undoes the original "setLength" command:
-> undo
*** TOP VIEW ***
width = 5 inches
length = 5 inches
*** SIDE VIEW ***
height = 5 inches
length = 5 inches
setLength undone
But typing "undo" a third time produces an error message:
-> undo
Error: Nothing left to undo.
Next, we "redo" the previously undone "setLength" command:
-> redo
*** TOP VIEW ***
width = 5 inches
length = 20 inches
*** SIDE VIEW ***
height = 5 inches
length = 20 inches
setLength redone
-> redo
*** TOP VIEW ***
width = 5 inches
length = 20 inches
*** SIDE VIEW ***
height = 15 inches
length = 20 inches
setHeight redone
-> redo
Error: Nothing left to redo.
The controller will convert the "setHeight AMT" message into an instance of the following command-derived class:
class SetHeightCommand: public Command
{
public:
SetHeightCommand(Brick* b = 0, int amt
= 5)
: Command("setHeight", b)
{
if (amt <= 0) throw
AFWError("amount must be positive");
newHeight = amt;
}
void execute();
void undo();
private:
int newHeight, oldHeight;
};
The Command Processor pattern decouples command creation time from command execution time, therefore "setHeight" commands need to retain the new height as a member variable. When a "setHeight" command is executed, the old height is recorded in a second member variable before the height of the model is modified:
void SetHeightCommand::execute()
{
Brick* b = (Brick*) theModel;
oldHeight = b->getHeight();
b->setHeight(newHeight);
}
The undo() function simply changes the height of the brick back to the retained old height:
void SetHeightCommand::undo()
{
Brick* b = (Brick*) theModel;
b->setHeight(oldHeight);
}
The "setLength" and "setWidth" commands follow the same pattern. By contrast, the controller converts "show" messages into instances of the ShowCommand class:
class ShowCommand: public Command
{
public:
ShowCommand(Brick* b = 0)
: Command("show", b)
{
undoable = false;
}
void execute();
};
Notice that the constructor clears the protected undoable flag inherited from the command base class. This is because "show" commands can't be undone. Of course we don't need to implement an undo() function in this case, but we still need to provide an appropriate execute() function.
The Brick CAD controller must provide an implementation of the pure virtual factory method inherited from the AFW 3.0 ActiveController base class:
class BCController: public ActiveController
{
public:
BCController(Model* m = 0):
ActiveController(m) {}
Command* makeCommand(Message msg);
};
The factory method implementation closely parallels the BCController::handle() function defined in Brick CAD 2.0. The type of the input message is determined, but instead of performing the prescribed action, a corresponding command object is created and returned to the caller:
Command* BCController::makeCommand(Message msg)
{
Brick* b = (Brick*)theModel;
Command* cmmd = 0;
double arg = 0;
if (msg == "setHeight")
{
cin >> arg;
if (!cin) throw
AFWError("amount must be a number");
cmmd = new SetHeightCommand(b, arg);
}
else if (msg == "setWidth") {
... }
else if (msg == "setLength")
{ ... }
else if (msg == "show") cmmd
= new ShowCommand(b);
else throw AFWError(string("unrecognized
command: ") + msg);
return cmmd;
}
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.
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:
Version 3.1 of AFW adds mementos to AFW 3.0. This is only a minor improvement, so bumping the version number to 4.0 might be an over doing it.
The AFW 3.1 command base class is similar to the AFW 3.0 counterpart, except undo() is no longer a virtual function that must be redefined in the many derived classes:
class Command
{
public:
virtual void execute() = 0;
void undo();
// etc.
protected:
Memento* myMemento;
// etc.
};
AFW 3.1 commands are caretakers for mementos, which are instances of classes derived from a polymorphic base class in the framework:
class Memento
{
public:
virtual ~Memento() {}
};
Although execute() is a pure virtual function that must be redefined in derived classes, we can provide a partial implementation that deletes the old memento, then asks the model to create a new one:
void Command::execute()
{
if (myMemento) delete myMemento;
myMemento = theModel->makeMemento();
}
The undo() function asks the model to create a new memento. This will be used if the command needs to be redone. Next, the model is asked to restore its former state using the memento encapsulated by the command:
void Command::undo()
{
if (myMemento && undoable)
{
Memento* m =
theModel->makeMemento();
theModel->restoreState(myMemento);
delete myMemento;
myMemento = m;
}
}
AFW 3.1 models are similar to AFW 3.0 models, except for a virtual factory method for creating mementos and a virtual function that uses a memento to restore the model to its former state. Both of these functions will need to be redefined in a derived class:
class Model: public Persistent
{
public:
virtual void restoreState(Memento* m)
{};
virtual Memento* makeMemento() { return
0; };
// etc.
};
Predictably, Brick CAD 3.1 customizes AFW 3.1. Much of the implementation is identical to Brick CAD 3.0. Of course we need to derive a class from Memento that encapsulates the non re-computable attributes of a brick. Since weight and volume can be re-computed from height, width, and length, we can get away with just storing these three attributes:
class BrickMemento: public Memento
{
friend class Brick;
BrickMemento(int h = 0, int w = 0, int
l = 0)
{
oldHeight = h;
oldWidth = w;
oldLength = l;
}
int oldHeight, oldWidth, oldLength;
};
Notice that all of the BrickMemento members are private. Only bricks will be able to access this information by virtue of the friend declaration. This preserves encapsulation of model data.
AFW 3.1 Model-derived classes must provide implementations of the makeMemento() factory method and restoreState(). The Brick implementation of makeMemento() simply makes a new brick memento using the current
class Brick: public Model
{
public:
void restoreState(Memento* m);
Memento* makeMemento()
{
return new BrickMemento(height,
width, length);
}
// etc.
};
The restoreState() function downcasts its memento argument to a brick memento, copies the encapsulates height, width, and length to its member variables, then re-computes volume and weight using updateProps():
void Brick::restoreState(Memento* m)
{
BrickMemento* bm = (BrickMemento*)m;
height = bm->oldHeight;
width = bm->oldWidth;
length = bm->oldLength;
updateProps();
modified = true;
notify();
}
AFW 3.1 command-derived classes don't need to declare or implement an undo() function:
class SetHeightCommand: public Command
{
public:
SetHeightCommand(Brick* b = 0, int h =
5); // as before
void execute();
// void undo(); // no longer neeeded!
private:
int oldHeight, newHeight;
};
The execute() functions need to call the partially implemented execute() function defined in the Command base class. Recall that this function instructs the model to create a momento:
void SetHeightCommand::execute()
{
Command::execute(); // make a memento
Brick* b = (Brick*) theModel;
oldHeight = b->getHeight();
b->setHeight(newHeight);
}
This note contains implementations of some of the helper functions called by the active controller's control loop. saveChanges() is called each time we want to prompt the user to save modifications made to the model since the last save. Notice that saveChanges() calls save():
void ActiveController::saveChanges()
{
if (theModel &&
theModel->getModified())
{
string response;
cout << "save
modifications? ";
cin >> response;
cin.sync();
if (response == "y") save();
}
}
The saveAs() function changes the model's associated file name, then calls save():
void ActiveController::saveAs()
{
if (theModel)
{
saveChanges();
theModel->setFileName(NO_NAME);
theModel->setModified(true);
save();
}
}
The open() function calls saveChanges(), asks for a file name, then deserializes the indicated file into the model:
void ActiveController::open()
{
string fn;
saveChanges();
cout << "enter a file name:
";
cin >> fn;
cin.sync();
ObjectStream os;
os.open(fn.c_str(), ios::in);
if (!os) throw AFWError("can't
open file");
theModel->deserialize(os);
os.close();
theModel->setFileName(fn);
theModel->setModified(false);
cout << "open done\n";
}
This note explains how buttons can be simulated. A button encapsulates a label. The constructor constructs rectangular component with the specified corner and large enough to contain the label:
class Button: public UIControl
{
public:
Button(Point p, string lab =
"Button")
: UIControl(p, 2, lab.length() + 1)
{
label = lab;
}
Result handle(Message msg);
void draw(GC* gc);
private:
string label;
};
The button handler converts a "mouse clicked" message into a "button clicked" message:
Result Button::handle(Message msg)
{
Result res;
if (parent)
if (msg == "mouseClicked")
res =
parent->handle(Message(label + "ButtonClicked"));
else
res
= parent->handle(msg);
else
res = Controller::handle(msg);
return res;
}
The draw() function displays the button's label, then draws a border around it:
void Button::draw(GC* gc)
{
gc->plot(Text(label, Point(corner.xc
+ 1, corner.yc + 1)));
UIComponent::draw(gc); // draw border
}
Provide an implementation of the graphical context interface (GC), that plots geometric objects in a two dimensional array of characters:
class GridGC: public GC
{
public:
void plot(const Point& x)
{
// remember: (xc, yc) = grid[yc][xc]
grid[x.yc % rowSize][x.xc % colSize]
= pen;
}
// etc.
private:
int rowSize, colSize;
char** grid;
// etc.
};
You'll need to use Orthodox Canonical Form (see Chapter 4). For now you can use the following definitions:
typedef char Brush;
typedef char Pen;
The default setting for pen should be '.' or '*'. The default setting for brush should be ' ' or '/'.
Finish the implementations of UIComponent, UIContainer, AppWindow, UIControl, and Button classes using the GridGC implementation from the previous problem. Test your implementation using the following test harness:
int row, col;
cout << "enter row & col dimension:";
cin >> row >> col;
AppWindow aw(row - 3, col - 3);
Button b1(Point(3, 3), "OK"), b2(Point(3, 9), "QUIT");
aw.add(&b1);
aw.add(&b2);
GC* gc = new GridGC(row, col);
aw.draw(gc);
b1.handle("mouseClicked");
b2.handle("mouseClicked");
b2.handle("mouseRoared");
Implement and test the polygon constructor declared earlier:
struct Polygon: public vector<Point>
{
Polygon(int numVerts, int radius, Point
center);
};
Hint: the coordinates of the ith vertex of an n-sided polygon of radius r centered at (x, y) are:
(x + r * cos(i * 2 * pi/n), y + r * sin(i * 2 * pi/n))
Following the example done earlier, develop a multi-document MFC application that displays a season (fall, winter, spring, summer). Each time the user clicks the left mouse button in the view area, the season should change. Hint: you'll need to use the class wizard to create a handler in your CView-derived class for the WM_LBUTTONDOWN message.
Complete the implementation of version 1.0 of AFW. Test your implementation by completing the implementation of Brick CAD version 1.0. You should also test the persistence mechanism to verify that models can be saved to and restored from files. What do these files look like when viewed using an ordinary text editor?
Complete the implementation of version 2.0 of AFW. Test your implementation by completing the implementation of Brick CAD version 2.0. You should also test the persistence mechanism to verify that models can be saved to and restored from files. What do these files look like when viewed using an ordinary text editor?
Complete the implementation of version 3.0 of AFW. Test your implementation by completing the implementation of Brick CAD version 3.0. You should also test the persistence mechanism to verify that models can be saved to and restored from files. What do these files look like when viewed using an ordinary text editor?
Complete the implementation of version 3.1 of AFW. Test your implementation by completing the implementation of Brick CAD version 3.1. You should also test the persistence mechanism to verify that models can be saved to and restored from files. What do these files look like when viewed using an ordinary text editor?
It's too bad that all Brick CAD views are notified each time a brick is modified. For example, why should a front view redraw itself just because the length of its associated brick has been changed? Front views don't even display the length. This isn't a big problem for Brick CAD, but it might introduce major inefficiencies into Jumbo Jet CAD, where views might have time consuming draw() functions. Describe in detail how you could solve this problem without modifying AFW.
Suppose a framework customization defines several derived classes of AFWError. How could handleExcpt() be re-defined to differentiate between these types of exceptions?
ATM (Automatic Teller Machine) allows users to deposit money into and withdraw money from a single bank account. Here's a sample test of ATM with three identical Account views:
-> about
AFW, version x.0
Cyberdellic Designs presents: ATM on a PC
-> help
General commands:
...
Application specific commands:
withdraw AMT: to withdraw money
deposit AMT: to deposit money
balance: to see balance
-> balance
$100
-> deposit 30
balance = $130
balance = $130
balance = $130
done
-> quit
save modifications? n
bye
Create ATM by customizing any version of AFW. Thoroughly test ATM. Draw a class diagram showing the relationships between ATM classes and AFW classes.
Calc is a stack calculator that allows users to perform "push" and "pop" operations on a stack. In addition, "add", "mul", "div", and "sub" commands allow the user to replace the top two numbers on a stack by their sum, product, quotient, or difference, respectively. Here is a test run with one stack view:
-> about
AFW, version x.0
Cyberdellic Designs presents: Stack Calculator
-> help
General commands:
...
Application specific commands:
push AMT: push AMT onto stack
pop: remove top element
add: replace top 2 by sum
mul: replace top 2 by product
div: replace top 2 by quotient
sub: replace top 2 by difference
-> push -3.14
<-3.14>
done
-> add
Error: not enough numbers to add
-> push 7
<-3.14 7>
done
-> push 6
<-3.14 7 6>
done
-> mul
<-3.14 42>
done
-> pop
<-3.14>
done
-> quit
save modifications? n
bye
Implement and test the stack calculator by customizing any version of AFW. Draw a class diagram showing the relationship between stack calculator classes and AFW classes.
Hint: Create a NumStack class that multiply inherits from AFW's Model class and from STL's stack<> template:
typedef double Number;
class NumStack: public Model, public stack<Number>
{
public:
deque<Number> getContainer()
const { return c; }
void add();
void sub();
void mul();
void div();
// etc.
};
NumStack provides the arithmetic manipulations of the stack. All of these should throw AFW errors if there aren't at least two items on the stack. In this case the stack should be left in its original state.
Recall that STL stacks and queues are adapters for STL deques (see Chapter 4). Stack view classes may want to access the STL deque adaptee using getContainer().
A minor enhancement of AFW is to add a protected flag called enableParse to the ActiveController class. The default value of this flag is false, but it may be set to true by derived classes. The flag is used by the control loop to determine if the entire message should be extracted from cin using getline(), or if just the first token should be extracted using the extraction operator (a token is a string containing no white space characters):
if (enableParse)
getline(cin, msg); // broken in VC++
else
cin >> msg;
Implementations of the handle() function in parser-enabled customizations may need to convert their message arguments into input string streams so tokens can be easily extracted and parsed.
Make this modification to any version of AFW. Prove that it works by customizing AFW into an expression evaluator program that is able to evaluate simple arithmetic expressions derived from the EXP symbol using the following EBNF grammar:
EXP ::= NUM + EXP | PROD
PROD ::= NUM * PROD | (EXP) | NUM
NUM ::= any floating point number
Simplification: An easier version of this problem disallows nested expressions:
EXP ::= NUM + NUM | NUM * NUM
Create an Application Wizard for AFW (any version) customizations. Your wizard should prompt the user for the name of the Model-derived class, XXX, then generate corresponding xxx.h and xxx.cpp files. The xxx.cpp file should compile, link with AFW, and run without modification, although it won't actually do anything until the programmer completes the definitions in xxx.h and xxx.cpp. Your App Wizard should generate the most completed application possible without knowing anything more than the name.
AFW 2.0 and AFW 3.x use the Prototype pattern to allow users to dynamically create new views. AFW 4.0 takes this idea to its extreme by using the Prototype pattern to also allow users to dynamically create new models:
-> new ModelType
done
Instead of static customization, AFW 4.0 is dynamically customizable!
After we start AFW 4.0, we dynamically create an account model:
-> new Account
done
The "help" and "about" commands tell us about AFW 4.0 and about the ATM application. We make a few deposits and withdrawals, undo and redo a few commands, then save the account model to a file called acct1:
-> deposit 50
deposit done
-> deposit 20
deposit done
-> show
balance = $70
-> save
Enter output file name: acct1
Account Manager model saved
To customize AFW 4.0 to Brick CAD, we simply ask for a new brick model. Notice that we are prompted to save the last change to the account model:
-> new Brick
Save old model? (y/n) -> n
done
The "help" and "about" commands magically change their behavior to tell us about Brick CAD. We make a few modifications to our brick model:
-> setLength 25
length done
-> setWidth 66
width done
-> show
height = 10
width = 66
length = 25
Now let's change AFW 4.0 into a stack calculator:
-> new Stack
Save old model? (y/n) -> n
done
We do a few calculations:
-> push 43
push done
-> push 27
push done
-> push 19
push done
-> add
add done
-> show
< 46 43 >
-> pop
pop done
We push a few more numbers onto the stack, then save it:
-> push 53
push done
-> push 100
push done
-> save stk1
Stack Calculator model saved
-> push 67
push done
-> show
< 67 100 53 43 >
-> save
Stack Calculator model saved
Now let's reopen the file acct1. The framework immediately becomes the ATM application again:
-> open acct1
done
-> show
balance = $70
-> deposit 66
deposit done
Opening stk1 causes the interpreter to revert back to the stack calculator:
-> open stk1
Save old model? (y/n) -> n
done
-> show
< 67 100 53 43 >
Of course AFW 4.0 needs access to the Model, View, and Command derived classes for Brick CAD, Stack Calculator, and ATM. In other words, the relevant object files (bc.obj, calc.obj, atm.obj) must be linked with AFW 4.0. (In standard C++ this linking occurs statically, but some platforms (e.g., Windows and Java) allow dynamic linking.)
Implement and thoroughly test AFW 4.0.
[1] The term "control panel" can be misleading because some control panels may contain components other than controls.
[2] We are following Microsoft's "Hungarian notation" for naming member variables, parameters, classes, etc.
[3] CString is MFC's pre-STL string class.
[4] ios::sync() doesn't work in DJGPP.