How do I Late Bind a Class Module from my Project? - excel

I have two class modules in my project, "ClassAlfa" and "ClassBeta". I have separated the code between the two classes so that if an end-user does not have ClassBeta included in their project, then any of ClassAlfa's late bound references to it gracefully fail. Where I'm having trouble is making the script work if the end-user DOES have ClassBeta available.
Within ClassAlfa, I have the following code:
Option Explicit
Public myClass as Object
Private Sub Class_Initialize()
On Error Resume Next
Set myClass = Application.Run("ClassBeta")
If Err = 0 Then 'End user has this class available, go ahead and use it.
'Do code with ClassBeta
Else
'Do code without ClassBeta
End If
On Error GoTo 0
End Sub
This throws the following error on the Set line:
Run-time error '1004':
Cannot run the macro 'ClassBeta'. The macro may not be available in this workbook or all macros may be disabled.
I have also tried replacing the Set line with this:
Set myClass = CreateObject("ClassBeta")
which instead throws the error
Run-time error '429':
ActiveX component can't create object
Also does not work:
Set myClass = CreateObject("'" & ThisWorkbook.Name & "'!" & "ClassBeta")
What is the proper way to late bind a custom class from my own project?

There is no mechanism in VBA that would allow you to check if a class exists by it's name and to create a new instance if it is.
However, it is possible to achieve. Let's dissect the problems and see how we can work around them.
Problem 1
You need to create an instance of a class that you are not sure it exists in the current project. How?
As you already tried, Application.Run and CreateObject do not work.
Application.Run is only capable of running methods in standard .bas modules (not class modules). It does not create instances of classes.
CreateObject does a few things behind the scenes. First, it calls CLSIDFromProgIDEx using the ProgID (the string) you are passing. Then, it calls CoCreateInstance. The problem is that VBA classes do not have their ProgIDs in the registry so CreateObject simply doesn't work. You would need to have your class in a registered .dll file of ActiveX.exe to make this work.
That leaves us with the New keyword to instantiate a new ClassBeta. That obviously works when the class is present but gives a 'User-defined type not defined' compiler error when it's not. There are only 2 ways to supress this compiler error:
Have a compiler directive
Not have the New ClassBeta at all
A compiler directive would look like this in ClassAlfa:
Option Explicit
#Const BETA_EXISTS = False
Sub Test()
Dim myBeta As Object
#If BETA_EXISTS Then
Set myBeta = New ClassAlpha
#End If
Debug.Print "Type of 'myBeta' is: " & TypeName(myBeta)
End Sub
This compiles fine without having the ClassBeta in the project but you could never make the BETA_EXISTS conditional compiler constant to turn True because:
Only conditional compiler constants and literals can be used in expression
So, the last option is not to have the New ClassBeta anywhere in the project. Except, we do. We can put it in the ClassBeta itself as a factory:
Option Explicit
Public Function Factory() As ClassBeta
Set Factory = New ClassBeta
End Function
When the class is missing, the factory is missing and the New ClassBeta will not throw a compiler error.
Problem 2
How do we call the .Factory method on the ClassBeta?
Well, we obviously cannot create a new instance of ClassBeta because that is our first problem.
What we can do is to make sure that ClassBeta always has a default global instance (like userforms have). To do that we need to export the class to a .cls text file and edit it with a text editor (like Notepad). The text should look like this:
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "ClassBeta"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = False
Option Explicit
Public Function Factory() As ClassBeta
Set Factory = New ClassBeta
End Function
Notice that I've changed the VB_PredeclaredId attribute to True (manually, in the text editor). Now we can import the class back. We can check if it worked by typing ?Typename(ClassBeta.Factory) in the Immediate window and then pressing Enter. We should see:
The code in ClassAlfa can now be written as:
Option Explicit
Sub Test()
Dim myBeta As Object
On Error Resume Next
Set myBeta = ClassBeta.Factory
On Error GoTo 0
Debug.Print "Type of 'myBeta' is: " & TypeName(myBeta)
End Sub
Problem 3
If we remove ClassBeta from the project, the following line does not compile:
Set myBeta = ClassBeta.Factory
But, compared to problem 1, this time the compiler error is 'Variable not defined'.
The only way that I could think of, to get rid of this new compiler error, is to turn off Option Explicit. Yeah! That bad!
ClassAlfa:
'Option Explicit 'needs to be off to be able to compile without the ClassBeta class
Sub Test()
Dim myBeta As Object
On Error Resume Next
Set myBeta = ClassBeta.Factory
On Error GoTo 0
Debug.Print "Type of 'myBeta' is: " & TypeName(myBeta)
End Sub
Final Thoughts
You could do the development while Option Explicit is on and turn it off for your users.
If your users will use the ClassAlfa with Option Explicit off, the the code should work fine. But if they want ClassBeta as well, then the only way they could get it would be via importing the .cls file. Copy-pasting code won't work because they would lose the global instance of ClassBeta (the one we set in the VB_PredeclaredId hidden attribute).
I must say that I do not recommend removing Option Explicit because that could lead to other issues. I do not think there is a way to achieve what you want in a 'clean' way.

Related

Latest Excel for Mac no longer compiles

The following code works fine on latest Excel Windows and also on Excel 16.28 on Mac. But on the latest Excel for Mac (16.29 and 16.30) it generates this error: "Compile error: Method or data member not found" on the code line MyShape.Select.
I assume that there is an alternative way to do what I want that the compiler will approve of, but I don't know what it would be. As an alternative, I tried not selecting the shape and just referring to it, but then I get the same error but on the With MyShape.ShapeRange.Fill line.
Dim MyShape As Shape
'Other stuff
Set MyShape = ActiveSheet.Shapes.AddShape(msoShapeRectangle, 400, 400, DistanceBetweenCells, LineWidth)
MyShape.Select
With Selection.ShapeRange.Fill
'stuff here
End With
I'm hoping that a newer version of Mac Excel, when released, will revert to the older version in allowing the above, but assuming that's not the case, any workarounds?
I like that you're explicitly referring to ActiveSheet, kudos!
The problem is that ActiveSheet is an Object, wich means the compiler is helpless: ActiveSheet.Shapes compiles, but so will ActiveSheet.Shapess - even with Option Explicit specified. The entire expression is evaluated at run-time.
Let's fix that first:
Dim sheet As Worksheet
Set sheet = ActiveSheet
Now sheet.Shapes gets intellisense and compile-time validation, along with subsequent the .AddShape member call. You even get parameter tooltips as you type up the argument list!
What happens next is interesting: you declared MyShape as a Shape, but it's not a Shape you're looking at - the Shape class doesn't have a ShapeRange property, so... where does MyShape.ShapeRange come from then?
If you break execution (F9 to set a breakpoint) after the MyShape.Select call, and then bring up the immediate pane (Ctrl+G), the answer appears:
?typename(selection)
Rectangle
If you press Shift+F2 on the word Rectangle...
Dim myRectangle As Excel.Rectangle '<~ here
...the VBE doesn't seem to figure it out ("identifier under cursor is not recognized"). So we press F2, then right-click somewhere and tick the "Show hidden members" option - and sure enough, there it is:
So your code says "let's use the Shape interface", but works with a Rectangle object. And since that works, it means a Rectangle "is a" Shape: the two interfaces simply describe the same object through different lens, so either works... but then Shape.ShapeRange doesn't look quite right, since the Shape class doesn't define that member and that's the interface we explicitly said we were going to be working with.
If we want to invoke the members of Rectangle, we can - and since we're now showing hidden members in the object browser, intellisense displays the hidden types and members too. If the entire With block is early-bound, everything makes much more sense:
With myRectangle.ShapeRange.Fill
...and explains how the late-bound code off ActiveSheet would work at run-time to resolve the member call, and now the compiler needs a completely other strategy to compile the VBA code: maybe that could shake things up enough to get it to work, maybe it won't. At least the type ambiguities and ignored-by-compiler statements are all gone :)
The thing that's surprising here, is that you can't do that with VBA user code. If you made a MyShape class with a DoSomething method:
'#ModuleDescription "A metaphorical Shape"
Option Explicit
Public Sub DoSomething()
MsgBox TypeName(Me)
End Sub
And then a MyRectangle class that implements MyShape and exposes a member on its own public interface, that yields a MyShape object reference:
'#ModuleDescription "A metaphorical Rectangle"
Option Explicit
Private sh As MyShape
Implements MyShape
Public Property Get Thing() As Object
Set Thing = sh
End Property
Private Sub Class_Initialize()
Set sh = New MyShape
End Sub
Private Sub MyShape_DoSomething()
MsgBox TypeName(Me)
End Sub
And now in any standard module, we can test this - first, all early-bound, and we'll have a factory method that returns a MyShape, to mimick Shapes.CreateShape:
Public Sub WorksMaybe()
Dim r As MyShape
Set r = CreateRect
r.Thing.DoSomething
End Sub
Private Function CreateRect() As MyShape
Set CreateRect = New MyRectangle
End Function
So we run this (on Windows), and I expected, the code doesn't compile:
Late binding however...
Public Sub WorksMaybe()
Dim r As Object
Set r = CreateRect
r.Thing.DoSomething
End Sub
Private Function CreateRect() As MyShape
Set CreateRect = New MyRectangle
End Function
...works? Nope:
Are we not looking at a MyRectangle object? No: we're looking at the limits of late-binding polymorphism in VBA - we created a New MyRectangle, but to the compiler CreateRect returns a MyShape object reference. If we place a breakpoint on End Function, run it, and then type ?TypeName(CreateRect) in the immediate pane (Ctrl+G) when the breakpoint is hit, then despite the declared type being MyShape, the runtime type is clearly MyRectangle.
And it should work - but it doesn't. Error 438, member not found: the late-bound/run-time equivalent of the "method or data member not found" compile error.
And if we use the interface we really mean to work with...
Public Sub WorksMaybe()
Dim r As MyRectangle
Set r = CreateRect
r.Thing.DoSomething
End Sub
Private Function CreateRect() As MyShape
Set CreateRect = New MyRectangle
End Function
...then everything "just works":
Now, I'm not running this on a Mac, but this code compiles for me...
Option Explicit
Const DistanceBetweenCells As Long = 50
Const LineWidth As Long = 2
Public Sub WorksMaybe()
Dim r As Excel.Rectangle
Set r = CreateRect
r.ShapeRange.Fill.BackColor.RGB = vbRed
End Sub
Private Function CreateRect() As Excel.Shape
Set CreateRect = Shapes.AddShape(msoShapeRectangle, 40, 40, DistanceBetweenCells, LineWidth)
End Function
...and systematically raises run-time error 13 as soon as CreateRect returns and the Shape reference gets assigned to a Rectangle - error 13 being "type mismatch". In other words, a Rectangle is not a Shape (!!?!??). Proof, if we make CreateRect return a Excel.Rectangle, we now get the type mismatch error as soon as we try to assign the function's return value, and nothing makes sense anymore: there's something weird going on, and, well, I'm out of ideas - there doesn't appear to be any way to work early-bound with a Rectangle, despite what TypeName(Selection) claims the type is (the class is hidden/undocumented for a reason after all!), which... pretty much destroys all hope, especially if neither With Selection.Fill nor With MyShape.Fill work (it does work perfectly fine here on my Windows box though).
Sending a frown with some repro code through the user feedback feature should get you heard from the product team at Microsoft. I doubt they removed anything from anywhere - but it's not impossible something broke how interfaces are resolved, somewhere deep down in some seemingly unrelated piece of internal API :)
Ok, I think it is figured out. In Mac Excel version 16.29 Microsoft has deleted certain class members for, at least, Shapes. For instance "Fill" and "Select" are no longer available. So any code that refers to them will generate an error.
I'm not sure how extensive this is or any other ramifications, but I do know that the code works fine in version 16.28, and also that the member list in 16.28 shows both "fill" and "select" but not in 16.29. Thanks to Mathieu Guindon above for the input and also to a poster who deleted his thread - both of these people really helped. I've reported the issue to Microsoft.
Answer from Steve Rindsberg (MVP):
Iterate through the .ShapeRange collection:
For x = 1 To .ShapeRange.Count
With .ShapeRange(x)
'...stuff....
End With
Next
I've had a few similar weird ones on Mac where perfectly good code (on
Windows) errors or sometimes make the Mac app go POOF! And disappear.
Iterating the collections this way has been the fix.

How to activate a wookbook named in a different module

I call module2 from module1 where I name a workbook "x" in module2. But later when I try "x.Activate" in module1 I get an error "Run-time error '424': Object required"
I have a rather lengthy module that I would like to organize by breaking it up into multiple modules. So far I have created a module called "INPUTS" in this module I have a "Sub RT_CMM_DATA_COMPILER_INPUTS()" presumably in the future I will have other Subs in this module "Sub RT_Some_Other_Project_INPUTS()" I name a workbook in "Sub RT_CMM_DATA_COMPILER_INPUTS()" and try to activate that workbook by name in a separate module called sandbox. But it displays an error.
'RT_Sandbox Module
Sub sandbox()
Call RT_CMM_DATA_COMPILER_INPUTS
wkbwatchFolders_table.Activate
lastShtRow = LASTSHEETROW(ActiveSheet)
MsgBox lastShtRow
End Sub
'Inputs module
Sub RT_CMM_DATA_COMPILER_INPUTS()
watchFolders_filePath = "D:\RT_CMM_Data_File_Paths.xlsx"
Set wkbwatchFolders_table = Workbooks.Open(Filename:=watchFolders_filePath)
End Sub
Am I going about this attempt to organize my code completely wrong? Should I be using class modules for this instead? Or is it just some syntax I am missing?
The critical part you're missing is Option Explicit at the top of every module.
With that option, code will refuse to compile until all variables are explicitly declared.
Without it, watchFolders_filePath is an undeclared variable in both procedures, and in the scope where it is read but not assigned, its data type is Variant/Empty.
Rubberduck (free, open-source VBIDE add-in project that I manage) can help locate and fix these issues (and others) in your code:
OptionExplicit inspection
UnassignedVariableUsage inspection
UndeclaredVariable inspection
VariableNotAssigned inspection
VariableNotUsed inspection
As for your code, you don't need any global variables. Avoid global variables whenever possible. Use functions (and parameters) instead:
Function RT_CMM_DATA_COMPILER_INPUTS() As Workbook
Dim watchFolders_filePath As String
watchFolders_filePath = "D:\RT_CMM_Data_File_Paths.xlsx"
Set RT_CMM_DATA_COMPILER_INPUTS = Workbooks.Open(Filename:=watchFolders_filePath)
End Function
Sub sandbox()
Dim wb As Workbook
Set wb = RT_CMM_DATA_COMPILER_INPUTS
wb.Activate
lastShtRow = LASTSHEETROW(wb.ActiveSheet)
MsgBox lastShtRow
End Sub
Using Public statement will work here:
Public x As Workbook
Public your_var As Object
You need to declare these outside your procedure, at the top of the module. After declaring these you can access them anywhere in any module.
Read More here: Declaring Variables

What is the difference between Me-Object and UserFormName-Object?

I just found out that the Me-Object and UserFormName is not the same Object. Here my example:
I have two custom classes FilterLine and FilterModel. All you need to know is that FitlerModel has a property N which is set to = if newed up. There is also a UserForm called frmFilter.
Sub testFilter()
Dim Filterm As FilterModel
Set Filterm = New FilterModel
With New frmFilter
Set .Model = Filterm
.ExampleSub ' This is the interesting part
.Show
End With
End Sub
Here the ExampleSub of the Userform frmFilter:
Public Sub ExampleSub()
Debug.Print Me.Model.N ' gives a 0
Debug.Print frmFilter.Model.N ' gives an error "Object not Found"
End Sub
I find this rather interesting what is going on here? Why are they different and how are they different?
They refer to different objects. A UserForm is just a Class that has a Default Instance, a free object created when you call the class by name.
Me.Model.N
References the object you instanced.
frmFilter.Model.N
Refers to the Default Instance of the Userform, which has no model associated with it.
Good write-up on this issue here: https://rubberduckvba.wordpress.com/2017/10/25/userform1-show/
You can use UserForm.Object only when the UserForm is explicitly created (Using Insert -> User Form) and shown in the project explorer tree.
If the userform is not shown in the project explorer tree, then you will get an error "Object not Found" if you use UserForm.Object

How to declare Global Variables in Excel VBA to be visible across the Workbook

I have a question about global scope and have abstracted the problem into a simple example:
In an Excel Workbook:
In Sheet1 I have two(2) buttons.
The first is labeled SetMe and is linked to a subroutine in Sheet1's module:
Sheet1 code:
Option Explicit
Sub setMe()
Global1 = "Hello"
End Sub
The second is labeled ShowMe and is linked to a subroutine in ThisWorkbook's module:
ThisWorkbook code:
Option Explicit
Public Global1 As String
Debug.Print("Hello")
Sub showMe()
Debug.Print (Global1)
End Sub
Clicking on SetMe produces a compiler error: variable not defined.
When I create a separate module and move the declaration of Global1 into it everything works.
So my question is:
Everything I have read says that Global variables, declared at the top of a module, outside of any code should be visible to all modules in the project. Clearly this is not the case.
Unless my understanding of Module is not correct.
The objects Sheet1, Sheet2, ThisWorkbook,... that come with a workbook: are these not modules capable of declaring variables at global scope?
Or is the only place one can declare a global, in a separate module of type Modules.
Your question is:
are these not modules capable of declaring variables at global scope?
Answer: YES, they are "capable"
The only point is that references to global variables in ThisWorkbook or a Sheet module have to be fully qualified (i.e., referred to as ThisWorkbook.Global1, e.g.)
References to global variables in a standard module have to be fully qualified only in case of ambiguity (e.g., if there is more than one standard module defining a variable with name Global1, and you mean to use it in a third module).
For instance, place in Sheet1 code
Public glob_sh1 As String
Sub test_sh1()
Debug.Print (glob_mod)
Debug.Print (ThisWorkbook.glob_this)
Debug.Print (Sheet1.glob_sh1)
End Sub
place in ThisWorkbook code
Public glob_this As String
Sub test_this()
Debug.Print (glob_mod)
Debug.Print (ThisWorkbook.glob_this)
Debug.Print (Sheet1.glob_sh1)
End Sub
and in a Standard Module code
Public glob_mod As String
Sub test_mod()
glob_mod = "glob_mod"
ThisWorkbook.glob_this = "glob_this"
Sheet1.glob_sh1 = "glob_sh1"
Debug.Print (glob_mod)
Debug.Print (ThisWorkbook.glob_this)
Debug.Print (Sheet1.glob_sh1)
End Sub
All three subs work fine.
PS1: This answer is based essentially on info from here. It is much worth reading (from the great Chip Pearson).
PS2: Your line Debug.Print ("Hello") will give you the compile error Invalid outside procedure.
PS3: You could (partly) check your code with Debug -> Compile VBAProject in the VB editor. All compile errors will pop.
PS4: Check also Put Excel-VBA code in module or sheet?.
PS5: You might be not able to declare a global variable in, say, Sheet1, and use it in code from other workbook (reading http://msdn.microsoft.com/en-us/library/office/gg264241%28v=office.15%29.aspx#sectionSection0; I did not test this point, so this issue is yet to be confirmed as such). But you do not mean to do that in your example, anyway.
PS6: There are several cases that lead to ambiguity in case of not fully qualifying global variables. You may tinker a little to find them. They are compile errors.
You can do the following to learn/test the concept:
Open new Excel Workbook and in Excel VBA editor right-click on Modules->Insert->Module
In newly added Module1 add the declaration; Public Global1 As String
in Worksheet VBA Module Sheet1(Sheet1) put the code snippet:
Sub setMe()
Global1 = "Hello"
End Sub
in Worksheet VBA Module Sheet2(Sheet2) put the code snippet:
Sub showMe()
Debug.Print (Global1)
End Sub
Run in sequence Sub setMe() and then Sub showMe() to test the global visibility/accessibility of the var Global1
Hope this will help.

MS Access VBA: Reference Excel Application Object created in separate Module

This seems like it should be an easy one but I'm stuck.
I'm running a VBA script in Access that creates a 40+ page report in Excel.
I am creating an Excel Application Object using Early Binding:
Public obj_xl As New Excel.Application
Here is an example of how I am referencing the object:
With obj_xl
.Workbooks.Add
.Visible = True
.Sheets.Add
.blahblahblah
End With
The problem is that the procedure has become too large and I need to break the code up into separate modules.
If I try to reference the Excel Application Object from a different module than it was created in, it throws an error ("Ambiguous Name").
I'm sure I could do something with Win API but that seems like it would be overkill.
Any thoughts? Thanks
this is the type of situation that can cause the error "Ambiguous Name"
Function Split(s As String)
MsgBox s
End Function
Function Split(s As String)
MsgBox s
End Function
I know the example is trivial, but what you are looking for is a function , an object and/or a form control with the same names.
If you convert your declaration to Global, you can reference it in all your modules. For example, in one module, put this at the top:
Global obj_xl As Excel.Application
Then in an another module,
Sub xx()
Set obj_xl = New Excel.Application
Debug.Print obj_xl.Name
End Sub

Resources