Implementing Forms

Figure 1 shows the architecture of the Form package. This is a Booch-style class diagram; the notation was discussed in Part I of this article. I've moved in the direction of the Unified Booch/OMT notation by pulling nested classes outside the container and identifying the nesting with a base_class:: in the name.

Figure 1 Form Class Architecture

Note that the Form "is displayed on" a window, but this is a "uses," not an "is" (derives from) relationship. This is one place where MFC invariably gets it wrong, primarily because the designers were thinking in terms of the underlying operating system rather than in true abstractions. Derivation is used properly in only two situations, which can be characterized as "extends" and "implements" relationships. An extends relationship adds properties to an existing class, the "is-a" test discussed in most books on OO. (An employee "is-a" person since employees have all the characteristics of people—names, and so forth—but add additional characteristics such as salary.) An implements relationship is used primarily in multiple-inheritance scenarios when you want to mix a capability into a class. In Figure 1, a Form implements a User_interface by deriving from it. This way a Form can be passed to any function that takes a User_interface argument without difficulty. This sort of derivation fails the is-a test (a Form certainly is not a user interface) but is nonetheless a legitimate use of the mechanism.

Both kinds of derivation have a common characteristic since conversion to the base class is commonplace. Here's the rule of thumb: derivation is appropriate only when a derived-class object is occasionally treated as a base-class object. Looked at another way, don't use derivation if you never pass a derived-class object a message whose handler is defined in the base class. A Form is not a window—it could be displayed on paper rather than on the screen, for example. Forms and windows are different things with different properties. More to the point, Forms are never passed to functions that expect to deal with windows. A Form simply uses a window to display itself in some situations, so derivation is inappropriate here. You could really apply the same logic to MFC's CDialog class, which derives from MFC's version of a window: CWnd. I've never passed a CDialog object a message defined in CWnd, and I've never passed a CDialog to a function that expected a CWnd argument. I can imagine occasional
rare situations where I
might want to do the above, but I haven't yet. I can also imagine winning the lottery, but that hasn't happened either. Consequently, deriving CDialog from CWnd seems inappropriate.

The Form is made up of several Field objects, each of which knows the name of the attribute and its position on the Form; once the Form is activated at runtime, the Field keeps track of the associated proxy. There are two sorts of Field objects: a normal Field represents an attribute of some object, the name attribute of some Employee, for example. A static Field is an attribute of the Form itself, such as a label next to another Field or a decorative icon on the Form.

A normal Field is created using the first constructor shown in the Form::Field cloud in Figure 1. You give the constructor the name of the class whose attribute is going to be displayed in the Field along with an attribute identifier. (Different classes could have attributes that happened to have the same name, of course.) The identifier could be as simple as the attribute's name. (You might just use "name" as an identifier for an Employee's name attribute, for example.) You could also use a more elaborate attribute name if that makes sense. A Drawing object, for example, could be asked to display a small part of a big drawing by encoding the desired display rectangle into the attribute identifier. The third constructor argument is a Form-relative rectangle that shows the size and position of the Field on the Form. The final argument establishes the behavior of the attribute proxy—an object that represents the display mechanics—on the Form. Possible values are Field::input, Field::output, and Field::input_output (the default). The input_output mode is provided primarily for documentation—it behaves identically to an input Field. Output Fields always send display messages to their proxies, even when the containing Field is sent an "interact" message. Input and input–_output Fields are sent either display or interact messages, as appropriate.

Note that a normal Field as yet knows nothing about the actual object that it will display. The Form definition would normally be persistent—it might be stored on the disk or as a const global variable, for example. (I haven't implemented persistence in the current article to make things a bit simpler, but it's easy enough to do. See a previous article of mine, "Roll Your Own Persistence Implementations to go Beyond the MFC Frontier", MSJ, June 1996, for information.) The Field is connected to the actual object that's using the Form for I/O at runtime.

A static Field is created with the second constructor shown in Figure 1. It's used for icons, labels, and so forth. A static Field differs from a normal Field primarily in that it knows exactly what it's going to display at definition time. (Normal fields get proxies at runtime.) In addition to the position rectangle, the Field constructor is passed a pointer to an object that implements a User_interface, and that object is always displayed in the static Field. (The User_interface object is never sent an interact message, only a display message.) Since static Fields are always output only, there's no need to pass a Field::Behavior into the static-Field constructor.

Again, for simplicity's sake, I haven't implemented persistence, but User_interface objects used for static Fields should obviously be persistent too because they will have to be stored on the disk along with the rest of the Form.

The Field objects must be allocated dynamically via new. (Most persistence implementations, including my own and the one implemented in MFC, work this way.) The Form can be populated with Fields in two ways. The constructor can be passed a variable-length list of pointers to Field objects, or you can call add_field to add Fields to a previously constructed form. The Form passes all of its Fields to delete when it is destroyed. The main concept here is ownership. You own the Field when you create it, but once it's passed to the Form, the Form owns it. It's up to the owner to delete the object.

A Form is created like this:


Form *the_form = 
  new Form( "form_name", new Field( Text("Name:"),  
            // This next line is Static text       
            Rect(0,  0,  20, 10)),
            // This next line is the Name Attribute
new Field("Employee", "Name", Rect(20,10, 100, 10)),
NULL
);

The Form is named "form_name." It contains a static Field that holds the label "Name." The second Field will eventually be used to display the Name attribute of the Employee class.

Automatic deletion of a Field by a Form is, of course, a potential maintenance problem because there's no way to stop someone from allocating a Field as a local variable and then passing a pointer to it to add_field. A sufficiently robust implementation of operator new and delete would detect this problem at runtime, but there's no guarantee that an idiot-proof memory manager will be used. In any event, the alternatives aren't much better. I could, for example, provide an overload of add_field that took a Field reference (rather than a pointer) for an argument, and then mark Fields that were added with the reference-version of add_field as "not to be passed to delete." All this overload does is introduce another possible bug, though: I could accidentally allocate a Field from new and then pass it to add_field using *p. The other possibility that comes to mind is passing the Fields by value, and actually putting copies of the Field into the Form. This is probably the safest path, but it also has the highest overhead. Passing by value can work if I use the same reference-counting strategy that I used in Part I's String class, but at the cost of considerable extra complexity. Requiring all Fields to be consistently allocated from new seems like a reasonable compromise.