If you are planning to create a general purpose automation controller that allows a user to write a script to drive objects, you'll need code structures or other such commands to identify the following operations:
- Object creation: A way to identify an object class and create an instance of it. Ideally, the class identification is tied as closely as possible to the CLSID. Storing the name in the registry as the value of the CLSID is a good technique for doing this. Sometimes, however, these names are unwieldy, so another choice—albeit a less precise one—is to identify a class using the ProgID or VersionIndependentProgID. Another way to find a class is by specifying a filename that is associated with the class. In this case, the object creation sequence is really meant to get at the document object that has loaded the contents of that file.
As an example, Visual Basic has an instrinsic function, GetObject, that takes either a ProgID— as in "Cosmo.Figure"— or a filename such as "C:\INOLE\CHAP14\COSMO\TEST\SAMP1.COS" and creates the object. In both cases, the information given is mapped to a CLSID and is passed to CoCreateInstance; in the latter case, the controller should create a file moniker with the name and bind to it, asking for IDispatch (which will launch the server, load the file, and so on, as we saw in Chapter 9). It is nice that users don't need to see CLSIDs. If you provide a user interface through which the user can specify the object to create (see the next section), the user doesn't have to type in any code.
- Object destruction: A way for the user to say, "This object is no longer needed," which internally calls the object's Release function followed by CoFreeUnusedLibraries if wanted. In Visual Basic, the expression Set doc = nothing accomplishes this for the object referenced by the doc variable, as does letting a local variable go out of scope.
- Connect to an active object: Some function or command that ties into the GetActiveObject API function that we saw in Chapter 14. GetActiveObject itself works only on a CLSID, but that doesn't mean you are limited to working with running objects based only on classes. Because the active object capability in OLE uses the running object table, you can support connecting to objects of any kind as long as you can create a moniker for that object and check for that moniker in the running object table. In Visual Basic, for example, the GetObject command can take either a pathname to a file or a classname (the ProgID), the same as its CreateObject function. If you specify a ProgID, Visual Basic uses the GetActiveObject function. If you specify a filename, Visual Basic creates a file moniker from that name and binds that file moniker. If the document is already loaded somewhere, the binding process will find it in the running object table and connect to it immediately. Otherwise, binding launches the application that can load the document, just as an object creation process would do.
You do not have to stop with the idea of binding to a running document—if you want to support additional syntax, you can allow the user to create a composite moniker by specifying additional information, such as a filename and one or more item names. When you bind the moniker successfully, you'll get an interface pointer to a portion of the document. This works very well in deeper object hierarchies, as described in "Design of an Object Hierarchy" in Chapter 14.
- Set a property: This should happen whenever an Object::Property reference is on the left side of an expression, as in Cosmo.LineStyle = 3. How your language expresses the Object::Property relationship is up to you. Visual Basic uses the dot operator, as we've seen, but there's nothing sacred about it. You could use :: or -> or ( ) or [ ] or { } or whatever else you want, as long as it's consistent and fits the programming language of the controller. A controller can also support any number of array indices with a property set.
- Get a property: This should happen whenever an Object::Property reference is on the right side of an expression, as in curStyle = Cosmo.LineStyle. As with a property put, you have a choice over how the user expresses the Object::Property relationship, and you will probably want to include some sort of array index operator as well.
- Call a method with arguments (possibly optional and named): Method calls are always on the right side of an expression, which makes them similar to property get operations. This is why some languages, Basic being one of them, cannot differentiate between property get operations and method calls. In Visual Basic, for example, Object.Property has the same syntax as Object.Method when the method has no arguments and has a return value. In any case, a method is always on the right side of an expression even when used alone: Object.Method is the same as temp=Object.Method without requiring the temp variable.
Your language must allow the user to express optional arguments as well as named arguments. A language such as Basic is good with optional arguments: if they're in the script, send those values to the object; if not, send empty arguments. Other languages may be more precise about the specification of such options. As far as named arguments are concerned, you must allow the user to specify the name and assign some value to it, as in Name=value, Name:=value, Name:Value, or whatever syntax best suits your language.
- Event sinks: Assign script code to the events that an object might fire, using the object's type information to find outgoing interfaces and displaying the functions in those interfaces. The user can then specify actions to perform when those events occur through additional script or code.
- QueryInterface: Include a way for a user to write a script that exploits the idea of multiple interfaces on an object. As we saw in Chapter 14, an object can have multiple dispinterfaces as easily as it can have multiple vtable interfaces. Providing the capability for a script to create a QueryInterface call to an object is as important here for robust evolution of functionality as it is anywhere else in OLE. In short, the script should be able to test the existence of functionality before attempting to execute that functionality.
With some sort of syntactical convention for each of these eight operations, you'll allow a user to fully exploit an object's interfaces, incoming and outgoing, through IDispatch and even through custom interfaces. There is also the possibility of creating controllers that have a graphically based programming environment, but even these have some way of expressing these operations and object-to-object relationships.