Search code examples
vb.netexcel-interopfinalize

Run a routine on COM object before finalize?


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

Solution

  • 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