Indirect cooperation

Instructor's Guide


intro, components, case study, crossing boundaries, styles, platform, summary, Q/A, literature
The standard way in which to control computation is to call a function or create an object and invoke a member function. However, in many situations such a model of direct control does not suffice. For example, in applications running in a window environment, control is delegated to the environment and user-defined classes must provide (callback) member functions that will be invoked whenever the window environment deems it appropriate. Callback functions are functions that are determined by convention or explicit declaration to be invoked at some point in the computation. In the following, we will study indirect control via callback by looking at a number of skeleton fragments illustrating how control may be effected in a window environment. The first two examples are inspired by the InterViews C++ graphical interface library (see Linton {\it et al.}, 1989). The third example gives a simplified rendering of the Model-View-Control paradigm in C++. The MVC paradigm was originally introduced in Smalltalk-80 to facilitate the development of graphical user interfaces.

Callbacks -- resource management

Our first example illustrates the use of callbacks to determine (dynamically) the resources a widget or gadget needs to be placed in a window. (The code below depicts in an abstract way how gadget resource allocation is handled by the InterViews library.) For those not familiar with window-based applications, a more comprehensive treatment of graphical user interface programming is given in section hush. A gadget is an item (such as a button) that may figure in a graphical user interface. A gadget is displayed on the screen by some window object. An abstract simplified definition of a gadget may look as shown in slide 7-c-gadget.

Gadgets -- graphical interface items

  class gadget { 
gadget
public: virtual void request(window* w)=0; virtual void draw()=0; };

slide: A gadget class

A gadget (in our example) has only two member functions: the function request (to notify the window of its requirements, the space it needs to display itself), and the function draw (that is called to display the gadget on the screen). For convenience, we associate a single gadget with a single window, although, as we will see in our next example, a window may contain an arbitrary number of gadgets. The definition of a (simplified) window class is given in slide 7-g-window.

Window -- to display gadgets on the screen

  class window { 
window
gadget* g; public: window(gadget* p) : g(p) { g->request(this); } void allocate(int x1, int y1, int x2, int y2) { ... } void damage() { g->draw(); } };

slide: A window class

The class window provides a constructor (taking a pointer to a gadget as an argument) and the function damage. By convention, gadgets are never directly drawn. When one or more gadgets needs to be (re)drawn this must be done by damaging the window containing the gadget. When the window constructor is evaluated, the gadget is requested (in an abstract fashion) to state its needs by invoking gadget::request. In its turn, the gadget will allocate some portion of the screen layout by invoking widget::allocate. As an example of a gadget, look at the definition of a button in slide 7-g-button.

Items

-- a button
  class button : public gadget { 
item
public: button( int x, int y ) : _x(x), _y(y) {} void request(window* w) { _w = w; w->allocate(_x , _y, _x + 20, _y + 15); } void draw() { w->rectangle(_x , _y, _x + 20, _y + 15); } private: int _x; int _y; window* _w; };

slide: A button class

Naturally, both the request and draw function of an actual gadget will in reality be somewhat more complex. However, the fragment below sufficiently illustrates how the button object is requested to state its requirements and to draw itself, in that order:
  gadget* b = new button(100,200); window* w = new window(b); w->damage();
  
The idea of callbacks is that an object (in this case a gadget) defines the functions that the controller object (in this case the window object) needs to get things right, that is displaying a gadget to the screen. In the following, we will extend this example to include more of the intricate details involved in handling windows and gadgets.

Window management {\em -- events}

Applications running in a window environment (such as X-windows or MS-windows) may in general be characterized as having an event-based control structure. The reason for this is that most languages do not provide multi-threading. Otherwise, it would be quite natural to have a thread per window or gadget. Conceptually, event-based control amounts to having a main loop that checks for incoming events and dispatches control according to the type of the event. However, many window environments (for example, the X-window system) also provide facilities to associate one or more callback functions with a particular event for a particular window. Moreover, an object-oriented architecture (such as the InterViews library) allows us to define these callbacks by means of virtual functions belonging to a gadget (instead of a window), as illustrated below. The example given here represents, in an abstract fashion, how a particular event handler (callback) function is selected in response to an event, mimicking features of the InterViews library.

Event

-- messages from the environment
  class event {  
event
int _info; public: event(int i) : _info(i) { } int info() { return _info; } };

slide: An event class

We start by defining an abstract notion of event, as given by the class in slide 7-w-event. In general, an event is associated with some information that says why (and possibly where) the event occurred. For our example, it suffices to incorporate an (integer) info field and a method to extract this information. Next, we define the class handler. The purpose of a handler object (which is created by a window in response to an event) is to check for which gadget the event is relevant. See slide 7-w-handler. The gadget class employed here, which is defined in slide 7-w-gadget, is an extension of the gadget class defined previously. For convenience, we assume that all event-related information is stored when the gadget is created.

Handler -- interception

  class handler { 
handler
event* e; public: handler(event* p) : e(p) { g = 0; } virtual ~handler() { delete e; } int info() { return e->info(); } void set(gadget* p ) { g = p; } virtual void operator()() { if (g) g->callback(); } protected: gadget* g; };

slide: A handler class

In addition to a constructor and a method to deliver the event information, a handler object provides a method to associate a gadget with a handler. As we will see later, the window receiving the event will invoke the actual callback function which is defined by the gadget in question.

Gadget

