Hide Your Data and Make Objects Responsible for Their Own User Interfaces, Part III
|The behavior of a Form is nothing like that of an MFC CDialog. The Form serves as a conduit from the user directly to the User_interface objects contained in the Proxies. You don't put an edit control on a Form; you tell Text to display its user interface on the Form.
|This article assumes you're familiar with C++, MFC|
Code for this article: FormsP3.exe (104KB)|
Allen Holub is a programmer, consultant, and trainer specializing in C++, object-oriented design, and Microsoft operating systems. He
can be reached at firstname.lastname@example.org or http://www.holub.com.
I presented the basic architecture of an OO Forms package in my previous two articles. This month, I'll continue the discussion by looking at the lower-level design details and the actual implementation. To refresh your memory, last month I introduced the idea of a Form, which is really just a surface on which an object places a user interface. The Form keeps track of where certain attributes of a given class are placed, but doesn't know anything about the object it's displaying beyond the name of the attributes. User input flows directly to the object, without the Form getting in the way at all (or even knowing that user input is occurring).
The basic goal was to decouple the layout of a Form from the classes displayed on the Form. With the architecture I developed, you can radically change the layout of a Form, and the objects displayed on it will be completely unaffected. They don't even know that the Form has been changed. The flip side is also true; you can radically change the implementation of some class, and the Forms on which objects of that class are displayed are equally unaffected.
Behavior like this is impossible using MFC's CDialog class, which not only hardcodes a dialog box with the C++ class that wraps it, but also forces you to export data from one object (a CDialog derivative) to another (the object that uses the CDialog derivative for its I/O). This behavior is at odds with good OO design, which mandates that changes to a class should not affect any other class in the system if at all possible.
Now for the details.
Shutting the Form Down
A Form is inherently modeless. This is Windows, after all, so when you send a "display" or "interact" message to the Form, it essentially creates a bunch of subwindows that are effectively doing their own thing in the background until they are closed by the Form shutting down. There's no particular reason for creating a Form on its own thread, though a Form can run on its own thread without difficulty. In MFC, you'd derive a class from CWinThread and activate the Form by sending it a display or interact message in the InitInstance function of your derived class. Message handlers for the subwindows created by the Form are executed on the thread. Don't confuse the objects linked to the Form with the thread. Objects don't execute on threadsfunctions do. The objects attached to the Form get updated the same way, regardless of the number of threads that are in use. It doesn't matter which thread is running when the Form is created. It does matter who activates the Form, though, because the Windows messages sent to the User_interface objects are processed on whichever thread executes the CreateWindow function. If the Form's message handlers are running on their own threads in this way, the objects attached to the Forms will appear to magically change state as the user interacts with the attached Proxies.® being Windows, the main difference in behavior in a single-threaded scenario is that message handlers for the controls won't be activated until whatever function created the Form returns, thereby reentering the main thread function's message pump. In a single-threaded application, this is probably the message pump in WinMain. Since a Form is typically created from a menu handler or equivalent, which does nothing beyond creating the Form object and activating it, this behavior is typically not a problem. In a multithreaded application, the Form can modify the linked objects as soon as it's activated, so the problem doesn't exist there at all.
There are problems associated with memory management, however. Though you'd like to be able to create a Form as a global variable, on the stack, or from operator new, I've decided to be a coward and only support the last of these. A Form must be created using new, and the memory will automatically be discarded when the Form shuts down. The Form can be shut down by the user, either by clicking the close box or by pressing a special button that I'll discuss in a minute. Note that the same Form can be used to initialize or display several objects in succession. You don't have to destroy it just because you're finished with it for the time being. Just pass the Form a release_proxies request and then reattach the Form to the new objects with one or more put_yourself_on_this_form messages. Also note that a Form can be passed a hide message, just like any other User_interface object. (It sends hide messages to all the Proxies in this situation rather than releasing them, so be sure to send release_proxies before sending hide if you want to disconnect the Form from the Elements before it's hidden.)
Remember that the behavior of a Form is nothing like that of an MFC CDialog. The Form serves as a conduit from the user directly to the User_interface objects contained in the Proxies. You don't put an edit control on a Form; you tell Text to display its user interface on the Form. That interface will probably look like an edit controlit might even be one in realitybut details of implementation are irrelevant. When the user types into the Text's user interface, the associated Text is updated right then, as the user types. Data validation is also probably being done as the user types as well. There's no need to export data from a control and then put the data into a CString or to validate data in an OK handler as you would in a standard MFC app. The Form itself contains nothing of interest to the user, so there's no reason for a Form object to exist once it disappears from the screen.
But how do you actually shut the thing down? The easiest way is to just let the user double-click the system box (or click the close box) on a Form, effectively releasing all the Proxies and discarding the memory for the Form itself and its Fields. If one or more of the Proxies was created with the notify_me_rather_than_delete_proxy attribute, the associated Element is effectively notified when the Form shuts down and the release_proxy message is received. In fact, you can create a Proxy that doesn't display an interface at all solely to get notified in this way. Proxies are guaranteed to be released in the same order that they were added to the Form, so you need only label the last proxy as a notifier if all you need is a close notification.
As an aside, other sorts of notification must be handled by the Proxy or its underlying User_interface object. For example, if you need to be notified when a button is pressed, you can derive a class from Form::Proxy or User_interface whose handler for the interact message displays a button. The Proxy derivative must catch the BN_CLICKED notification and do whatever it wants with it, presumably notify whatever Element created the Proxy by sending a normal C++-style message. This is also how you'd handle Proxies that must interact with one another to do data validation.
A special Form::Close_button object is supported to make the Form look more like a dialog. An object of class Form::Close_button, when passed to the Form as a static attribute, causes the Form to be destroyed when it's pushed. You'd put one on a Form like this:
Form a_form ( "a_form", new Field( Form::Close_button("OKAY"),
Rect(0,0, 20, 10) ), NULL);
The Form will also shut down when the user presses the OK button in the upper-left corner of the form.
A Complete Example
Figure 1 is a simple but complete example of a Form. A simple Employee class (that has one attributea name) is at the top of the listing. The name is implemented internally as a Text objectremember, Text is a String that exposes a User_interface, so it can be used in an attribute proxy as it stands. The private section underneath the name declaration provides overrides of the two Form::Element virtual functions needed to put an Employee on the form. The functions are defined just below the class definition.
Employee::give_me_a_proxy_for creates a new Form::
Proxy that represents the name fieldthe address of this field is just passed into the Form::Proxy constructor. I'm ignoring the form_name and attribute_id arguments here because the Employee has only one attribute, but a more-complicated implementation would probably have a lookup table or some similar mechanism to decide which attribute the Proxy actually represented. Both arguments to the Form::Proxy constructor are used here. The second argument tells the Form to send a release_proxy message rather than destroying the Proxy internally when the Form shuts down.
The release_proxy function comes next in the listing. It's essentially a one-liner that does nothing but delete the Proxy. The callback is needed here to prevent the Form object from trying to send the address of the Employee's name field to operator delete.
The Form is created in the make_form function. It has three fields, a label that holds the string "Name:", a placeholder that will be filled with the interface for the Employee's name field, and a close button. OnTest (which is called in response to picking a menu item) puts the Employee object onto the Form. Sending the Form an interact message makes it pop up (see Figure 2). The contents of the Employee's name field is displayed in the edit control. Text typed in the edit control flows directly into the Employee object's name field without the Form being involved at all. Most importantly, the Employee is completely isolated from user-interface issuesit's written entirely in terms of Text objects with nary a window in site. The architecture, then, is essentially platform-independent. The vast bulk of the code doesn't care about the platform at allit's written in terms of primitives like Strings. Changing platforms involves changing two files only (wrappers.h and wrappers.cpp, discussed in Part I and below).
Figure 2 The Form
Implementing the foregoing design is relatively straightforward. As is usually the case, translating a detailed design into code is more a matter of rote copying than anything else. Scenario and class diagrams are really just graphical ways of representing the same concepts that you can express in an OO programming language; in fact, there's a one-to-one correspondence between the diagrams and the class definitions.
Before looking at the code, I do need to stay in the design realm a moment longer and flesh out a few implementation details. The earlier figures were the result of what's usually called the analysis phase. They represent the problem in graphical form, but without sufficient detail to be implemented. Figure 3 and Figure 4 represent the design phase, where you start filling in those details. The design-level drawings add information to the analysis-level docs; they don't replace them. (That's one of the main strengths of OO in my opinionthe products of the analysis phase are also the first part of the design phase, thereby reducing your work considerably.)
Figure 3 shows the relationships between the various support classes and the Form class discussed earlier. Figure 4 shows the runtime interaction as a Form is displayed and closed.
Starting with the relationships in Figure 3, the window object is the surface on which the Form is drawn. Windows in MFC are odd things in that, with a few exceptions, you can't reliably delete them in your program. Usually you create the memory for a window with new, activate the window, then forget about it. It's the user who shuts down the window, not the program. How then do you delete the memory occupied by the C++ window object? The MFC CWnd class has a virtual member function called PostNcDestroy (overridden in window) that's called as the absolute last thing when a window is destroyed. The override contains the expression
thereby freeing the memory automatically when the window is destroyed. The main problem is that you cannot allocate such a window on the stack or as a global variable because the delete will fail. Always use new. If you need to destroy the window programmatically, send it a DestroyWindow message to simulate the user shutting the window down.
The Form uses a Window; this is a containment relationship, not derivation. Consequently, the Window needs some means to tell the Form that the window has shut down. I've used the Notifier/Notifiable mechanism described in Part I for this purpose. The window's constructor is passed a pointer to a Notifiable object to which it passes a notification message when it closes. I didn't want to derive Form from Notifier both because I didn't want to complicate the higher-level design and because I might decide that I want the Form to be notified about other things in the future. Instead, I've created a class called Destroyer inside the Form's namespace, declared a Destroyer as a member of the Form, and passed a pointer to the Destroyer member to the window at create time.
Looking at the lower part of Figure 4, Windows sends PostNcDestroy when the window shuts down. The window then notifies the Form's Destroyer object by calling its notify override. Notify then passes to delete a pointer to the associated Form. The Form's destructor calls release_proxies, which leads to the release-proxies process from Figure 5.
How does the user shut down the window? If the Window is created with a title, there will be a title bar with a close box on it, so that's an option. If there's no title, there's no title bar or close box, which brings me to the Close_button discussed earlier. The Close_button class essentially wraps a User_interface around a Button object. The Button is a wrapper around CButton that uses the Notifier/Notifiable mechanism to tell another object when it's been pressed. That is, the constructor is passed a pointer to a Notifiable object (in this case, the Close_button that creates it), which is notified on a button press. The Close_button keeps track of its parent window (a pointer to which is passed with the display or interact message), and sends a DestroyWindow message to its parent when the button is pressed. This causes a PostNcDestroy to be sent to the parent window.
Since the Close_button implements a User_interface, it can be used as a static attribute in a Form, and that's how you get the button on the Form. When the Form displays the button, it sets itself up to be destroyed by the button. Since the button is on the Form, it will be destroyed when the Form shuts down, just like any other static attribute.
Note that the CloseButton is declared in the Form's namespace, but it actually can be used to destroy any Window, so you might want to move the definition out to the global level if you want to use Close_buttons in other situations.
Form Class Code
So, finally, it's time to look at the code. You ought to have a pretty good idea by now how just about everything works. The code is a straightforward implementation of the design. A few details are worth discussing.
The Window and Button classes in Figure 6 are just extensions of the wrappers.h and wrappers.cpp file from Part I (MSJ August 1996). In fact, wrappers2.h is just #included at the bottom of wrappers.h, so you won't see a mention of it anywhere else in the code (of course, if this wasn't a three-part article, I would have put everything in one file).
The Window class at the top of both files derives from MFC's CWnd, adding Notifier functionality to support the notify-on-close message. The main issue here, other than the implementation of PostNcDestroy discussed above, is the use of PreCreateWindow. This function is called during the create process just before the Windows CreateWindow call is made. It is passed a structure with a field for each argument that will be passed to Windows, and gives you a chance to modify any of these arguments. In the current code, I'm registering a new Window class with Windows (by calling AfxRegisterWndClass) and passing the resulting class name to Windows. The Windows window is actually created in the Window-class constructorthere is no two-part creation as in MFC. The window is initially hidden, so passing the window object a show message displays it.
The Button class, which is a very small wrapper around MFC's CButton, is also implemented in Figure 6. All it does is catch the OnClicked message that comes in from Windows and translates it into a notification. The main purpose of this class is platform independenceyou can write code in terms of Buttons without having to worry about Windows messages.
The Form and related classes are implemented in form.h and form.cpp, shown in Figure 7. The main implementation problem worth discussing is in the Form's constructor, which takes a variable number of arguments. (The function definition is the second one in form.cpp.) Normally, a function like this could get by with only one argument, but C++ provides a rather nasty gotcha here. The va_start macro initializes the va_list object to hold the address on the runtime stack of the argument following the first one. A call like
va_start( field_list, start);
expands more or less like this:
field_list = (char *)&start;
The problem is that the first argument to the Form constructor is a reference, and even though reference arguments are passed to functions via pointers, there's no way to get the address of that pointer. Taking the address of a reference always gives you the address of the referenced objectthe contents of the pointer that's used internally to represent the reference. Consequently, if I were to pass form_name to va_start, I'd initialize the field_list to the address of whatever String is used to hold the form_name rather than to the address, on the stack, of the pointer used to pass the form-name reference. I've solved the problem by adding a second explicitly declared argument to Form::Form, and passing that second argument to va_start.|
The final issue is the relationship between the Form and its window. Normally, the Form keeps track of the space required to hold the fields and creates a window exactly that size. The Form's display and interact messages interpret a NULL window parameter as a create-your-own-window request. You can adjust the size by deriving a class from User_interface, whose virtual-function overrides literally do nothing, and then installing an object of that class in the Form with a position rectangle specifying an upper-left corner of (0,0) and a lower-right corner specifying the window size. Alternatively, you can create a window object that has the desired size and pass a pointer to that window object to the Form's display or interact message. The Form will use the indicated window in that case.
Making the Code Useful in the Real World
The code from this month is a start, but a lot needs doing to get it ready for real-world use. The main deficiency is the lack of persistence. Several changes need to be made for real-world use. The rest of this article describes these recommended changes. AWT package. Rather than specifying Field positions using absolute rectangles, I'd like to represent the position as a weighted percentage coverage of the entire Form. This way the Proxies could change size when the size of the Form changed. This change can be done entirely within the Form if you're willing to hide the Proxy and then redisplay it every time the Form changes size. With quite a bit more work, you could even allow the user to drag the Proxies around on the form at runtime and have the Form remember the new position in some persistent way.
Everything needs to be made nicer looking on the screen. Text objects, for example, are shown with a gray background when sent a display message, but the underlying window has a white background. There's lots of little stuff like this that needs fixing.
The next problem is that a Form is a very passive container for Fields. I'd like to implement dialog-style tabbing and the notion of a default button too. This can be done by expanding the User_interface class so that it can notify the Form when it detects a press of the tab key.
The Form and Fields (but not the Elements or Proxies) need to be persistent so that their lifetimes can be longer than that of the program. Among other things, persistence would allow me to make a graphical Forms editor that would work like a dialog editor but store Form objects on the disk instead of in a .rc file. Persistence is easy enough to implement, but I haven't done it here.
Another useful feature is auto-resizing along the lines of Java's
I'd also like to clean up the memory model, making everything reference-counted much like the String developed in Part I and eliminating the restrictions that everything must come from new.
Another issue to think about at least is the fact that the User_interface objects I've been using (such as Text) create their user interfaces by making windows. If the Form has a lot of Fields, this could affect the time it takes for the Form to appear on the screen. A better solution might be to use a Canvas class that encapsulates the Windows device context rather than using a Window class. That is, the User_interface's display and interact messages would be passed pointers to canvases (device contexts) on which they had to draw themselves. Similarly, the Form would have to process all Windows messages for all Fields and route them to the correct Proxy. (The easy solution is to add a virtual WndProc function to the User_interface and have the Form's WndProc route messages to the Proxy's WndProc when it had the focus.) This routing capability would, of course, make the Form implementation more complicated.
In spite of all these peccadilloes, the Forms package is pretty useful as it stands and certainly serves to demonstrate the basic OO principles involved. A Form really does solve the problem of an object needing to display or load itself in different ways in different situations, and it does it in an OO way that doesn't require the object to expose any information at all about its internal implementation. Unlike an MFC CDialog, you can actually use a Form in an OO design without compromising the design at all.
From the January 1997 issue of Microsoft Systems Journal.
Get it at your local newsstand, or better yet, subscribe.
© 1997 Microsoft Corporation. All rights reserved. Legal Notices.