Managing Object Lifetimes in OLE Automation

Douglas Hodges

Created: August 25, 1994
Revised: January 20, 1995

Abstract

The techniques and lessons that have been learned in dealing with Compound Document scenarios can be applied to the scenarios involving OLE Automation. See the document "Managing Object Lifetimes in OLE" by Douglas Hodges for a thorough discussion of managing object lifetimes involving Compound Document scenarios.

Managing the Root of an OLE Automation Object Model

The root objects exposed through an OLE Automation Object Model must deal with the same object life-time complexities as the root document object in a Compound Document linking scenario. These objects will typically need to deal with a combination of weak and strong references. In addition, these objects will need to deal with a combination of both internally and externally generated strong references. Thus, in order to properly handle all shutdown scenarios, these objects will need to manage the weak and strong references on their own. The root objects exposed through OLE Automation will need to:

Types of Applications

There are three basic types of applications:

Exposing an Object Model for an MDI Application (and Also for an Old-Style SDI Application)

An MDI application exposes two root-level objects that need this special handling: an application object and a document object. These two objects are clearly distinct abstractions in an MDI application. An MDI application is required to have a one CLSID/ProgID to identify the class of its application object and a different CLSID/ProgID to identify the class of its document object. For example, Microsoft® Excel 5.0 exposes the following application object class:

CLSID
{00020841-0000-0000-C000-000000000046} = Microsoft Excel 5.0 Application
ProgID = Excel.Application.5

Microsoft Excel 5.0 also exposes the following two document object classes:

CLSID
{00020810-0000-0000-C000-000000000046} = Microsoft Excel 5.0 Worksheet
ProgID = Excel.Sheet.5
{00020811-0000-0000-C000-000000000046} = Microsoft Excel 5.0 Chart
ProgID = Excel.Chart.5

Using these CLSIDs, an automation driver can launch and connect to the MDI application or directly instantiate a document object without directly dealing with the application object. In order to enable an automation driver to connect to a running instance of a document object, the automation application registers its document object in the ROT, typically with a FileMoniker. In order to allow automation drivers to connect to the currently running instance of the application object, the automation application registers its application object using the RegisterActiveObject API. This API actually registers the application object in the ROT using the application's CLSID as the Moniker.

The most complex part of dealing with an MDI application and OLE Automation is the combination of user-created documents and programmatically created documents and deciding whether the user is in control of the application. Ideally, if the application is launched by OLE Automation, used invisibly, and then released, the application should shut down. On the other hand, if the application is initially launched by OLE Automation and then made visible, it is not completely clear whether the application should shut down. It should be possible to programmatically launch the application, create a document, make it visible, and then have the document continue to run after the automation script terminates. It should also be possible to programmatically launch the application, create a document, make it visible, close the document, and then have the application shut down.

The ideal goal is that if two automation drivers are executing against the same OLE Automation object, they should not interfere with each other with regard to object lifetime issues. For example, Driver 1 should be able to launch the automation object application and start executing. Meanwhile, Driver 2 should be able to connect to the same object. Then when Driver 1 terminates and releases the automation object, Driver 2 should successfully continue running. Later, when Driver 2 terminates and releases the object, the automation application should shut down.

In order to give the programmer using Visual Basic® for Applications complete control over the shutdown of the MDI application, the following methods and properties are needed on the application object and document object:

Document.Visible Property

This property controls whether the document is visible. If Visible = TRUE, then the document is made visible; otherwise the document is hidden. If the document is left in the visible state when all programmatic references are released, the document will remain running and visible. If the document is hidden when the last programmatic reference is released, the document will close. If there are unsaved changes when the hidden document is closing after its final release, these changes are thrown away without prompting the user. The programmer using Visual Basic for Applications should explicitly handle saving the document by either calling Save/SaveAs or by explicitly closing the document with the Close(saveChanges, fileName) method. When the document is being made visible, the application MDI frame window is also made visible if it is not already so. The Application.UserControl property is not automatically set when making a document visible; it must be explicitly set by the programmer. When the document is being hidden, the application MDI frame window will also be hidden unless there is another visible document open or the user is in control of the application (that is, Application.UserControl = TRUE).

