AFW 3.0

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.

Static Structure

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.

AFW Commands

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.

The AFW Command Processor

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

AFW 3.0 Controller

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.

Customizing AFW: Brick CAD 3.0

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:

Demonstration

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

Issuing a second "redo" command redoes the "setHeight" command:

-> redo
*** TOP VIEW ***
width = 5 inches
length = 20 inches
*** SIDE VIEW ***
height = 15 inches
length = 20 inches
setHeight redone

Issuing the "redo" command a third time generates an error message:

-> redo
Error: Nothing left to redo.

Brick CAD Commands

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

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