Design of an Object Hierarchy

When you add complete OLE Automation support to an application, you're doing more than making a single object with a few properties and methods, as we've been doing with the Beeper variations. Instead, you're trying to fully describe the functionality, features, and capabilities of your application through programmatic interfaces.

Your user interface is a good place to start your design because it already describes the capabilities of your application as well as how users access those capabilities. Much of what you try to accomplish in automating an application is to allow people to write macro scripts in an automation controller that can drive your application. It makes good sense to design your automation objects and interfaces to provide the same conceptual model as your user interface. So, for example, if you have an object that represents the application as a whole, you should be able to tell that application programmatically to change the position of a window, the window's visibility, or the contents of the status line or caption bar. You can see already that through a programmatic interface, you can provide a way to affect elements of the application for which there might not be a user interface. Windows doesn't offer an element by which a user can change a caption bar directly, but it's easy to provide the capability through a programmatic interface.

Analyzing your application and breaking it into objects of different sorts results in some kind of object hierarchy, or what some might call an object model. There are plenty of academic methodologies for object-oriented analysis that easily apply to formulating your object hierarchy. No matter how you find the objects you want to expose, each object will represent some functionality and some content or information. Naturally the functionality maps well to methods and the content to properties. Some of the methods and properties might be restricted or hidden (such as methods named with a leading underscore—for example, _NewEnum); others might be intended primarily for the eyes of a developer or an end user. With the richness of ODL, you can describe detailed and complex object hierarchies.

Your object hierarchy will also invariably involve collections. For example, an MDI application should have a documents collection that manages the group of singular document objects. This is simply a way of programmatically exposing the functionality of the MDI client window and the MDI child window. So just as you ask the MDI client window to create an MDI child, so too will you ask a documents collection to perform a task with a document object. The relationship between a document, a documents collection, and the application object in which all of these are contained generally forms the basic automation model for applications. The OLE Programmer's Reference has a chapter on standards and guidelines for document-centric applications.

You should understand that document is used here as a generic term to describe whatever your application uses as a file or an individual entity of data (a spreadsheet, a presentation, a drawing, and so on). When naming objects, you do not have to use Document at all—Cosmo, for example, uses Figure for the document-level object and Figures for the collection of figure objects. Use names that are appropriate and that will be most meaningful to the target users of your objects. That is what is most important in all of this.

The next sections describe OLE's guidelines for document-centric applications. These are intended primarily for user interfaces. If you have a single-document interface or an application that is generally not user interactive, many of these guidelines will not be that useful or important to you, and your design will be basically an object hierarchy with a lot of truly custom dispinterfaces. But if the functionality or content you want to expose matches a standard method or property in these guidelines, you are encouraged to conform to them. Don't forget, however, that they are just guidelines and are not enforced in any way.

One topic you might find missing in most of the guidelines is security; only one standard method for opening a file takes a password argument. This means that the design of security is up to you. You can enforce a password on many function calls or have a security checkpoint function that has to be called before any other method or property succeeds. You can enable security in many ways, but the guidelines themselves don't deal with these issues.

Before looking at the details, let's cover some concerns with the naming of objects, properties, methods (and events), arguments, constants, and enumerations. Keep in mind that all of these names (including arguments when the controller supports named arguments) will generally be visible to developers and end users alike. Because of that, keep names as whole, readable, and grammatically correct as possible. For example, use application instead of app, document instead of doc, window instead of wnd. If you must abbreviate, you can use shorter names sparingly to keep the length of the name at a reasonable size, using whole syllables as much as possible. You should also use mixed case names without underscores, such as ActiveDocument rather than activeDocument, Active_Document, Activedocument, or, horror of horrors, ACTIVEDOCUMENT. In addition, match the names in your automation interfaces to the names a user will be familiar with in your visible user interface. This can only enhance a user's understanding. Finally, remember that these names are important only in your type information—internally you can use whatever names you want.

A Basic Automation Hierarchy

Many current applications operate through the MDI interface that Windows provides, so the basic standards for an automation object hierarchy were written assuming that model: one application object, one or more documents collection objects, and any number of document objects, as illustrated in Figure 14-7. Obviously, if you have a single-document interface, you can merge the capabilities of the documents collection into the application object because there is no need to manage a group of one document. In cases in which you also have different collections of different document types within the same application, you can have multiple collections. OLE Automation defines a few standard properties and methods for each type of object shown in Figure 14-7.

Figure 14-7.

The basic MDI object hierarchy, including the application and document levels.