Document.Close(saveChanges, fileName)

Forces the document object to close. The parameters control whether unsaved changes are saved or discarded. This method will forcibly break any programmatic connections to the document. If instead the programmer wants to take the document away visibly from the user (from the user's point of view the document is closed) but not break programmatic references to the document, the Document.Visible property should be set to FALSE instead.

Application.Visible Property

This property controls whether the application MDI frame window is visible. If Visible = TRUE, the application MDI frame window is made visible; otherwise the application is hidden. Setting the Visible property does not automatically set the UserControl property. The Visible property can only be set to FALSE if the Application.UserControl property is FALSE and there are no visible documents open; if the user is in control of the application or there are visible documents open, setting the application Visible property to FALSE has no effect. The programmer must either explicitly hide/close each document individually or use the Application.Quit() method.

Another option, instead of having the VBA programmer control visibility with a Visible property, is to use the next three Show and Hide methods:

Document.Visible Readonly Property

This property indicates whether the document is currently visible. If Visible = TRUE, the document is visible; otherwise the document is hidden. The property is read only. The Show() method should be used to make a document visible. The Hide() method should be used to make a document invisible.

Document.Show(Boolean GiveUserControlOfApp)

This method makes a document object become visible to the user. This will also make the application MDI frame window visible too, if it is not already so. When making the document object visible, the programmer must decide whether to give the user control over the lifetime of the application. If the GiveUserControlOfApp parameter is TRUE, the application will not shut down after the last document is closed; it will remain running and visible, under the control of the user. If the GiveUserControlOfApp parameter is FALSE and no other document has given the user control of the application, the application will shut down after the last document is closed.

Setting the GiveUserControlOfApp parameter to TRUE is used when the programmer wants to programmatically bring up a document just as if it had been done by the user via the File.New or File.Open commands. On the other hand, GiveUserControlOfApp = FALSE should be used when the programmer wants to bring up a document, make it visible, use it programmatically, later hide and release it, and then have the application shut down as long as there is no other reason to stay running (in particular, if the user has not opened another document). Even if the document is already visible, the GiveUserControlOfApp parameter will still take effect to give the user control over the application if necessary.

Document.Hide()

This method makes a document object become invisible to the user. If the document is already hidden, this method has no effect. When a document is hidden it will automatically close when the last programmatic reference to it releases. The programmer must take care to save the document if there are unsaved changes. If there are unsaved changes after the last connection to the hidden document is released, the document will close without saving; the invisible document will not prompt the user to save changes. If the application is not under the control of the user, the application will shut down after the last document is closed.]

Application.UserControl Property

This property indicates whether the user has control over the application. If UserControl = TRUE, the application will remain running and visible after the last document is closed; if UserControl = FALSE, the application will hide after the last document is closed. If there are no documents existing and the application is hidden when the last programmatic reference to any object in the application is released, the application will shut down. If the user launches the application from the Program Manger or the user creates or opens a document with the File.New or File.Open commands, the Application.UserControl property is set to TRUE. If a document is created programmatically, the programmer must explicitly set the Application.UserControl property when making the document visible, depending on whether the programmer wants the application to automatically shut down when the document is closed.

Application.Quit()

Takes control of the application away from the user. This method has the same effect as the user issuing a File.Exit command from the menu. All visible documents will be closed. If there are visible documents open with unsaved changes, the user is prompted to save each document individually. The application will be hidden from the user and the UserControl property set to FALSE. If there are no more invisible documents being used programmatically (for example, an object in-place active in another container), the application itself will shut down. Otherwise the application will remain running invisibly until these final invisible documents are released.

Exposing an Object Model for a New-Style SDI Application

The document and the application of a new style SDI application are presented as a single abstraction in the Object Model. The user sees these as indistinguishable, just as the user sees only one window for the document. An SDI application does not register any object using the RegisterActiveObject API. Also, an SDI application does not have to deal with the complexity of deciding whether the user is in control of the application. The shutdown logic only has to deal with the document object. If the document is left visible after being launched by OLE Automation, the document remains running under the control of the user. If the document object is invisible at the time the last programmatic reference releases, the document object will shut down. This is managed by maintaining weak and strong references on the document and by treating the fact that the document is visible as a strong reference on behalf of the user.

