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.
Messages or commands are 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(GC& gc = theGC) =
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.
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 defines a pure virtual message handling function which will be implemented by 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:
ActiveController(Model* m = 0)
{
theModel
= m;
}
virtual ~ActiveController() {}
void controlLoop();
void addView(View* v);
virtual Result handle(Message msg) = 0;
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;
Model* theModel
};
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[1]
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";
}
};
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";
}