All objects in an application hierarchy of this sort support two basic read-only properties: Application and Parent, both of which are IDispatch pointers. Application is the way to navigate from any object in the hierarchy back to the top, to the application object. Parent is the way to navigate from one object up to the next level. Obviously, for an object such as a documents collection, Parent and Application are the same thing. The only object for which these properties make little sense is the application object. You might want to provide them for consistency, in which case the application object just returns itself.

The application object

This object represents what you can do with the application's frame window and also provides the means to navigate down the object hierarchy shown in Figure 14-7. It is also the object through which a controller can access any sort of global state variables in the entire hierarchy. In an ODL file, this object should be given the appobject attribute to mark it with this special role. The recommended properties for an application object are shown in Table 14-2; the recommended methods are shown in Table 14-3. You'll notice that most of the properties have to do with either window position or the name, caption, or default path of the application. The methods are few; the most important ones are Help and Quit. Probably the foremost element of the whole set is the Documents property (which can be named more appropriately for your application, as in Figures), which allows the controller to navigate to other objects where the interesting functionality lies. The ActiveDocument property (which can also be named differently) is the next most important.

Property

Description

[read-only] IDispatch *
ActiveDocument

The IDispatch of the active document object, or VT_EMPTY if none.

[read-only] IDispatch *
Application

Returns the application object.

BSTR Caption

Sets or returns the title of the application window. Setting the caption to VT_EMPTY returns control to the application.

BSTR DefaultFilePath

Sets or returns the default path specification used by the application for opening files.

[read-only] IDispatch *
Documents

Returns the documents collection object.

[read-only] BSTR FullName

Returns the full path of the application EXE, as in C:\INOLE\CHAP14\COSMO14.EXE.

[read-only] BSTR Path

Returns the path of the application's EXE, as in C:\INOLE\CHAP14.

[read-only] BSTR Name

(Default property: DISPID_VALUE.) Returns the name of the application—for example, "Cosmo Chapter 14".

boolean Interactive

Sets or returns whether the application accepts actions from the user regardless of visibility.

boolean Visible

Sets or returns whether the application is visible to the user.

BSTR StatusBar

Sets or returns the text displayed in the application's status bar.

long Left

Distance between the left edge of the screen and the left edge of the application window. Setting this property moves the window (as does setting Width, Top, and Height).

long Width

Width of the application window, including all borders.

long Top

Distance between the top of the screen and the top edge of the application window.

long Height

Height of the application window, including borders, menu, caption, and so on.


Table 14-2

Guidelines for application object properties. Properties not marked [read-only] can be modified from a controller. Note that Name is the default property with DISPID_VALUE.

Method

Description

void Help([optional] BSTR HelpFile, [optional] long HelpContext, [optional] BSTR HelpString)

Displays help information for the application. If HelpFile and HelpContext are given, the application launches WinHelp. The help string is another way of specifying the context, in addition to HelpContext.

void Quit(void)

Terminates the application, closing all documents. If the user has taken control of the application while it is being driven programmatically, this method must be ignored. Otherwise, you face the possibility that the user has independently created a new document through your user interface, in which case the user must now close the document and the application manually.

void Repeat(void)

Repeats the previous action in the user interface. This method can be part of a document object instead if you want.

void Undo(void)

Reverses the previous action in the user interface. This method can be part of a document object instead if you want.


Table 14-3

Guidelines for application object methods.

The documents collection object

Earlier in this chapter, we saw some basic requirements for a generic collection object. These are summarized in Table 14-4. In addition to the Parent and Application properties, OLE defines a few standards for collections of documents, as described in Table 14-5 on page 720. The default member, DISPID_VALUE, is the Item method in all cases so that a piece of controller code such as Collection(index) can be used in place of Collection.Item(index).

Method/Property

Description

[read-only] long Count

The number of items in the collection.

Property: [read-only] IUnknown * _NewEnum Method: IUnknown *_NewEnum(void)

Returns the IEnumVARIANT enumerator for the collection. (Older OLE documentation incorrectly states the return value here as IDispatch *, which enumerators do not implement. The correct return type is IUnknown *.)

<type> Add(void) or void Add(<type>)

Adds a new item to the collection. If the item cannot exist outside the collection, the collection itself should create the item and return its value (first syntax). For example, adding a new object will return that object's IDispatch pointer. If the item can exist separately, the collection simply maintains it in the group (calling AddRef for IUnknown or IDispatch items) but does not create it or destroy it. In this case, the item's value is an argument to Add, and the method itself has either no return value or a boolean to indicate success or failure.

IDispatch * Item([optional] long Index, [optional] BSTR Name)

