3

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?

SandPiper
  • 2,816
  • 5
  • 30
  • 52
  • Do you have break on all errors set in settings? – QHarr Feb 14 '21 at 22:04
  • @QHarr, negative. I don't expect there to be an error if the class is actually present. If that module is NOT there, the code works as desired. – SandPiper Feb 14 '21 at 22:39
  • 1
    You can't use Class B with Application Run. This is why you get the error when it's there as VBA doesn't know how to 'run' Class B because it's not a method. – freeflow Feb 15 '21 at 08:21
  • @SandPiper This [question](https://stackoverflow.com/questions/1057670/create-a-new-object-using-the-text-name-of-the-class) is similar to your question and may give you some clues. – Brian M Stafford Feb 15 '21 at 19:25

1 Answers1

1

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:

  1. Have a compiler directive
  2. 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:
enter image description here

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.

Cristian Buse
  • 4,020
  • 1
  • 13
  • 34
  • This is a solid answer, thank you. I ended up taking the functionality from ClassBeta and replicating it in ClassAlfa. Not great for DRY principles, but it's the workaround I had to employ. – SandPiper Feb 20 '21 at 16:34
  • @SandPiper If you don’t mind me being inquisitive, what are you using them for? – Cristian Buse Feb 20 '21 at 17:06
  • Not at all, thank you for asking. I am working on my first open-source project (https://github.com/M-Scott-Lassiter/Fred-the-Developer). I expect to reach version 1.0 this weekend. I spun it off from another project I'm working on. – SandPiper Feb 24 '21 at 05:15
  • @SandPiper Interesting. I also have a code manager that I never posted and a couple of 100% VBA tools some of which are on GitHub. The [KeyedCollection](https://github.com/cristianbuse/VBA-KeyedCollection) is a good alternative to the Scripting.Dictionary when working on a Mac but I guess you have other dependancies like the Extensibility lib. Actually I might contact you on GitHub regading the latter (hope I have some time later this week). – Cristian Buse Feb 24 '21 at 07:27