-- callback
  class gadget {  
gadget
public: enum { PRESS, RELEASE }; gadget(int i) : info(i) { next = 0; } void pick(handler* h) { if (h->info() == info) { h->set( this ); } else if (next) next->pick(h); } virtual void callback() = 0; void insert(gadget* g); private: int info; gadget* next; }; void gadget::insert(gadget* g) { if (!next) next = g; else next->insert(g); }

slide: Another gadget class

We also assume that a window may contain multiple gadgets. These gadgets are simply chained using the next field (as illustrated in the function insert). The most important member function of a gadget, in our example, is the function pick, used to determine whether an event is of relevance to the gadget. If the event is relevant to the gadget, which is checked by comparing the handler information with the gadget info field, then the gadget is associated with the handler by handler::set. Otherwise, the next gadget in the chain is asked to check whether the event is relevant. The class gadget is an abstract class, since it provides a pure virtual function callback (which is assumed to be defined by an actual gadget, as illustrated in the definition of the button class in slide 7-w-button).
  class button : public gadget { 
button
public: button() : gadget(PRESS) { } void callback() { cout << "..."; } };

slide: Another button class

Finally, we may define a window with a function receive that is invoked when there is an event for that window. See slide 7-w-window.

Window

-- receives events
  class window { 
window
public: window(gadget* p) { g = p; } void insert(gadget* p) { g->insert(p); } void receive(event* e) { handler* h = new handler(e); g->pick(h); (*h)(); } private: gadget* g; };

slide: Another window class

When an event is received, a handler for that event is created, the function pick is invoked for the first gadget in the chain, and finally, the callback associated with the handler as a result of picking the chain is executed.

Discussion

There is no need to say that in this example many of the details have been suppressed. However, note that the user is obliged only to define the function callback in the derived gadget class button. It should be noted that in the InterViews library, other forms of callback, including member functions of arbitrary objects, are allowed as well. However, these forms require the use of notationally quite complex pointers to member functions, which proved to be an obstacle for many student programmers. In section hush we will look at an alternative approach to object-oriented window programming.

The MV(C) paradigm

The previous examples (describing the cooperation between gadgets, windows and events) illustrate how the complex details of the interactions involved may be hidden from the user, who merely has to define the appropriate (virtual) functions to specify the particularities of the application. In a similar way, the interactions between user-defined objects may be specified by employing a collection of abstract classes, governing the overall structure of the process of cooperation. As an example we will look at a skeleton implementation of the MVC-paradigm, which is an essential part of the graphical user interface development framework of the Smalltalk-80 system (see section MVC). For convenience, we omit the controller (C) part (which is responsible for dealing with user input). However, in the next section we will discuss how we may employ events to deal with user input in an (even more) elegant way. Omitting the controller class, the MV(C) structure consists of a model class (from which the class describing the functional behavior of the application will be derived), a view class (to display the state of the model to the user) which may be refined by the application to fit its particular needs, and one or more model realization classes defined by the user to model the functional behavior of the application. An abstract model class may be defined as in slide 7-mv-model. By employing a template class we have abstracted from the actual value type of the model.

Model -- functional behavior

  template< class T >
  class model { 
model
public: void tell(view* v) { dependent = v; }
one view only

void changed() { if (dependent) dependent->update(); } virtual T value()=0; protected: view* dependent; model() { dependent = 0; }
restricted creation

};

slide: A model class

The model class provides three public member functions. A function tell (to install a view object for the model), a function changed (to notify the view objects associated with the model of a change), and a function value (which is merely used to illustrate how the model object may give information concerning its state). For convenience, we assume that there is only one view object associated with a model object. (It is an easy exercise to extend the skeleton to associate multiple views with a model.) The view object is stored in the instance variable dependent. Also, the class model needs to have a virtual destructor, to reclaim the resources needed by its derived classes and their views. Now look at the definition of the view class in slide 7-mv-view. To allow for models supporting various value types, the class view must also be defined as a template class.

View -- user interface

  template< class T >
  class view { 
view
public: view(model< T >* p) { p->tell(this); m = p; } virtual void update() { cout << m->value() << endl;
or whatever
} protected: model< T >* m; };

slide: A view class

The view class provides a constructor which takes a pointer to a model object as its argument. Evaluating the constructor results in setting the dependent view pointer of the model to the view being created. When a view object receives the request update, the model associated with the view is consulted and the information delivered as a result is (in some way) displayed to the user. As an example of a concrete model class, look at the account class defined in slide 7-mv-account. It is derived from model since the value type of the account class is simply int.

Example -- account

  class account : public model< int > { 
account
int n; public: account() { n = 0; } void operator++() { n++; changed(); } int value() { return n; } };

slide: The account class

The account class introduces an integer instance variable, an operator to increment the instance variable (just like a counter), and (re)defines the function value (which merely returns the value of the integer instance variable). There is no need to redefine the view class, although this might have been easily done. As an example of using the account class look at the fragment below:
  account a; view< int > v(&a); a++; a++; 
  

slide: The account example again?

An instance of view< int > must be created since account is derived from model. Note that incrementing the account results in displaying the value automatically. This illustrates in a nutshell how the modification or access of a value may be propagated into a chain of arbitrarily complex (inter)actions. Yet, the abstract architecture of the MV(C) framework ensures that the interactions take place in a carefully controlled way. However, there is a serious problem with using the MV(C) paradigm in C++. Despite its elegant appearance, it is hard to generalize the relation between model and view classes to include arbitrary interactions. In the next section, we will elaborate on this by generalizing the notion of event-driven control to include user-defined events.