Returns the item identified by its ordered position (Index) or its name (Name). Both arguments are optional. Item can also take a VARIANT to support both types with one argument. If no argument is given, returns the documents collection pointer itself.

void Remove(<index>)

Removes an item from the collection; <index> is the same argument list as for Item. For items that cannot exist outside the collection, this also destroys the item. If the item is another object, that object should support some sort of Close or Destroy method in lieu of the collection supporting Remove.

Remove takes out of the list any item that exists outside the collection, calling Release for IUnknown or IDispatch pointer items.


Table 14-4

Guidelines for generic collection objects.

You'll notice that a collection's methods vary slightly if the items in the collection can or cannot exist outside the collection. In some cases, the collection creates the items within it, thus acting as the only way to create the items. This means that the items can exist only as part of the collection and not outside it. If the items in the collection are objects themselves, the collection should not support Remove but should depend instead on a Close or a Destroy method in the subordinate object. A document object in a documents collection is such an example. For nonobject items, which by virtue of not being objects have no methods, the collection should support Remove. Older

Method/Property

Description

IDispatch * Add(void)

Creates a new hidden document and adds it to the collection, returning that document's IDispatch.

(Remove)

Should not be part of a collection; instead, the document object should support a Close method.

void Close([optional] boolean AskSave)

Closes all documents in the collection, optionally prompting the user to save changed documents.

IDispatch * Open(BSTR File, [optional] BSTR Password)

Opens an existing document and adds it to the collection, returning the document object's IDispatch pointer (or VT_EMPTY on error).

IDispatch * Item([optional] long Index, [optional] BSTR Name)

Same as a generic collection's Item except that Name identifies the document filename rather than a generic name.


Table 14-5.

Additional guidelines for documents collection objects.

OLE documentation is unclear on this point because it assumes that a collection contains other objects, when in fact a collection can contain anything you can enumerate with a VARIANT.

The document object

In the context of the OLE guidelines shown in Tables 14-6 and 14-7, the document object is the richest object of them all, which you would expect because a document is where most of the action takes place within an application. Again, document is used generically to describe a child window with meaningful stuff—it doesn't have to contain text or whatever else you might associate with a document.

A note about method names: if you try working with an automated application using DispTest or Visual Basic 3, these controllers might complain about the use of the Print and Close methods on a document object and the Close method on a documents collection. In these controllers, you need to wrap the member name in square brackets—for example, [Close] and [Print]—to make things work properly.

Property

Description

[read-only] BSTR FullName

The full pathname of the document.

[read-only] BSTR Name

The filename of the document, not including the path.

[read-only] BSTR Path

Same as FullName without Name.

[read-only] boolean ReadOnly

TRUE if the file is read-only; otherwise, FALSE.

[read-only] boolean Saved

TRUE if the document has not been changed since creation or loading; FALSE if the document is dirty.

boolean Interactive

Sets or returns whether the application accepts actions from the user regardless of visibility.

boolean Visible

Sets or returns whether the application is visible to the user.

long Left

Distance between the left edge of the parent window's client area and the left edge of the document window. Setting this property moves the window (as does setting Width, Top, and Height).

long Width

Horizontal width of the document window, including all borders.

long Top

Distance between the top of the parent window's client area and the top edge of the document window.

long Height

Vertical height of the document window, including borders, caption, and so on.

BSTR Author
BSTR Comments
BSTR Keywords
BSTR Subject
BSTR Title

The fields in document summary information.


Table 14-6.

Guidelines for document object properties.

Method

Description

void Activate(void)

Activates the first window associated with the document.

void Close([optional] boolean SaveChanges, [optional] BSTR File)

Closes all windows associated with the document and removes the document from the documents collection. SaveChanges indicates whether to save changes; File indicates the file in which to save those changes. File appears only if SaveChanges is TRUE.

void NewWindow(void)

Creates a new window for the document.

void Print([optional] short FromPage, [optional] short ToPage, [optional] short Copies)

Prints the document from the range of FromPage and ToPage. Copies specifies the number of copies to print. Can also be called PrintOut.

void PrintPreview(void)

Previews the pages and page breaks of the document. Equivalent to choosing Print Preview from the File menu.

void RevertToSaved(void)

Discards changes to the document and reloads it.

void Save(void)

Saves changes to the document under the FullName property.

void SaveAs(BSTR SaveFile)

Saves the document's contents to the file specified by SaveFile, which might or might not include a path.


Table 14-7.

Guidelines for document object methods.

Cosmo's Automation Hierarchy

The Cosmo example for this chapter (in the COSMO directory) is a sample implementation of the basic hierarchy described in the previous section. The ODL file describing these objects is found in COSMO\COSMO000.ODL.

