In the class defined below I'm using a public property to hold an Excel.Application
object instance (this is my attempt at a singleton class).
Whenever the Finalize
routine runs and calls RestoreApp()
, it throws an InvalidComObjectException
(pasted below) on the first attempt to access a member of that Application
object.
I wrote this simple test routine to try to figure out what was gong on, and I verified I can access this property before the Finalize
method is called, but I always get the InvalidComObjectException
when the RetoreApp()
method tries to access that same property.
The MSDN reference for Object Lifetime says:
Before releasing objects, the CLR automatically calls the Finalize method for objects that define a Sub Finalize procedure. The Finalize method can contain code that needs to execute just before an object is destroyed...
However, maybe COM objects are a special case where the object is partially destroyed (RCW disconnected form the underlying COM object) before the Finalize method is ran? I don't know - that's just what seems to be happening.
Could anyone direct me to how to ensure the ResoreApp()
procedure is ran before my object disconnects from the COM object?
The Test:
Sub Main()
Dim ExcelTest As New MyExcel
Debug.Print(ExcelTest.Excel Is Nothing)
Debug.Print(ExcelTest.Excel.DisplayStatusBar)
End Sub
The Exception:
{"COM object that has been separated from its underlying RCW cannot be used."} _className: Nothing
_COMPlusExceptionCode: -532462766
_data: Nothing
_dynamicMethods: Nothing
_exceptionMethod: Nothing
_exceptionMethodString: Nothing
_helpURL: Nothing
_HResult: -2146233049
_innerException: Nothing
_ipForWatsonBuckets: 0
_message: "COM object that has been separated from its underlying RCW cannot be used."
_remoteStackIndex: 0
_remoteStackTraceString: Nothing
_safeSerializationManager: {System.Runtime.Serialization.SafeSerializationManager}
_source: Nothing
_stackTrace: {SByte()}
_stackTraceString: Nothing
_watsonBuckets: Nothing
_xcode: -532462766
_xptrs: 0
Data: {System.Collections.ListDictionaryInternal}
HelpLink: Nothing
HResult: -2146233049
InnerException: Nothing
IPForWatsonBuckets: 0
IsTransient: False
Message: "COM object that has been separated from its underlying RCW cannot be used."
RemoteStackTrace: Nothing
s_EDILock: {Object}
Source: "mscorlib"
StackTrace: " at System.StubHelpers.StubHelpers.StubRegisterRCW(Object pThis) at
Microsoft.Office.Interop.Excel.ApplicationClass.get_DisplayStatusBar()"
TargetSite: {Void StubRegisterRCW(System.Object)}
WatsonBuckets: Nothing
The Class:
Imports Microsoft.Office
Imports Microsoft.Office.Interop.Excel
Imports System.Windows.Forms
Public Class MyExcel
Dim CreateNew As Boolean
Dim Prepped As Boolean
Dim Prepping As Boolean
Dim pExcel As Microsoft.Office.Interop.Excel.Application
Dim pStatBar As Boolean
Dim pScreenUpdates As Boolean
Dim pEventsEnabled As Boolean
Dim pAlerts As Boolean
Private Property ScreenUpdates As Boolean
Get
Return pScreenUpdates
End Get
Set(value As Boolean)
pScreenUpdates = value
End Set
End Property
Private Property EventsEnabled As Boolean
Get
Return pEventsEnabled
End Get
Set(value As Boolean)
pEventsEnabled = value
End Set
End Property
Private Property StatBar As Boolean
Get
Return pStatBar
End Get
Set(value As Boolean)
pStatBar = value
End Set
End Property
Private Property AlertsDisplay As Boolean
Get
Return pAlerts
End Get
Set(value As Boolean)
pAlerts = value
End Set
End Property
Public ReadOnly Property Excel() As Microsoft.Office.Interop.Excel.Application
Get
Try
If Not CreateNew Then
If pExcel Is Nothing Then pExcel = GetObject([Class]:="Excel.Application")
End If
If pExcel Is Nothing Then pExcel = CreateObject("Excel.Application")
Catch ex As Exception
MessageBox.Show(ex.ToString)
End Try
Return pExcel
End Get
End Property
Public Sub New(Optional ByVal NewExcel As Boolean = True)
CreateNew = NewExcel
PrepApp()
End Sub
Private Sub PrepApp()
'Set prepping to true to avoid creating a stack overflow
Try
With Me.Excel
ScreenUpdates = .ScreenUpdating
EventsEnabled = .EnableEvents
StatBar = .DisplayStatusBar
AlertsDisplay = .DisplayAlerts
.DisplayStatusBar = True
.ScreenUpdating = False
.EnableEvents = False
.DisplayAlerts = False
End With
Catch ex As Exception
MessageBox.Show(ex.ToString)
End Try
End Sub
Private Sub RestoreApp()
'Restores the original values for screen updating, event triggers, and status bar display
Try
With Me.Excel
.DisplayStatusBar = StatBar
.ScreenUpdating = ScreenUpdates
.EnableEvents = EventsEnabled
.DisplayAlerts = AlertsDisplay
End With
Catch ex As Exception
MessageBox.Show(ex.ToString)
End Try
End Sub
Protected Overrides Sub Finalize()
RestoreApp()
MyBase.Finalize()
End Sub
End Class
The VB.Net finalizer is undeterministic and therfore doesn't allow a safe access to a COM created member. You could use a deterministic disposer by implementing the IDisposable interface: https://msdn.microsoft.com/en-us/library/s9bwddyx(v=vs.90).aspx
Sub Main()
Dim xl As New MyExcel
...
xl.Dispose()
End Sub
Another way would be to work with a new COM instance in the finalizer to retore your settings:
Private Sub RestoreApp()
Dim xl As Microsoft.Office.Interop.Excel.Application = pExcel
If Me.CreateNew Then
xl = CreateObject("Excel.Application")
Else
xl = GetObject(, "Excel.Application")
End If
xl.DisplayStatusBar = StatBar
xl.ScreenUpdating = ScreenUpdates
xl.EnableEvents = EventsEnabled
xl.DisplayAlerts = AlertsDisplay
xl.Quit()
System.Runtime.InteropServices.Marshal.ReleaseComObject(xl)
End Sub