16

I have been fighting with an Excel 2007 problem for several days now. Below is a listing of all facts I can think of that might be relevant:

  1. IDetailSheet is a class declared in the VBA project with several methods, and which throws an error in its Class Initializer so that it cannot be instantiated (making it abstract).

  2. Option Explicit is set in all modules.

  3. Ten worksheets in the VBA project implement IDetailSheet and compile cleanly (as does the entire project).

  4. CDetailSheets is a class declared in the VBA project that wraps a Collection object and exposes the Collection object as a Collection of IDetailSheet. It also exposes some additional methods to perform certain methods of IDetailSheet on all collection menmbers.

  5. In its Class initializer (called from the Workbook_ Open event handler and assigned to a global variable), CDetailSheet executes the following code to populate the private collection DetailSheets:

    Dim sht as EXCEL.WorkSheet
    For Each sht in ActiveWorkbook.Worksheets
      If TypeOf sht is IDetailSheet Then
        Dim DetailSheet as IDetailSheet
        Set DetailSheet = sht
        DetailSheets.Add DetailSheet, DetailSheet.Name
      End If
    Next sht
    
  6. In certain Ribbon call-backs the following code is run:

       If TypeOf ActiveWorkbook.ActiveSheet is IDetailSheet Then
          Dim DetailSheet as IDetailSheet
          Set DetailSheet = ActiveWorkbook.ActiveSheet
          DetailSheet.Refresh  *[correction]*
       End If
    
  7. All ActiveX controls have been removed from the Workbook, after having been identified with other stability issues (There were a few dozen originally). A Fluent Interface Ribbon has been created to replace the functionality originally associated with the ActiveX controls.

  8. There is a Hyperion add-in from the corporate template, but it is not used in this workbook.

When all is said and done, the following symptom occurs when the workbook is run:

  • Any number of instances of IDetailSheet are recognized in the CDetailSheets Initializer by TypeOf Is, from 1 (most common) to occasionally 2 or 3. Never zero, never more than 3, and most certainly never the full 10 available. (Not always the same one, though being near the front of the set seems to increase likelihood of being recognized.)
  • Whichever instances of IDetailSheet implementation are discovered in the CDetailSheets initializer (and as near as I can determine, only such instances) are also recognized by TypeOf ... Is in the Ribbon call-back.

Can anyone explain why most of the TypeOf ... Is operations are failing? Or how to fix the issue?

I have resorted to manually creating v-tables (i.e. big ugly Select Case ... End Select statements) to get the functionality working, but I actually find it rather embarrassing to have my name beside such code. Besides which, I can see that being a future maintenance nightmare.

Thinking that it might be a stale p-code issues, I went to the extent of deleting the Project.Bin file from the expanded XLSM zip, and then manually importing all the VBA code back in. No change. I also tried adding the project name to all the usages of IDetailSheet to make them miFab.IDetailSheet, but again to no avail. (miFab is the project name.)

Vogel612
  • 5,620
  • 5
  • 48
  • 73