Cosmo's application object supports the Application, Caption, FullName, Name, Left, Top, Width, Height, Visible, and StatusBar properties and the Quit method, as described in the guidelines. But Cosmo calls these properties ActiveFigure and Figure instead of ActiveDocument and Documents; this is more appropriate to the type of information in each document window.

Cosmo's figures collection is the same as the documents collection described earlier, with the Application, Parent, and Count properties and the Add, Open, Item, and _NewEnum methods.

Each figure in the collection supports most of the standard document methods and properties, with the exception of Interactive, Author, Comments, Keywords, Subject, Title, NewWindow, Print, and PrintPreview, mostly because Cosmo doesn't have summary information or printing capabilities. In addition, each figure supports a number of custom properties and methods, shown in Table 14-8, that express the specific functionality for which we're implementing OLE Automation in Cosmo in the first place. You can see from the contents of Table 14-8 that everything you can achieve through Cosmo's user interface is available through this automation interface.

Property/Method

Description

[read-only] short NumberOfPoints

The number of points in the figure.

long BackColor

The background color of the figure.

long LineColor

The line color of the figure.

short LineStyle

The style of line drawn in the figure (a Windows GDI pen style, PS_*, value).

boolean AddPoint(short x, short y)

Adds a point to the figure, where (x,y) is expressed on a (32,767, 32,767) grid. Equivalent to a mouse click in the figure window.

void RemovePoint(void)

Removes the last point added to the figure, equivalent to Cosmo's Undo command.


Table 14-8.

Cosmo's custom properties and methods on a figure object.

Deeper Automation Object Hierarchies

Most applications will probably have a deeper object hierarchy than the basic object hierarchy described in the previous sections. An example is illustrated in Figure 14-8 on the following page, in which each document itself has a collection of things (call them objects) and each object can have various rich properties that it makes sense to manipulate as separate objects as well. Rich properties are complex to the extent that they have many subproperties and even their own methods. (Usually they include only properties.) A font property of a title text object on a presentation slide is a good example—in fact, the OLE guidelines spell out some standards for font objects of this kind. A graphical object may have a complex palette object with subproperties.

Figure 14-8.

A deeper automation object hierarchy.

For complex applications such as Microsoft Word and Microsoft Excel, you can easily end up with several dozen or more different objects in the entire hierarchy. Word and Excel, for example, include automation interfaces to their custom macro (WordBasic and Visual Basic for Applications) and dialog box features, meaning that these applications provide quite a complex object hierarchy that includes individual controls that you might have in a custom dialog box. If you do not have such facilities, it is unnecessary to provide interfaces to control elements of predefined dialog boxes—you don't need to allow a user to write a script that would essentially describe placing this or that information in this or that dialog box control unless you wanted to make a really nice CBT system. But for the most part, your automation objects and interfaces should describe the user's intent of driving your application, not the details of each minute operation. This is why a documents collection object has an Open method instead of the application having a menu object with an Open method that shows the dialog box and gives you an IDispatch pointer through which you could get at each individual control and enter the pathname of the file and click OK. The latter would be a ludicrous number of steps just to open a file; the former expresses the user's real intent.

As you might expect, there is a lot of extra code involved in making such a rich set of objects, and the more your application's internal structure reflects that hierarchy, the easier it will be for you to add automation support. Cosmo, for example, had a frame-client-document structure that made it very easy and, in fact, noninvasive to add automation support. I implemented this support in two days. Yes, two days, and most of it worked the first time. (Most of what the OLE Automation interfaces did was call member functions of objects that already existed in the application and were already tested.) This is what I mean by noninvasive: I changed almost nothing in Cosmo's existing code—my automation work simply called the code the way an external client would, exactly what I was trying to achieve. The Automation layer is simply providing a standard programmatic way to access Cosmo's functionality.

In your own automation work, the time you spend making a good application architecture will make Automation a lot easier and a lot cleaner and your entire application faster. If you've been waiting for a reason to rework your application's architecture, now is the time. If you're creating a new application from scratch and plan to add support for Automation, be sure that accessibility through Automation is considered in your design from the very early phases of development.

One final note of caution: do not use Microsoft Excel 5 as a model of how to implement arguments to methods and properties. Excel 5, developed at the same time as OLE, uses VARIANTs for absolutely all arguments to properties and methods. A VARIANT is necessary only when you want to support an optional argument or an argument that can take a variety of types. Otherwise, use as specific a type as you see fit. Using VARIANTs everywhere will only add unnecessary code in every property or method implementation.