AFW 1.0: An Application Framework

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.

A Few Simplifications

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.

Error Reporting

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) {}
};

Static Structure

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.

The AFW Model

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.

The AFW Controller

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);
}

Customizing AFW: Brick CAD 1.0

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.

Static Structure

The following diagram hides some of the framework classes and focuses instead on the customization classes:

Demonstration

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

Bricks

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.

The Constructor

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;
}

The BC Controller

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";
}

Brick Views

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";
   }
};

Programming Notes

Programming Note 6.1

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";
}

 



[1] ios::sync() doesn't work in DJGPP.