In order to give the programmer complete control over the shutdown of the SDI application, the following methods and properties are needed on the document object:

Document.Close(saveChanges, fileName)

Forces the document object to close. The parameters control whether unsaved changes are saved or discarded. This method will forcibly break any programmatic connections to the document. If instead the programmer wants to take the document away visibly from the user (from the user's point of view the document is closed) but not break programmatic references to the document, the document should instead be hidden by setting the Document.Visible property to FALSE.

Document.Visible Property

This property indicates whether the document is currently visible. If Visible = TRUE, the document is visible; otherwise the document is hidden. For an SDI document object this property can be read/write. It is not required to use a Show() method to make a document visible because it is not necessary to deal with user control of the application. It would be perfectly reasonable to still have Show() and Hide() methods on the document in addition to this property.

Exposing Sub-Objects Through OLE Automation

The same strategy that is used for managing pseudo-objects should be applied to the subobjects exposed through an OLE Automation object model. Objects that are exposed as part of an Object Model to OLE Automation implement an interface called IDispatch. Through methods on the IDispatch interface, an OLE Automation driver can invoke methods and get/set properties of the object. The IDispatch implementation as well as any dual interfaces and dispinterfaces (see the OLE 2 Programmer's Reference, Vol. 2 for more information on OLE Automation) exposed by a subobject should be organized in a COM object that treats all of its references as strong. Whenever there is a reference to the Automation subobject, the subobject should hold a lock on its parent object.

OLE Automation Creation Scenarios

Instantiating an MDI Application object with CreateObject Statement

Visual Basic for Applications code:

Dim x as Object
Set x = CreateObject("Excel.Application")
' do some actions
' Release the object (or let it fall out of scope)
Set x = Nothing

The above code fragment results in the following OLE API calls:

ClassIDFromProgID("Excel.Application", &clsid)
CoCreateInstance(clsid,...)

The call to CoCreateInstance causes the Microsoft Excel application to be launched with "/Automation -Embedding" on the command line. (The "-Embedding" switch is added by the OLE libraries as part of launching a LocalServer application by CoCreateInstance. The "/Automation" switch is added by Microsoft Excel itself by the standard conventions of OLE Automation. It is included as part of the "LocalServer = c:\excel\excel /Automation" registration key in the CLSID section of the REGDB for the application.) As part of its launch sequence, Excel must register its application class object before yielding. The application class object will be registered as REGCLS_SINGLEUSE with the CoRegisterClassObject API. (Microsoft Excel also registers class objects for its Excel.Sheet and Excel.Chart Document classes. These classes are registered as REGCLS_MULTIUSE.) OLE will connect to the application class object and call IClassFactory::CreateInstance. Microsoft Excel will then return the object that exposes its application level IDispatch interface. Microsoft Excel will also register its application object in the ROT by calling the RegisterActiveObject API. It is important that this registration is made as a weak registration by passing the REGOBJ_TABLEWEAK flag.

Note   By default the RegisterActiveObject API registers the object with a strong reference. In 32-bit OLE the REGOBJ_TABLEWEAK flag has been defined to allow the registration to be weak. (See the 32-bit OLE 2 Programmer's Reference.). In 16-bit OLE it would be necessary to organize a separate COM identity for the object passed to RegisterActiveObject in order to deal with the fact that the registration is strong.

The following picture shows the state of the Microsoft Excel application after the launch sequence is complete:

In this scenario, the instance of Microsoft Excel started by the CreateObject call should shut down when the single strong reference to the application object is released. The "Set app = Nothing" line causes the external reference to the application object to be released. If the Microsoft Excel application does not shut down, this results in an orphaned instance of Microsoft Excel running invisibly. The only way to get rid of this instance is to reboot or use a process killer application (not something most end-users are familiar with).

Instantiating an MDI Document Object with CreateObject Statement

Visual Basic for Applications code:

Dim x as Object
Set x = CreateObject("Excel.Sheet")
' do some actions
' Release the object (or let it fall out of scope)
Set x = Nothing

The above code fragment results in the following OLE API calls:

ClassIDFromProgID("Excel.Sheet", &clsid)
CoCreateInstance(clsid,...)

The call to CoCreateInstance causes the Microsoft Excel application to be launched with " -Embedding" on the command line. As part of its launch sequence, Microsoft Excel must register its Sheet Document class object before yielding. This class object will be registered as REGCLS_MULTIUSE with the CoRegisterClassObject API because Microsoft Excel is an MDI application. Microsoft Excel also registers its Chart class object. Microsoft Excel, however, will not register its application class in this case. OLE will connect to the Sheet Document class object and call IClassFactory::CreateInstance. Microsoft Excel will then return the object that exposes its Sheet Document level IDispatch interface. Microsoft Excel will also register its Sheet object in the ROT by calling IRunningObjectTable::Register.

The following picture shows the state of the Microsoft Excel application after the launch sequence is complete:

In this scenario the instance of Microsoft Excel started by the CreateObject call should shut down when the single strong reference to the Sheet Document object is released. The Sheet Document should hold a strong reference on the application. When the strong reference on the Sheet object is released, the Sheet will execute its Close method. This will then lead to the Sheet being destroyed. In its destructor, the document will release its lock on the application object. This will then cause the Microsoft Excel application to shut down.

Instantiating an MDI Application Object with New Declaration

Visual Basic for Applications code:

Dim x as New Excel.Application
 ' do some actions
' Release the object (or let it fall out of scope)
Set x = Nothing

The above code fragment results in the following OLE API calls:

ClassIDFromProgID("Excel.Application", &clsid)
CoCreateInstance(clsid, ...)

In this scenario a new instance of the application will always be launched even if the application is already running. This method of instantiating an object with the "New" syntax is functionally equivalent to the CreateObject statement.

Connecting to an MDI Application Object with GetObject(, "progID") Statement

Visual Basic for Applications code:

Dim x as Object
Set x = GetObject(, "Excel.Application")
' do some actions
' Release the object (or let it fall out of scope)
Set x = Nothing

The above code fragment results in the following OLE API calls:

ClassIDFromProgID("Excel.Application", &clsid)
GetActiveObject(clsid)

The GetActiveObject API looks for an application object registered with clsid in the RunningObjectTable. The RegisterActiveObject API is used to register an application object in the ROT. Note that in this scenario the Visual Basic for Applications expression GetObject(, "Excel.Application") will fail unless the Microsoft Excel application is already running and registered in the ROT. If the Microsoft Excel application is not already running, a new instance will not be launched.

Instantiating an MDI Application Object with GetObject("", "progID") Statement

Visual Basic for Applications code:

Dim x as Object
Set x = GetObject("", "Excel.Application")
' do some actions
' Release the object (or let it fall out of scope)
Set x = Nothing

The above code fragment results in the following OLE API calls:

ClassIDFromProgID("Excel.Application", &clsid)
CoCreateInstance(clsid, ...)

This variation of the GetObject syntax varies from the previous scenario in that a new instance of the application will always be launched even if the application is already running. This variation is equivalent to a CreateObject statement.

Instantiating an Document Object with GetObject("filename") Statement

Visual Basic for Applications code:

Dim x as Object
Set x = GetObject("c:\foo.xls")
' do some actions
' Release the object (or let it fall out of scope)
Set x = Nothing

The above code fragment results in the following OLE API calls:

MkParseDisplayName("c:\foo.xls", &mk)
mk.BindToObject()

This variation of the GetObject call uses moniker binding to connect to the object. Typically the argument passed to GetObject will be a filename and the MkParseDisplayName API will return a FileMoniker. If the document object is already running, the running instance of the document will be located via the ROT. If the document object is not already running, a new instance of the object's server application will be launched, and the application will be told to open the corresponding file. To support this scenario, the object server application must register the document in the ROT with a FileMoniker and must implement an IPersistFile interface.

Instantiating an Document Object with GetObject("filename", "progID") Statement

Visual Basic for Applications code:

Dim x as Object
Set x = GetObject("c:\foo.xls", "Excel.Sheet")
' do some actions
' Release the object (or let it fall out of scope)
Set x = Nothing

The above code fragment results in the following OLE API calls:

ClassIDFromProgID("Excel.Application", &clsid)
CoCreateInstance(clsid, ...)
QueryInterface(IID_IPersistFile, &pObj)
pObj->Load("c:\foo.xls")

This variation of the GetObject syntax varies from the previous scenario in that a new instance of the application will always be launched even if the document is already open in a running instance of the application. In this case Moniker binding is not used, and the ROT is ignored. The IPersistFile interface is still used to command the application to open the file.

Navigating to a Sub-Object Through OLE Automation

Dim app as Object, wb as Object, ws as Object
Set app = CreateObject("Excel.Application")
Set wb = app.Workbooks.Add
Set ws = wb.Worksheets(1)
Set app = Nothing
' This call should work even though app object is released
wb.Worksheets(1).Cells(1, 1).Value = 10
Set wb = Nothing
' This call should work even though wb and app object are released
ws.Cells(2, 2).Value = 20

In this scenario the Microsoft Excel application is explicitly launched and then a Workbook is created. The application object is released when Set app = Nothing, but the Workbook reference (wb) and the Worksheet reference (ws) remain. The wb and ws references should remain valid and the application should not shut down because the Workbook should hold a lock on the application. Even after the wb reference is released, the application should remain running with the ws reference still valid. The Worksheet is a sub-object (or pseudo-object) of the Workbook. The programmatic reference (from OLE Automation via ws) to the Worksheet should cause the Worksheet to hold a lock on the Workbook, which holds a lock on the application. Finally, when the ws reference goes out of scope, everything should shut down.

Key OLE Automation Scenarios

Scenario 1

Initial State:

Word application not running

VBA Code:

Dim app as Object
Set app = CreateObject("Word.Application")
' Release the object (or let it fall out of scope)
Set app = Nothing

Result:

Scenario 2

Initial State:

Word application running

Visual Basic for Applications Code::

Dim app as Object
Set app = CreateObject("Word.Application")
' Release the object (or let it fall out of scope)
Set app = Nothing

Result:

Scenario 3

Initial State:

Word application not running

Visual Basic for Applications Code:

Dim app as Object
Set app = CreateObject("Word.Application")
' Make app visible but do not set UserControl
app.Visible = True
' Release the object (or let it fall out of scope)
Set app = Nothing

Result:

Scenario 4

Initial State:

Word application not running

Visual Basic for Applications Code::

Dim app as Object
Set app = CreateObject("Word.Application")
' Make app visible and set UserControl
app.Visible = True
app.UserControl = True
' Release the object (or let it fall out of scope)
Set app = Nothing

Result:

Scenario 5

Initial State:

Word application not running

Visual Basic for Applications Code:

Dim app as Object
Dim doc as Object
Set app = CreateObject("Word.Application")
' Create a new Document object
Set doc = app.Documents.Add(Visible=FALSE)
' Release the objects (or let it fall out of scope)
Set doc = Nothing
Set app = Nothing

Result:

Scenario 6

Initial State:

Word application not running

Visual Basic for Applications Code:

Dim app as Object
Dim doc as Object
Set app = CreateObject("Word.Application")
' Create a new Document object
Set doc = app.Documents.Add(Visible=False)
' Make doc visible and set app.UserControl
doc.Visible = True
app.UserControl = True
' Release the objects (or let it fall out of scope)
Set doc = Nothing
Set app = Nothing

Result:

Scenario 7

Initial State:

Word application not running

Visual Basic for Applications Code:

Dim app as Object
Dim doc as Object
Set app = CreateObject("Word.Application")
' Create a new Document object
Set doc = app.NewDocument()
' Make doc visible but do not set app.UserControl
doc.Visible = True
'<@> Manipulate doc here...
' Make doc invisible again
doc.Visible = False
' Release the objects (or let it fall out of scope)
Set doc = Nothing
Set app = Nothing

Result: