21

Consider the following code:

Public Sub VBACompilerIsMad()

    Dim Ap As Application     
    Dim Wb As Workbook
    Dim Ws As Worksheet

    Debug.Print Ap.XXX ' No compile error
    Debug.Print Wb.XXX ' No compile error
    Debug.Print Ws.XXX ' Compile error

End Sub

When I compile this, I get a compiler error for referring to an inexisting member of Worksheet. However, if I comment out the last line, there is no compiler error, even though neither Application nor Workbook have a method or property XXX. It is as if I declared Ap and Wb as Object variables.

Why does the compiler treat Application / Workbook differently from Worksheet?

Are there any other classes like this, that the compiler seems to treat as if they were Object?

Community
  • 1
  • 1
Martin Roller
  • 307
  • 1
  • 12
  • 2
    I mean the code verbatim. The method is XXX, and there is nothing that would print - infact you can leave out Debug.Print altogether. What I'm saying is that I don't get the compiler error message that I expect. – Martin Roller Jan 29 '15 at 16:08

4 Answers4

24

As I have been explained (kudos go respectively), this is a COM feature.

By default COM assumes an interface is extensible, that is, it allows adding members at run time. If that is not the desired behaviour, one can apply the [nonextensible] attribute to the interface definition, which declares the interface only accepts methods explicitly defined in the type library.

dispinterface _Application and dispinterface _Workbook do not have this flag set in the Excel type library, dispinterface _Worksheet does.

Similarly, ADO's dispinterface _Connection does not have [nonextensible], dispinterface _Command does.

To learn which are extensible, add a reference to TypeLib Info in the project's References and run:

Dim t As tli.TLIApplication
Set t = New tli.TLIApplication

Dim ti As tli.TypeLibInfo
Set ti = t.TypeLibInfoFromFile("excel.exe")

Dim i As tli.InterfaceInfo
For Each i In ti.Interfaces
    If (i.AttributeMask And tli.TYPEFLAG_FNONEXTENSIBLE) <> tli.TYPEFLAG_FNONEXTENSIBLE Then
      Debug.Print i.Name
  End If
Next

You will see that almost all interfaces are extensible here, so most of them get pushed out of the debug window and you will only see the last ones. Change the <> to = to print those that are not extensible, there are much less of them.

Community
  • 1
  • 1
GSerg
  • 76,472
  • 17
  • 159
  • 346
6

A bit of a hypothesis:

You can call a stored procedure on an ADODB.Connection object like a native method (at the bottom). (The examples for this on several msdn sites look oddly messed up).
So there is some mechanism like 'anonymous/dynamic methods' in VBS/VBA. It may be a similar mechanism activated here for Application and Workbook classes - although I don't see where and how exactly.

A test supports the basic idea:
I have tested this with a reference to Microsoft ActiveX Data Objects 2.8 Library:

Public Sub testCompiler()
    Dim cn As ADODB.Connection
    Dim cmd As ADODB.Command

    Debug.Print cn.XXX
    Debug.Print cmd.XXX
End Sub

cn.XXX does not throw a compile error, cmd.XXX does.

KekuSemau
  • 6,830
  • 4
  • 24
  • 34
  • 3
    +1 for addressing the *"Are there any other classes like this, that the compiler seems to treat as if they were Object?"* part of the question :) – Mathieu Guindon Jan 29 '15 at 18:19
  • In Excel, too, you can call worksheet functions as if they were native methods of `Application` (http://www.cpearson.com/excel/callingworksheetfunctionsinvba.aspx). This could be the reason for 'anonymous/dynamic methods' on the `Excel.Application` class. The Applicationn classes of Word, Powerpoint and Access appear to be ordinary classes. – Martin Roller Jan 29 '15 at 22:39
  • 1
    Pearson says, you can call any worksheet function on `Application` and on `Application.WorksheetFunction`. The class `WorksheetFunction` declares all these functions as regular class methods, the class `Application` does not, not even as "hidden members". – Martin Roller Jan 29 '15 at 23:42
  • Oh right, sorry, I overlooked that. Alright then, that seems to be it for the `Application` class! :) – KekuSemau Jan 29 '15 at 23:54
2

GSerg's answer is indeed outstanding, I love the whole COM type library IDL and how some attributes there can govern the behaviour in the Excel VBA IDE. Long may this arcane knowledge of COM be handed down! And, I realise this question has been bountied to give that answer more rep but when a bounty is set it appears on my radar and I have a view on this matter.

So although GSerg's answer gives the mechanism it does not give the rationale, i.e. it gives the how but not the why. I'll attempt to answer the why.

Some of the answer why is already given by Martin Roller (OP) in his comments about Application and WorksheetFunction. This, to me, is a convincing reason to keep Application extensible and I'll not consider Application further.

Let us turn to Workbook and Worksheet and we best start with some code to demonstrate, so you will need to begin with two fresh workbooks, call them MyWorkbook.xlsm and OtherWorkbook.xlsm. So some instructions:

In OtherWorkbook.xlsm go the code module ThisWorkbook and paste the code

Option Explicit

Public Function SomeFunctionExportedOffOtherWorkbook() As String
    SomeFunctionExportedOffOtherWorkbook = "Hello Matt's Mug!"
End Function

In MyWorkbook.xlsm go the Sheet1 code module and paste the code

Option Explicit

Public Function SomeFunctionExportedOffCodeBehindSheet1() As String
    SomeFunctionExportedOffCodeBehindSheet1 = "Hello Martin Roller!"
End Function

Now, in the VBA IDE change the codename of Sheet1 to codebehindSheet1 Now, in a new standard module in MyWorkbook.xlsm add the following code

Sub TestingObjectLikeInterfacesOfWorkbookAndCodeBehindWorksheet_RunMany()

    '* For this example please rename the 'CodeName' for Sheet1 to be "codebehindSheet1" using the IDE
    Debug.Assert ThisWorkbook.Worksheets.Item("Sheet1").CodeName = "codebehindSheet1"


    Dim wb As Workbook
    Set wb = Application.Workbooks.Item("OtherWorkbook")

    '* Workbook dispinterface needs to not marked with nonextensible attribute
    '* so that it doesn't trip up over exported function in another workbook
    '* below SomeFunctionExportedOffOtherWorkbook is defined in the ThisWorkbook module of the workbook "OtherWorkbook.xlsm"
    Debug.Print wb.SomeFunctionExportedOffOtherWorkbook


    '*Not allowed --> Dim foo As Sheet1
    '*have to call by the 'code behind' name which is usually Sheet1 but which we changed to illustrate the point
    Debug.Print codebehindSheet1.SomeFunctionExportedOffCodeBehindSheet1


End Sub

Now run this code above.

You've probably read the code and hopefully understood the point I'm making but let me spell it out. We need Workbook to remain extensible because it may contain a reference to another workbook which may be exporting a method or function and we'd like no compile errors.

However, for the Worksheet, to do a similar export we again add code to the code behind module but there is a difference in referencing the module: one grabs a reference to that code behind module by using its VBA code name, most people do not change this from Sheet1 (that is why you were invited to change it above).

So the interface obtained by the code behind module name needs to extensible and not the Excel.Worksheet interface.

P.S. Anyone got a copy of TLI.dll?

S Meaden
  • 8,050
  • 3
  • 34
  • 65
  • Cool. You're welcome. Happy to help bounty setters (irrespective of who wins) because they are serious. – S Meaden Jan 11 '17 at 18:38
0

As a workaround it could still be possible to create your own interface and implement this interface. Then declare a variable as INewInterface and all the compiler messages will be there :). Here simple example with custom interface for a UserForm. HTH

Interface

Public CancelButton As MSForms.CommandButton
Public DataList As MSForms.ListBox
Public CommandBox As MSForms.TextBox

Implementation

Implements IMyForm

Private Property Set IMyForm_CancelButton(ByVal RHS As MSForms.ICommandButton)

End Property

Private Property Get IMyForm_CancelButton() As MSForms.ICommandButton

End Property

Private Property Set IMyForm_CommandBox(ByVal RHS As MSForms.IMdcText)

End Property

Private Property Get IMyForm_CommandBox() As MSForms.IMdcText

End Property

Private Property Set IMyForm_DataList(ByVal RHS As MSForms.IMdcList)

End Property

Private Property Get IMyForm_DataList() As MSForms.IMdcList

End Property

Usage

enter image description here

Note: MyForm is existing VBA Form which has been added to the project.

Daniel Dušek
  • 13,683
  • 5
  • 36
  • 51