Pieter Geerkens
  • 11,775
  • 2
  • 32
  • 52
  • 1
    Are `Foreach` and `EndIf` typos in your question? – Doug Glancy Aug 29 '13 at 01:13
  • No; but the indentation wont' work for me today. The If ... EndIf is nested inside the ForEach ... Next statement. However, the code is being typed from memory without Excel being handy for syntax checking. – Pieter Geerkens Aug 29 '13 at 01:15
  • 1
    Yes, but in VBA it would be `For Each` and `End If`. – Doug Glancy Aug 29 '13 at 01:16
  • does your first symptom mean when you run the code in (5) you only have 1-3 of the 10 worksheets recognized as IDetailSheet and added to the collection? I don't suppose you could use `TypeName` instead of `TypeOf`? I've not tried using `TypeName` with a custom class, though. – enderland Aug 29 '13 at 03:07
  • By definition, as I need to cast to an *IDetailSheet*, the variable cannot be a variant. *From Excel Documentation: TypeName(varname) The required varname argument is a Variant containing any variable except a variable of a user-defined type.* – Pieter Geerkens Aug 29 '13 at 03:08
  • Besides which, any sensible implementation of TypeName() has to return the base-class, not an implemented interface, which in this case would always be *EXCEL.WorkSheet*. – Pieter Geerkens Aug 29 '13 at 03:13
  • This is a real stretch, but maybe you could pass your worksheet pointers `ByRef` to a Win32 native API to get type information about a particular COM object. Not sure which particular API you'd use though; tis been a while since I `QueryInterface`'ed. – allquixotic Aug 29 '13 at 03:49
  • @somequixotic: Good thought; that would probably work, but at this point in time I am constrained to using VBA. As noted above, by writing a half-dozen-plus largish SELECT ... END SELECT statements to emulate v-tables, I have achieved the required functionality. The question is rather about why such was necessary, and will I have to do it again next time also? – Pieter Geerkens Aug 29 '13 at 05:00
  • 3
    This [blog entry](http://productivebytes.blogspot.co.uk/2012/10/using-implements-behind-excel-worksheet.html) suggests that using `Implements` with `Worksheet` objects results in instability – barrowc Aug 29 '13 at 05:27
  • @barrow: Yes, but that blog is not otherwise very informative on the issue. In extensive searching I have not found any other issue with *TypeOf* and *Implements* in VBA. Are we really the only two users who have encountered a problem with them? – Pieter Geerkens Aug 29 '13 at 06:00
  • have you checked [all conditions](http://www.mrexcel.com/forum/excel-questions/663582-visual-basic-applications-difference-between-type-type-name.html)? (*second post*) –  Aug 29 '13 at 07:07
  • @mehow: Yes! That is the list of why *TypeName* won't work here, and *TypeOf* is required. – Pieter Geerkens Aug 29 '13 at 10:59
  • 2
    This is certainly an interesting issue, and one I've never run into before. If there's any way you can refactor your VBA application to use encapsulation instead of inheritance, you could define a bunch of bare user-defined types or classes that don't extend/implement anything, and just declare them as a private field in each of the worksheets, with the desired properties/methods/functions to keep track of data you need. It's ugly, but it would *work*, probably, since the issue seems to be specifically with worksheets implementing interfaces. – allquixotic Aug 29 '13 at 13:32
  • 1
    @somequixotic: But how do I call them? They are all implementing the unique functionality anyways, but inheritance means I can call the unique method without a big SELECT END SELECT every time. – Pieter Geerkens Aug 29 '13 at 21:27
  • You could use a Dictionary mapping a unique identifier, such as an integer, declared in each worksheet module, to a particular instance of a class, or alternatively an instantiable type. Then, when you need to call one of the methods for a given sheet, you would simply access the dictionary passing the appropriate integer as a key, to get the desired payload object out as a value. You could just store this dictionary in a regular (public) Module. Whenever I hear about needing to do a large SELECT (or `switch` in other languages), a hash table or dictionary comes to mind. – allquixotic Aug 29 '13 at 21:41
  • ...particularly when performance is not of key importance. In C, a `switch` is much faster than a hash table, but in VBA, where COM server calls are so slow to begin with, there's probably not that big of a difference. Edit: just got another idea; instead of storing the unique integer in each worksheet class module, you could simply store the integers in an Enum, and then reference the values by name instead of having to memorize 10 numbers. This would only break if you are dynamically creating new worksheets at runtime, but I can't imagine how you'd do that while providing code for them... – allquixotic Aug 29 '13 at 21:42
  • @somequixotic: Huh! A Collection **is** a dictionary. You are describing in detail exactly the mechanism that isn't working, and which requires a working *TypeOf* operator to eliminate the SELECT ... END SELECT constructs. – Pieter Geerkens Aug 30 '13 at 00:27
  • 5
    Not really...? In your case, "ten worksheets in the VBA project implement IDetailSheet". I'm not the only commenter here who thinks that specifically using inheritance/interfaces on Worksheet objects in particular is the cause of the problem. This is why I'm suggesting that you create a per-sheet plain old class module implementing `IDetailSheet` instead, and put *those* in your collection. There are a number of ways that you can code up figuring out which `IDetailSheet` instance belongs to which Worksheet without using `TypeOf` or `Select ... End Select`. – allquixotic Aug 30 '13 at 14:12
  • @somequixotic: OK; got it now. Does anyone know if the failure of **implements** on Worksheets is new in 2007? Is it possibly due to introducing multiple threads in 2007, which would explain the inconsistent behaviour? – Pieter Geerkens Aug 30 '13 at 16:30
  • Don't know, but I reproduced the issue in Excel 2010, so they haven't fixed it yet (unless it's fixed in Excel 2013...). – allquixotic Aug 30 '13 at 17:40
  • 1
    I can reproduce this problem back in Excel 2003 - all of the `Worksheet` objects initially fail the `TypeOf` test. Interestingly evaluating the worksheet name in the Immediate window - `?Sheet1.Name` and so on - seems to cause the interface to be recognised and the `TypeOf` subsequently succeeds. Not sure how to produce the same effect in actual module code though – barrowc Aug 31 '13 at 07:29
  • @barrow: Does saving the value of `SheetN.Name` in a local variable *outside* the `If TypeOf sht Is IDetailSheet` test (since it will be used as the collection Key anyways when the test succeeds) make a difference? (I can't try it at work until Tuesday.) – Pieter Geerkens Aug 31 '13 at 11:55
  • 1
    If you use the object name directly - `sName = Sheet1.Name` - then subsequently the `TypeOf` test succeeds. If you use a reference into the `Worksheets` collection - `For Each ws In ThisWorkbook.Worksheets : sName = ws.Name : If (TypeOf ...` - then the `TypeOf` comparison still fails – barrowc Aug 31 '13 at 19:50
  • @barrow: Initial results ar positive. Thank you for the idea. – Pieter Geerkens Sep 04 '13 at 01:59
  • The only way I could reproduce your issue in 2010 is by not implementing all the methods of the interface in the sheet objects. This is consistent and I would have thought to be expected. Have you definitely got routines for all the methods in your interface in EVERY sheet module? – SWa Sep 04 '13 at 15:57
  • I wasn't, things are easy to overlook. The code compiled for me when I left a routine out of the sheet object. It ony errored when I actually called and tried to use the sheet – SWa Sep 04 '13 at 23:41
  • @Kyle: With *option explicit*? – Pieter Geerkens Sep 05 '13 at 00:00

3 Answers3

8

There are a few ways you could cheat using CallByName. You're going to have to work around this bug one way or another.

A quick dirty example

Every sheet that starts with an implementing line should have a public GetType function. I attached the "TestSheet" sub to a button on my ribbon. It puts the returned type name in cell A1 to demonstrate the function.

Module1

'--- Start Module1 ---
Option Explicit

Public Sub TestSheet()
  Dim obj As Object
  Set obj = ActiveSheet
  ActiveSheet.[A1] = GetType(obj)
End Sub

Public Function GetType(obj As Object) As String
  Dim returnValue As String
  returnValue = TypeName(obj)
  On Error Resume Next
  returnValue = CallByName(obj, "GetType", VbMethod)
  Err.Clear
  On Error GoTo 0
  GetType = returnValue
End Function
'--- End Module1 ---

Sheet1

'--- Start Sheet1 ---
Implements Class1
Option Explicit

Public Function Class1_TestFunction()
End Function

Public Function GetType() As String
    GetType = "Class1"
End Function
'--- End Sheet1 ---
Community
  • 1
  • 1
AndASM
  • 9,458
  • 1
  • 21
  • 33
  • 1
    I thought VBA had a CallByNae function but I was unable to find it last week. I have it now though; thank you. – Pieter Geerkens Sep 04 '13 at 02:01
  • 1
    You're welcome. I figure you can probably do all your work through the CallByName function, but that could be annoying and ugly to write. This way you can still cast the worksheet to the relevant interface, or retrieve a wrapper object if `implements` is causing other problems. – AndASM Sep 04 '13 at 15:22
  • I knew it would be sufficient, and a *less messy* work-around than I built last week, but for some reason I couldn't find it at that time. – Pieter Geerkens Sep 05 '13 at 00:12
3

I found this question after posting my own similar issue as TypeOf fails to work with Excel workbook's ActiveSheet that implements interface

I don't have a definitive explanation, but I think I do have a workaround.

I suspect it is because [the code] is implementing an interface on Sheet1 or Chart, and is extending Sheet1/Chart1, but Sheet1 is already extending Worksheet (and Chart1 is already extending Chart).

In my testing, I am able to force VBA to return the real value of TypeOf by firstly accessing a property of the sheet. That means, doing something ugly like:

'Explicitly access ThisWorkbook.ActiveSheet.Name before using TypeOf
If TypeOf ThisWorkbook.Sheets(ThisWorkbook.ActiveSheet.Name) Is PublicInterface Then
Community
  • 1
  • 1
ThunderFrame
  • 9,352
  • 2
  • 29
  • 60
0

If you're not trusting TypeOf, plough on and ignore errors:

Dim sht as EXCEL.WorkSheet
For Each sht in ActiveWorkbook.Worksheets
  'If TypeOf sht is IDetailSheet Then
  Dim DetailSheet As IDetailSheet
  On Error Resume Next
  Set DetailSheet = sht
  On Error GoTo 0
  If Not DetailSheet Is Nothing Then
    DetailSheets.Add DetailSheet, DetailSheet.Name
  End If
Next sht

If this doesn't work, the worksheets really aren't IDetailSheet at that time at least.

Mark Hurd
  • 10,665
  • 10
  • 68
  • 101
  • 1
    That was the very first work-around I tried; several days before writing the original post. My best guess is that there is some sort of timing issue / race condition going on in the STA. – Pieter Geerkens Sep 04 '13 at 21:30