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

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

Related

What about "Application" as default object in Excel VBA?

I have just written this easy macro in Excel VBA for merging a group of selected cells:
Sub Macro_Merge()
Dim Temp As String
Dim S As Variant
Temp = ""
For Each S In Selection
If Temp = "" Then
Temp = CStr(S.Value)
Else:
Temp = Temp + "," + CStr(S.Value)
End If
Next
Selection.Merge
Selection.Value = Temp
Selection.VerticalAlignment = xlTop
End Sub
This works fine, but I always see that annoying dialog box, warning me about loosing data while merging (which is exactly what I'm trying to avoid in my macro).
I can get rid of that dialog box, configuration the Application's DisplayAlerts property:
Application.DisplayAlerts = False
Selection.Merge
Selection.Value = Temp
Application.DisplayAlerts = True
This is working fine.
So, as Application is the default object, I tried to clean up my code, as follows:
DisplayAlerts = False
Selection.Merge
Selection.Value = Temp
DisplayAlerts = True
As you see, I simply omit mentioning the Application object. This is something which is allowed and I've done in the past. (If not in VBA, then Delphi, maybe?)
... but to my surprise, the dialog box appears again (although pressing F1 brings me to the official "Application.DisplayAlerts" documentation).
This leaves me with a simple question:
If a simple DisplayAlerts = ... does not equal Application.DisplayAlerts = ... anymore, what does it mean and how can I use it?
For your information, I'm working with Excel-365.
DisplayAlerts is an undeclared variable.
Certain Application properties and methods can (effectively) have the Application omitted:
ActiveCell, ActiveSheet, ActiveWorkbook, ActiveWindow, Addins, Charts, Selection, etc.
Calculate, Evaluate, Intersect, Run, Union, etc.
(but see this answer why/how this works):
A boolean property such as DisplayAlerts (EnableEvents, ScreenUpdating, etc) doesn't fall into the above category.
A golden rule in order not to fall into such a trap is the usage of Option Explicit while writing macros.
Just to add some information to the answer of #BigBen. If you write something like Workbooks or ActiveSheet in your code, VBA is not looking into the Application-object - it is looking into a (rather well hidden) object named Global.
The global object is exposing some (but not all) properties and methods of the Application-object, so ActiveSheet is referring to Application.ActiveSheet - but not because the Application has a member with this name but because the Global object defines that ActiveSheet means Application.ActiveSheet. In fact even the Application-object is accessed via the Global object.
There is hardly any information about this Global object or its concept. I found a page from Microsoft describing the Global object of MS Word, but the only explanation there is "Contains top-level properties and methods that don't need to be preceded by the Application property.". For Excel, I found this page on O'Reilly.
From time to time you get strange error messages like "Excel VBA Method 'Range' of object'_global' failed" - this is a pointer to the Global object. I would be glad to learn more about the concepts and mechanics of this object, but I am afraid that there are only very few people around that know more (except of course Mathieu Guindon AKA Mr. Rubberduck...). In daily life, we take it for granted that things like ActiveSheet simply works.

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

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.

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

Call helper function within parent without redefining objects from parent in the helper

I'm working in Excel with VBA to collect data for a table I'm building I have to go out to a TN3270 emulator to get it. In order to work with with the emulator I have to define a few objects to do the work. I also have a few helper functions that are used by multiple functions to navigate to different screens in the emulator. So far in order to use them I have had to copy the object definitions into those functions to get them to work. This works most of the time but occasionally (and in a way I cant predictably replicate) I get an error when the helper is recreating a particular object to use.
Option Explicit
Public Sub gather_data()
Dim TN_Emulator As Object
Dim Workbook As Object
Set TN_Emulator = CreateObject("TN_Emulator.Program")
Set Workbook = ActiveWorkbook
Dim string_from_excel As String
#for loop to go through table rows
#put value in string_from_excel
If string_from_excel = some condition
go_to_screen_2
#grab and put data back in excel
Else
go_to_screen_3
#grab and put data back in excel
End If
go_to_screen_1
#next loop logic
End Sub
Public Sub go_to_screen_1()
Dim TN_Emulator As Object
#the next step occasionally throws the error
Set TN_Emulator = CreateObject("TN_Emulator.Program")
#send instructions to the emulator
End Sub
Is there a way to import the existing objects (that get created and used without any errors) without redefining them into the helper functions to avoid this problem? I have tried searching in google but I don't think I'm using the right search terms.
First thanks goes to #JosephC and #Damian for posting the answer for me in the comments.
From JosephC 'The Key words you're looking for are: "How to pass arguments to a function".', and he provided the following link ByRef vs ByVal describing two different ways to pass arguments in the function call.
And from Damian the solution to my immediate concern. Instead of declaring and setting the objects that will be used in body of the helper function. Place the object names and types in the parentheses of the initial helper name, and when calling the helper from the other function also in the parentheses, shown below.
Option Explicit
Public Sub gather_data()
Dim TN_Emulator As Object
Dim Workbook As Object
Set TN_Emulator = CreateObject("TN_Emulator.Program")
Set Workbook = ActiveWorkbook
Dim string_from_excel As String
#for loop to go through table rows
#put value in string_from_excel
If string_from_excel = some condition
Call go_to_screen_2(TN_Emulator)
#grab and put data back in excel
Else
Call go_to_screen_3(TN_Emulator)
#grab and put data back in excel
End If
Call go_to_screen_1(TN_Emulator)
#next loop logic
End Sub
Public Sub go_to_screen_1(TN_Emulator As Object)
#send instructions to the emulator
End Sub
I believe I understood the instructions correctly, and have successfully tested this for my-self. I also passed multiple objects in the helper function definition and calls as needed for my actual application, in the same order each time Ex.
Sub go_to_screen_1(TN_Emulator As Object, ConnectionName As Object)
and
Call go_to_screen_1(TN_Emulator, ConnectionName)

Only user-defined type defined in public object modules can be coerced when trying to call an external VBA function

I am trying to call an Access function from Excel and get this error:
Compile Error: Only user-defined types defined in public object
modules can be coerced to or from a variant or passed to late-bound
functions.
I tried to adopt this solution I found, but with no luck. Here is my code:
In the Excel Module ExternalStatistics
Option Explicit
Public Type MyExternalStatistics
esMyInvites As Single
esMyInvitePerTalk As Single
End Type
Public MyExtRecStats As MyExternalStatistics
In the Sheet1(A-Crunched Numbers) object:
Option Explicit
Public appRecruitingAccess As Access.Application
Public Sub Worksheet_Activate()
Dim MyExtRecStats As MyExternalStatistics
Dim RecruitWindow As Integer
Dim test As String
Set appRecruitingAccess = New Access.Application
With appRecruitingAccess
.Visible = False
.OpenCurrentDatabase "C:\Dropbox\RECRUITING\Remote0\Recruiting 0.accdb"
RecruitWindow = DateDiff("d", Format(Date, Worksheets("ActivityAndIncentive").Range("IncentiveStart").Value), Format(Date, Worksheets("ActivityAndIncentive").Range("IncentiveEnd").Value))
RecruitWindow = DateDiff("d", Format(Date, Worksheets("ActivityAndIncentive").Range("IncentiveStart").Value), Format(Date, Worksheets("ActivityAndIncentive").Range("IncentiveEnd").Value))
MyExtRecStats = .Run("ExternalRecruitingStats", RecruitWindow) '*** ERROR HERE ***
.CloseCurrentDatabase
.Quit
End With
Set appRecruitingAccess = Nothing
End Sub
In the Access Module ExternalStatistics
Option Compare Database
Option Explicit
Public Type MyExternalStatistics
esMyInvites As Single
esMyInvitePerTalk As Single
end Type
Public Function ExternalRecruitingStats(StatWindow As Integer) As MyExternalStatistics
Dim MyRecStats As MyExternalStatistics
Dim Invites As Integer, Talks As Integer
Invites = 1
Talks = 2
With MyRecStats
.esMyInvites = CSng(Invites)
.esMyInvitesPerTalk = CSng(Invites/Talks)
End With
ExternalRecruitingStats = MyRecStats 'return a single structure
End Function
It does not like the MyExtRecStats = .Run("ExternalRecruitingStats", RecruitWindow) statement. I would like to eventually assign several set in the Access function and bring them all back with one object. Then I can place those values where they should be in the spreadsheet.
Type definitions in VBA are very local and they don't work well when you try to use them with objects that may not have access to the exact definition of the Type (which is probably the case here).
Sometimes, using a Class may work. You would need to make the class public and instantiate it before passing it around, but I have some doubts that it will actually work (for the same reason that the class definition won't be visible from one app to the other).
Another simple solution would be to use a simple Collection object instead, where you add your values as items to the collection. Of course the exact order of how you add/retrieve items is important.
There are a few interesting answers to a similar issue in User Defined Type (UDT) As Parameter In Public Sub In Class Module. It's about VB6 but it should also apply in great part to VBA.
Having said all this, you may be able to resolve all your issues by importing your Access code into Excel instead.
You can use DAO or ADO from Excel and manipulate Access databases just as if you were in Excel, for instance:
Connecting to Microsoft Access Database from Excel VBA, using DAO Object Model
Using Excel VBA to Export data to Ms.Access Table

Resources