Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

"TypeOf...Is Child" from Parent causes broken Excel file

Tags:

excel

vba

I've been tracking down this issue for several days, so I thought I'd post it here to help others with the same problem, as well as learn more about the cause. I've simplified the problem code to the two class modules at the end of this post.

Basically, the simplified scenario is this: Two class modules, Parent and Child, where Child implements Parent. Somewhere in Parent is the line TypeOf Me Is Child, where Me could be any Object.

From my understanding, when the TypeOf...Is line is compiled to P-code (Debug > Compile, or calling the method) and saved to the file (.xlsm or .xlsb), it causes the file to not open properly. The code will run fine, but when the file is saved, closed, and reopened, it gives an error upon opening (or opening the VBE) saying either Invalid data format or Error accessing file. Network connection may have been lost, and the Parent module can no longer be opened, nor can any VBA be run (try ?1=1 in the Immediate Window and it gives the same error).

If the type is checked using TypeName() instead of TypeOf...Is, this issue does not appear (which is the solution I've used in my project).

Can anyone shed some more light on what exactly is going wrong here, or at least confirm I'm on the right track in terms of what's causing the problem (the P-code)?

PS Yes, I'm aware the parent having knowledge of the child is poor design, but I was near the end of one-off project which wasn't worth taking the time to redesign.

Useful links:

  • Explains VBA's code states, and what is saved to the file. http://orlando.mvps.org/VBADecompilerMore.asp?IdC=OrlMoreWin#WhatItIs

Class Modules:

Parent:

Option Explicit
' Class: Parent

' The problem (so far as I can tell):
'   When the compiled version of the method below is saved to the file, the file
'   will no longer load properly. Upon saving and reopening the file, I get a
'   "Invalid data format" error, and the code for this class module can no longer be
'   accessed. Furthermore, no VBA code will run after this happens. Try typing "?1=1"
'   into the Immediate Window - you'll get another "Invalid data format" window.
'   Alternatively, the error will be "Error accessing file. Network connection may
'   have been lost." if the code is changed from using "Me" to "tmp" as noted in the
'   comments in DoSomething().

' Steps to replicate:
'   1. Debug > Compile VBAProject.
'   2. Save file.
'   3. Close Excel.
'   4. Reopen file (and may need to open VBE).

Public Sub DoSomething()
    ' The TypeOf...Is statement seems to be what causes the problem.
    ' Note that checking "Me" isn't the cause of the problem (merely makes
    '   for shorter demo code); making a "Dim tmp as Object; set tmp = new Collection"
    '   and checking "TypeOf tmp Is Child" will cause the same problem.
    ' Also note, changing this to use TypeName() resolves the issue.
    ' Another note, moving the TypeOf...Is to a "Private Sub DoSomethingElse()" has
    '   no effect on the issue. Moving it to a new, unrelated class, however, does
    '   not cause the issue to occur.
    If TypeOf Me Is Child Then
        Debug.Print "Parent"
    End If
End Sub

Child:

Option Explicit
' Class: Child

Implements Parent

Private Sub Parent_DoSomething()
    Debug.Print "Child"
End Sub
like image 378
aplum Avatar asked Oct 21 '22 21:10

aplum


1 Answers

IMPLEMENTS statement causes circular dependency

The issue is not the TypeOf statement per se. The issue is that you have set up a circular dependency that VBA cannot resolve. As user2140173 mentioned VBA does not truly implement polymorphism. The circular reference you have created is that the definition of your interface "Parent" includes (requires existence of) your object "Child" and the definition of your class "Child" implements (requires existence of) "Parent". Therefore VBA cannot properly create the interface at compile time and the interface class becomes corrupted and inaccessible next time you have saved, closed and re-opened the workbook and VB Editor.

The OP could be misinterpreted as implicating the statement TypeOf .. Is as being somehow to blame. However, the TypeOf statement is not special. Any statement at all in the interface class that references a class that itself IMPLEMENTS the interface class will set up the circular dependency problem. For example:

Person.cs

'Class Person
Option explicit

Public Sub SaySomething()
   Dim B as Boy            '<--- here we cause the problem!
End sub 

Boy.cs

'Class Boy
Option explicit

Implements Person

Private Sub Person_SaySomething()
   Debug.Print "Hello"
End sub

So i hope you can see that Boy.cs Implements Person.cs which contains a Boy.cs which Implements a Person.cs which contains a Boy.cs .... VBA goes crazy at this point :)

It's a little unfortunate that the VB Editor doesn't give a more helpful error message than the "Invalid data format" error or the "Error accessing file. Network connection may have been lost." which leaves the User baffled!

The solution is to remove these statement(s) from the source code of the interface class. If this proves difficult to do because you have a lot of business logic actually written in the interface class then a useful approach is to move the business logic out to a separate class. Simply doing this on its own can resolve the compile problem with the Interface and get your code running again. In my own experience, for this very reason I deliberately try to remove any business logic from the interface class to ensure this kind of error cannot occur, and the interface classes become extremely simple - just a list of method signatures. If there is common business logic that I don't want to have to duplicate in each of the classes that will IMPLEMENT my interface then I create an additional class to hold this common business logic and ensure that the interface requires this class to exist. For example:

iMusicalInstrument.cs

'iMusicalInstrument interface
Option Explicit
Property Get Common() as csMusicalInstrumentCommon
End Property

csMusicalInstrumentCommon.cs

'MusicalInstrumentCommon class
Option Explicit
' add any methods you want to be available to all implementers of the interface.
Property Get GUID() as string        '<-- just an example, could be any method
     GUID = 'function to create a GUID
End Property

csTrumpet.cs

' csTrumpet class
Option Explicit
Implements iMusicalInstrument
Private mCommon As csMusicalInstrumentCommon
Private Sub Class_Initialize()
    Set mCommon = New csMusicalInstrumentCommon
End Sub
Private Sub Class_Terminate()
    Set mCommon = Nothing
End Sub
Private Property Get iMusicalInstrument_Common() As csMusicalInstrumentCommon
    Set iMusicalInstrument_Common = mCommon
End Property

Usage

Public Sub Test()
    Dim Trumpet As New csTrumpet
    Dim iTrumpet As iMusicalInstrument
    Set iTrumpet = Trumpet
    Debug.Print iTrumpet.Common.GUID
End Sub

:)

like image 62
MonkeyFace Avatar answered Oct 24 '22 00:10

MonkeyFace