Fundamental operations
No matter which server you re dealing with, there are certain operations you need: opening the server, closing the server, opening and closing a document, checking whether the server is open, checking whether you have a document to work with, and so forth. Methods and properties to handle these operations are added to cusWrapServer, though some of the methods are abstract because they require server-specific code. Table 1 shows the custom properties of cusWrapServer. Table 2 shows the custom methods of the class.
Table 1
. Properties of cusWrapServer. These properties are added to the abstract wrapper class. Property | Type | Description |
cServerName | Character | The name of the Automation server, for example, "Word.Application". |
lOpenAsNeeded | Logical | Indicates whether to open the server when a method is called that requires its use. If this is .F., some methods may fail. |
oDocument | Object | Object reference to the active document. "Document" here is used in the broad sense to refer to the primary type of object used by the application. |
oServer | Object | Object reference to the server. |
Table 2
. Methods of cusWrapServer. These custom methods are added to the abstract class so they re available to all wrapper classes. Many of them are abstract at this level; that is, the code to be executed must be specified in subclasses. Method | Description |
CloseDocument | Close a document of the primary type for the server. Abstract. |
CloseServer | Close the server instance. Abstract. |
HideServer | Make the server instance invisible. Abstract |
IsDocOpen | Indicates whether there s a document open. Abstract. |
IsServerOpen | Indicates whether there s an instance of the server available. Abstract. |
NewDocument | Creates a new document of the primary type for the server. Abstract. |
oDocument_Access | Returns a reference to the active document. |
OpenDocument | Opens a document of the primary type for the server. |
OpenServer | Opens an instance of the server. |
OpenServerDocument | Server-specific portion of OpenDocument. Handles server-specific details. Abstract. |
SaveDocument | Saves a document of the primary type for the server. |
ShowServer | Makes the server visible. Abstract. |
TellUser | Displays a message to the user. |
zReadme | Documentation method. |
Talking to the user
All communication with the user is sent through a single method, TellUser. This provides centralized message services for the wrapper class. In the version of cusWrapServer provided, TellUser is quite simple (and is really aimed more at the developers using the wrapper class than at end users). It uses either a WAIT WINDOW or a basic message box and doesn t accept any input. While this version is fine for simple testing, you ll probably want to replace this method with one that ties into the message services already in use in your applications.
* cusWrapServer.TellUser
* Pass a message along to the user. By sending all messages through this
* method, it's easy to change the mechanism used for messages.
* This is just a very basic message handler.
LPARAMETER cMessageText, lDontWait
IF SYS(2335) = 1 ; && Not in unattended mode
AND This.Application.StartMode = 0 && Normal VFP interactive session.
IF lDontWait
WAIT WINDOW cMessageText NOWAIT
ELSE
MessageBox( cMessageText )
ENDIF
ELSE && Can't generate a UI dialog
ERROR cMessageText && generates error 1098
* You may prefer to use COMReturnError() here instead.
ENDIF
RETURN
Because every method that communicates with the user calls on TellUser, it would be quite easy to substitute a more sophisticated messaging mechanism. One obvious change is to offer the option of "silent messaging," returning error codes rather than displaying messages.
Creating a server instance
Probably the hardest, most error-prone part of Automation is getting a server started. The server might be missing or not registered, or Windows may be unable to start it for some reason. To deal with all these possibilities, cusWrapServer uses a separate class whose sole purpose is to instantiate an Automation server. This class, called cusOpenServer, is also derived from Custom. Although the methods in cusOpenServer could be included in cusWrapServer, it makes sense to keep them in a separate class because many processes need to open an Automation server. Rather than forcing them all to load the entire abstract wrapper class, which has many methods unrelated to the task of opening a server, they can use just cusOpenServer.
cusOpenServer has two custom properties and two custom methods. The cServerName property holds the name of the server to be instantiated; the oServer property holds the reference to the newly-created server. The OpenServer method does the actual work of creating the server. The zReadMe method of the class is a documentation method (as in all the classes presented here).
The Init method of cusOpenServer accepts two parameters, the name of the server to instantiate and a logical value indicating whether to instantiate it right away or wait until OpenServer is called explicitly. It then uses the Registry class (provided as a VCX with VFP 6 and as a PRG with VFP 5) to make sure the server is registered. If not, Init returns .F., which prevents this object (cusOpenServer) from being created. Here s the code for cusOpenServer.Init:
* cusOpenServer.Init
* Check whether the specified server name is valid and,
* if the user specified, create the server
LPARAMETERS cServerName, lCreateAtInit
LOCAL oRegistry
IF VarType(cServerName) = "C"
* Check to see if this is a valid server name
* In development/testing, you may need to add the
* Registry.VCX from HOME()+"\FFC\" to your path
oRegistry = NewObject("Registry","Registry")
IF oRegistry.IsKey(cServerName)
This.cServerName = cServerName
ELSE
RETURN .F.
ENDIF
RELEASE oRegistry
ELSE
* No point in creating this object
RETURN .F.
ENDIF
IF VarType(lCreateAtInit) = "L" AND lCreateAtInit
This.OpenServer()
ENDIF
RETURN
The OpenServer method of cusOpenServer instantiates the server and preserves the reference. Here s the code:
* cusOpenServer.OpenServer
* Open the server specified by This.cServerName
* Store the new instance to This.oServer
IF VARTYPE(This.cServerName) = "C"
This.oServer = CreateObject(This.cServerName)
ENDIF
RETURN This.oServer
There are actually some other problems that can occur when you attempt to instantiate the server. They re discussed in Chapter 14, "Handling Automation Errors." In an attempt to keep this code easy to read, we haven t dealt with all of them here. If you use this class as a basis for your own wrapper class, be sure to integrate the code from Chapter 14 into this Init method.
The OpenServer method of cusWrapServer instantiates cusOpenServer, passing .F. for the lCreateAtInit parameter. If cusOpenServer is successfully instantiated, OpenServer retrieves the reference to the new server and stores it in the oServer property. If not, the user is informed that the server couldn t be started.
* cusWrapServer.OpenServer
* Create an instance of the Automation server
* and store a reference to it in This.oServer
LOCAL oGetServ
IF EMPTY(This.cServerName)
This.TellUser("No server specified")
RETURN .F.
ENDIF
oGetServ = NewObject("cusOpenServer","Automation","",This.cServerName,.F.)
IF VarType(oGetServ) = "O"
This.oServer = oGetServ.OpenServer()
IF VarType(This.oServer)<>"O"
This.TellUser("Can't create automation server")
RETURN .F.
ENDIF
ENDIF
RETURN
Closing the server
Unlike creating instances of servers, the method for shutting down the different servers varies. In particular, you need to take different actions to be sure that the user doesn t see a dialog when you attempt to close the server application. So, the CloseServer method is left empty (abstract) at the cusWrapServer level and must be coded in each concrete subclass.
However, we want to be sure not to leave server applications running when we shut down our wrapper class. So, the Destroy method contains this code:
* cusWrapServer.Destroy
* Close the server on the way out, if necessary,
* so we don't leave it running in the background.
IF This.IsServerOpen()
This.CloseServer()
ENDIF
RETURN
Opening a document
The term "document" in Windows is used to mean the primary type of file on which an application operates. In Excel, it s a workbook, and in PowerPoint, it s a presentation. Word confuses the issue, because its primary file type is also called a document. The remainder of this chapter uses the term "document" in the Windows sense, rather than meaning a Word file.
Much of what you need to do to open a document is the same regardless of the application. You need to make sure the user specified a document to open, make sure the document exists, and make sure the server is open or, if appropriate, open the server. However, the actual process of opening a document varies for the individual servers. The wrapper class divides the code into these two parts. The OpenDocument method does everything it can, then calls the OpenServerDocument method, which is coded at the subclass level. Here s the code for OpenDocument:
* cusWrapServer.OpenDocument
* Open a specified document and make it the active document.
* Return a logical value indicating whether the document
* was successfully opened.
LPARAMETER cDocument
* cDocument = name of document to be opened including FULL PATH
LOCAL lIsServerOpen, lSuccess
* Check whether parameter was passed and is valid
IF VarType(cDocument)<>"C"
This.TellUser("Must specify document to open")
RETURN .F.
ENDIF
IF NOT FILE(cDocument)
This.TellUser("Can't find " + cDocument)
RETURN .F.
ENDIF
* Make sure server is open
lIsServerOpen = This.IsServerOpen()
DO CASE
CASE NOT lIsServerOpen AND NOT This.lOpenAsNeeded
This.TellUser("Program is not available")
RETURN .F.
CASE NOT lIsServerOpen
* Open server
This.OpenServer()
OTHERWISE
* Server is open and all is well
ENDCASE
* Attempt to open document
* Call server specified method
lSuccess = This.OpenServerDocument( cDocument )
RETURN lSuccess
Saving a document
The techniques for saving documents are similar enough in the Office servers that the SaveDocument method can be defined in the abstract class. This method accepts two parameters, an object reference to the document to be saved and the filename, including the full path, where it s to be saved. Both parameters are optional. If the document reference is omitted, the active document is saved. If the filename is omitted, the file is re-saved to its current location. As written, the method overwrites existing files without informing the user.
* cusWrapServer.SaveDocument
* Save the specified document. If a filename is passed,
* save it with that filename. Otherwise, use the name it
* already has. If it's a new document and no name is
* specified, fail.
* DOES NOT CHECK FOR OVERWRITE OF FILE
* If no document is passed, use the active document.
LPARAMETERS oDocument, cFileName
LOCAL lReturn, cOldCentury
IF VarType(oDocument) <> "O"
oDocument = This.oDocument
ENDIF
DO CASE
CASE IsNull(oDocument)
* No document to save
* Tell the user and get out
This.TellUser("No document to save.")
lReturn = .F.
CASE VarType(cFileName) = "C"
* Just save it with the specified name
lReturn = .T.
oDocument.SaveAs( cFileName )
tFDateTime = This.GetFileDateTime( cFileName)
IF NOT FILE(cFileName) OR NOT (tFDateTime + 1 > DATETIME())
* If file doesn't exist or wasn't updated in the last second
This.TellUser("Couldn't save document as " + cFileName)
lReturn = .F.
ENDIF
CASE VarType(cFileName) = "L" AND NOT (oDocument.Name == oDocument.FullName)
* No name, but previously saved, so save with current name
lReturn = .T.
oDocument.Save()
cFileName = oDocument.FullName
tFDateTime = This.GetFileDateTime( cFileName)
IF NOT (tFDateTime + 1 > DATETIME())
* If file wasn't updated in the last second
This.TellUser("Couldn't save document")
lReturn = .F.
ENDIF
OTHERWISE
This.TellUser("Specify filename")
lReturn = .F.
ENDCASE
RETURN lReturn
This method works for Word and PowerPoint. Excel doesn t let you use SaveAs to overwrite an existing file without prompting the user, so the Excel-specific subclass contains its own version of SaveDocument that works around this annoyance.
The GetFileDateTime method used here returns the time the file was last saved as a datetime value. We were surprised to have to resort to this round-about method of checking for a successful save, but surprisingly, the Save and SaveAs methods don t return a value to indicate whether they were able to save the file. In a network situation, this test can fail even though the file was saved if the clock on the server is out of synch with the clock on the local machine.
Other basic operations
Although there are other methods that are clearly needed for every server, such as printing or closing the server application, they have to be defined in subclasses because they depend on specific properties and methods of the servers, or the way of doing them is too different from one server to the next.
Copyright 2000 by Tamar E. Granor and Della Martin All Rights Reserved