VBA - Global Variable dropping out of scope - vba

I'm having an issue with a global variable dropping out of scope. I have defined a public variable in "ThisWorkbook":
Public dict As Scripting.Dictionary
This gets initalized in "Workbook_Open()"
Set dict = New Scripting.Dictionary
After initialization I run a Sub (code located in "ThisWorkbook" still) that fills this dictionary with IDs and instances of a custom class.
I'm having trouble working with this dict variable in other modules though. The goal is to build a number of public functions that the worksheet will be able to call. These functions manipulate/retrieve/etc data in the custom classes in the dictionary.
For example, this test sub (code in ModuleXYZ) throws "Object variable or With block variable not set"
Private Sub TestSub()
Dim x As Integer
x = ThisWorkbook.dict.Count
End Sub
This is the same error I would get when I first started this coding project, when the dict fell out of scope in the "ThisWorkbook" module, and I'd have to redo the "Set dict = New Scripting.Dictionary"
My hope was that by setting the dict as a Public variable in "ThisWorkbook", that it would stay in scope the entire time this workbook was open.
Thanks - KC

Try declaring the public variable in a module rather than the workbook. Also check that you haven't declared it twice. Convention is to declare all globals in one module which is often called globals.bas.

In cases like these, I sometimes like to use a Singleton pattern of sorts (at least, this is my understanding of the singleton pattern). I create a publicly available function that either returns the object if it exists, or creates then returns the object. This way, you don't have to worry too much about it falling out of scope.
Here's an example:
Public oDict As Object
Public Function GetDictionary() As Object
If oDict Is Nothing Then
Set oDict = CreateObject("Scripting.Dictionary")
For Each cel In Range("A1:A10")
oDict.Add cel.Value, cel.Offset(, 1).Value
Next cel
End If
Set GetDictionary = oDict
End Function
To reference it, it's like:
Sub GetDictCount()
MsgBox GetDictionary().Count
End Sub
Sub OtherDictTest()
MsgBox GetDictionary()(1)
End Sub

Related

Assigning an entity attribute to a variable

I'm new to programming in VBA or object-oriented programming. I'm trying to get this code to work in the VBA compiler of Arena by Rockwell Automation.
I sometimes get Error 424: 'Object required', and after debugging I'm referred to the XL.Quit at the end. Sometimes I've got an error related to the use of ActiveEntity. I don't know if I'm wrong getting the Excel file to work or if it's something do to with accessing the entity attributes of Arena anymore.
Can you spot the error? Or do you have a suggestion on how to find it myself?
Public Sub ModelLogic_RunBeginSimulation()
Dim m As Model
Set m = ThisDocument.Model
Dim s As SIMAN
Set s = m.SIMAN
Dim FileToOpen As String
Dim ArenaDir As String
Set XL = GetObject("", "Excel.Application")
ArenaDir = Mid(m.FullName, 1, Len(m.FullName) - Len(m.Name))
FileToOpen = ArenaDir & "data.xlsx"
XL.Workbooks.Open FileToOpen
End Sub
Public Sub VBA_Block_1_Fire()
Dim RNEG_deltaw As Double, Z_deltaw As Double, RPOS_deltaw As Double
Dim R_SI As Double, Z_SI As Double, S_SI As Double
Dim SI As Double, NumContenedor As Integer, Wo As Double, W_1 As Double, deltaW As Double
NumContenedor = s.EntityAttribute(s.ActiveEntity,s.SymbolNumber("NumContenedor"))
SI = XL.Workbooks(1).Worksheets(1).Cells(NumContenedor, 3).value
s.EntityAttribute(s.ActiveEntity, s.SymbolNumber("deltaW")) = SI
End Sub
Public Sub ModelLogic_RunEnd()
XL.Quit
End Sub
The XL Object is declared as procedure-level and therefore doesn't exist in the scope of another procedure.
If you want a variable or object to be visible to all procedures, declare it at the beginning of the module, perhaps like:
Public XL as Object
That being said, you didn't declare XL it in the other procedure either. Add this line to the very top of this module (and every module, ever):
Option Explicit
This will force you to properly declare and handle your variables, objects, properties, etc, and help prevent issues like this.
One more thing: I bet you've run this code at least a few times, right?
Save your work and then REBOOT, right away. Just trust me.
More information:
MSDN : Understanding Scope and Visibility
When Are You Required To Set Objects To Nothing?
Jon Skeet : Writing the Perfect Question
Assuming XL is declared at module level, I believe the error relates to this line:
Set XL = GetObject("", "Excel.Application")
This requires Excel to be running. It appears when ModelLogic_RunEnd() is fired, tries to call the .Quit() method of Excel, but XL is nothing.
Try changing it to CreateObject() and see if that helps:
Set XL = CreateObject("Excel.Application")

Understanding VBA Object Variables

I'm learning Visual Basic for Applications, and I'm a little unsure of when to use procedures. For example, is a procedure needed every time an object variable is created? Or can an object variable be created without being executed as a procedure?
Could I have just
Dim wkbInventory As Workbook
Set wkbInventory = Application.Workbooks("name.xlsm")
Thank you!
A object variable can be declared outside a procedure. So the
Dim wkbInventory As Workbook
can be in the Declarations (see https://msdn.microsoft.com/en-us/library/dd897495%28v=office.12%29.aspx#odc_ac2007_bk_BeginningAccess2007VBA_Chapt2_CreatingModules) .
The scope of the variable is dependent on where the declaration happens. See http://www.cpearson.com/Excel/Scope.aspx.
But a variable can't be instantiated outside a procedure or function. So the
Set wkbInventory = Application.Workbooks("name.xlsm")
must be inside the Procedures within a sub procedure or a function.
Almost all of the work in VBA happens inside of macros. You can declare variables outside of macros (although this changes the variable's scope compared to declaring it inside a macro) and specify some compiler options but that's about it.
This is OK:
Option Explicit
' Declaration
Dim c As Collection
Sub foo()
Set c = New Collection
c.Add "hello", "world"
MsgBox c.Count
End Sub
whereas this will generate a compile error - "Invalid outside procedure":
Option Explicit
' Declaration
Dim c As Collection
' This causes an error
Set c = New Collection
Sub foo()
c.Add "hello", "world"
MsgBox c.Count
End Sub
If you use implicit creation with your variable declaration (Dim x As New y) then that works outside of a macro. Not all objects can be created that way though. This code works:
Option Explicit
' Declaration with implicit creation
Dim c As New Collection
Sub foo()
c.Add "hello", "world"
MsgBox c.Count
End Sub
whereas this code doesn't work (you'll get error 429 - ActiveX component can't create object):
Option Explicit
' Declaration with implicit creation
Dim w As New Workbook
Sub foo()
MsgBox w.FullName
End Sub
First recommendation I would make is to put Option Explicit at the top of your code(above all subs and functions). That will help you know when things are not correct.
Dim wkbInventory As Workbook Set wkbInventory = Application.Workbooks("name.xlsm")
Will not work. You will have to do
Dim wkbInventory As Workbook
Set wkbInventory = Application.Workbooks("name.xlsm")
FYI, there is late binding versus early binding that might help you (see also this thread for a an overview) . This is something to learn unrelated to procedures so I'm just providing as an if you already din't know...
You can early bind
' Set reference to 'Microsoft Excel 8.0 Object Library' in
' the Project|References dialog (or Tools|References for VB4 or VBA).
' Declare the object as an early-bound object
Dim oExcel As Excel.Application
Set oExcel = CreateObject("Excel.Application")
' The Visible property is called via the v-table
oExcel.Visible = True
' Good practice to explicitly set object variables to "Nothing" when
' done but not mandatory (especially not for local references).
Set oExcel = Nothing
Or late bind
' No reference to a type library is needed to use late binding.
' As long as the object supports IDispatch, the method can
' be dynamically located and invoked at run-time.
' Declare the object as a late-bound object
Dim oExcel As Object
Set oExcel = CreateObject("Excel.Application")
' The Visible property is called via IDispatch
oExcel.Visible = True
' Good practice to explicitly set object variables to "Nothing" when
' done but not mandatory (especially not for local references).
Set oExcel = Nothing

Declare a Workbook as a Global variable

I am starting to write a code that will become applicable to multiple workbooks, but always uses the same reference workbook. The code will have many subs, and as I am trying to avoid to dim a variable to the reference workbook in every sub I would like to declare them Global.
First I had:
Global Locations As Excel.Workbook
Set Locations = Workbooks.Open("M:\My Documents\MSC Thesis\Italy\Merged\locXws.xlsx")
Which gave me:
"Compile error: Invalid outside procedure"
After some googling I found the following bit of code somewhere:
Public Const Locations As Excel.Workbook = "Workbooks.Open("M:\My Documents\MSC Thesis\Italy\Merged\locXws.xlsx")"
Which gave me:
"Compile error: Expected: type name"
Edit:
Using:
Public Const Locations As Excel.Workbook = "Workbooks.Open('M:\My Documents\MSC Thesis\Italy\Merged\locXws.xlsx')"
(Single quotation marks within the Workbooks.Open statement) results as the same error as when using double quotation marks.
Who knows what I am doing wrong?
Edit2:
I also tried to declare the variables in the "ThisWorkbook", following this answer using:
Private Sub Workbook_Open()
Dim Locations As Excel.Workbook
Dim MergeBook As Excel.Workbook
Dim TotalRowsMerged As String
Locations = Workbooks.Open("M:\My Documents\MSC Thesis\Italy\Merged\locXws.xlsx")
MergeBook = Workbooks.Open("M:\My Documents\MSC Thesis\Italy\Merged\DURUM IT yields merged.xlsm")
TotalRowsMerged = MergeBook.Worksheets("Sheet1").UsedRange.Rows.Count
End Sub
But then it returns an
"Object Required"
within my module.
Edit3:
I now have this which works, but has the downside of having to copy the SET lines into every Sub, there has to be a better way to do this?
Global Locations As Workbook
Global MergeBook As Workbook
Global TotalRowsMerged As String
Sub Fill_CZ_Array()
Set Locations = Application.Workbooks("locXws.xlsx")
Set MergeBook = Application.Workbooks("DURUM IT yields merged.xlsm")
TotalRowsMerged = MergeBook.Worksheets("Sheet1").UsedRange.Rows.Count
I think the most universal way for workbook global variable would be creating a module with a Public Property Get procedure. You can refer to it without calling any code first, and you don't have to worry if the file is open or not.
Here is the sample module code for one of the variables:
Private wLocations As Workbook
Public Property Get Locations() As Workbook
Const sPath As String = "M:\My Documents\MSC Thesis\Italy\Merged\locXws.xlsx"
Dim sFile As String
If wLocations Is Nothing Then
'extract file name from full path
sFile = Dir(sPath)
On Error Resume Next
'check if the file is already open
Set wLocations = Workbooks(sFile)
If wLocations Is Nothing Then
Set wLocations = Workbooks.Open(sPath)
End If
On Error GoTo 0
End If
Set Locations = wLocations
End Property
You can use it anywhere in the code as a global variable:
Sub Test()
Debug.Print Locations.Worksheets.Count
End Sub
Your question implies that you want a global workbook constant, not a variable. Because VBA doesn't allow objects to be initialised outside of a procedure, you can't have an object constant. The best you can do is have a public workbook variable that's initialised in an event.
You can declare a global variable, but you can't execute code to assign a value outside of a procedure:
Public myBook As Excel.Workbook
Sub AssignWorkbook()
Set myBook = Workbooks.Open("C:\SomeBook.xlsx") '// <~~ valid, inside sub
End Sub
Sub TestItWorked()
MsgBox myBook.Name
End Sub
So in a normal module you could have:
Public myBook As Excel.Workbook
And in your Workbook_Open() event:
Private Sub Workbook_Open()
Set myBook = Workbooks.Open("C:\SomeOtherBook.xlsx")
End Sub
Then you can use myBook elsewhere in your code without having to re-assign it.
It might be worth having a look at Chip Pearson's article about variable scope in VBA here
what you want is some sort of Factory with static properties, for example in a separate module
mFactoryWkbs
Private m_WkbLocations As Workbook
Private m_WkbMergeBook As Workbook
Public Property Get LOCATIONS() As Workbook
If m_WkbLocations Is Nothing Then
Set m_WkbLocations= Workbooks.Open("wherever")
End If
Set LOCATIONS = m_WkbLocations
End Property
Public Property Get MERGEBOOK () As Workbook
If m_WkbMergeBook Is Nothing Then
Set m_WkbMergeBook = Workbooks.Open("wherever")
End If
Set MERGEBOOK = m_WkbMergeBook
End Property
To use, just call the property where & when you need it, no extra variables (or Sets for them) required.
TotalRowsMerged = MERGEBOOK.Worksheets("Sheet1").UsedRange.Rows.Count
This is the best I can come up with until now. The result is that there is now only one place to change the name of the file, however I still need to copy the SET function within every subroutine. Not completely ideal yet, but better then nothing.
Public Const DESTBOOK = "DURUM IT yields merged.xlsm"
Global Locations As Workbook
Global MergeBook As Workbook
Global TotalRowsMerged As String
Sub Fill_CZ_Array()
Set Locations = Application.Workbooks("locXws.xlsx")
Set MergeBook = Application.Workbooks(DESTBOOK)
TotalRowsMerged = MergeBook.Worksheets("Sheet1").UsedRange.Rows.Count
Whenever I run into this, I declare wb as a public constant string:
public wb as string = "c:\location"
Then, throughout the code in the project, you can refer to
workbooks(wb).anything
This is the sort of thing I usually do when I have global variables that need to be properly initialized:
In a general code module put the following code:
Public Initialized As Boolean
Public Locations As Workbook
Sub Initialize()
If Initialized Then Exit Sub
Const fname As String = "M:\My Documents\MSC Thesis\Italy\Merged\locXws.xlsx"
On Error Resume Next
Set Locations = Workbooks(Dir(fname))
On Error GoTo 0
If Locations Is Nothing Then
Set Locations = Workbooks.Open(fname)
End If
Initialized = True
End Sub
Then in the workbook's code module put:
Private Sub Workbook_Open()
Initialize
End Sub
Furthermore, in any "gateway" sub or function (e.g. event-handlers, UDFs, etc.) which might launch your code, put Initialize (or maybe: If Not Initialized Then Initialize) as the first line. Typically most subs won't be directly launched and can rely on Locations being properly set by the caller. If you need to test something which won't run properly if the variable isn't set then you can just type initialize directly in the Immediate Window.
You could also do it with a class module and rely on the class initialiser to do the work for you when it gets used in the module:
Class module called cLocations:
Public Workbook As Workbook
Private Sub Class_Initialize()
Set Workbook = Workbooks.Open("C:\Temp\temp.xlsx")
End Sub
And where you like in your module, or anywhere for that matter:
Dim Locations As New cLocations
Sub dosomething()
Locations.Workbook.Sheets(1).Cells(1, 1).Value = "Hello World"
End Sub
And then, you can just use Locations.Workbook to refer to the locations workbook, and ThisWorkbook to refer to the workbook the code is running in and ActiveWorkbook to refer to the workbook that has focus. This way you could run your code from one workbook (ThisWorkbook), using the locations workbook (Locations.Workbook) as a reference and iterate over other workbooks (ActiveWorkbook) to add another level of automation.
If you step through the code, you will see that the class is only initialised when you hit a line of code that requires it, not when the workbook is loaded.
I must add though, in this case I think if you give us a slightly bigger picture of what you are trying to achieve we might be able to give you a solution to a better problem than the one you have hit while coding.
You could also take this a step further, and abstract to the application level, keep the locations workbook hidden, and even provide intellisense for named sheets if you know their position or their name explicitly:
Class module:
Private App As Application
Public Workbook As Workbook
Public NamedSheet As Worksheet
Private Sub Class_Initialize()
Set App = New Application
App.Visible = False
App.DisplayAlerts = False
Set Workbook = App.Workbooks.Open("C:\Temp\temp.xlsx") 'maybe open read only too?
Set NamedSheet = Workbook.Sheets("SomethingIKnowTheNameOfExplicitly")
End Sub
Public Sub DoSomeWork()
'ThisWorkbook refers to the one the code is running in, not the one we opened in the initialise
ThisWorkbook.Sheets(1).Cells(1, 1).Value = Wb.Sheets(1).Cells(1, 1).Value
End Sub
Public Function GetSomeInfo() As String
GetSomeInfo = NamedSheet.Range("RangeIKnowTheNameOfExplicitly")
End Function
And then in your module, the first time you use the variable it will be initialised in one line of code:
Dim Locations As New cLocations
Dim SomeInfo
Sub DoSomething()
SomeInfo = Locations.GetSomeInfo 'Initialised here, other subs wont re-initialise
Locations.Workbook.Sheets(1).Cells(1, 1).Value = _
ThisWorkbook.Sheets(1).Cells(1, 1).Value
Locations.NamedSheet.Cells(1,1).Value = "Hello World!"
Locations.Workbook.Save
End Sub
This solution will work only if you know the numbers and names of all the worksheets that you will use from referenced workbook.
In your module, declare worksheet public variable for all your worksheets as follows:
Public sht1 As Worksheet
Public sht2 As Worksheet
Public sht3 As Worksheet
...
Instantiate these public variables in the application load event.
Sub Workbook_Open()
Workbooks.Open ("your referenced workbook")
'Instantiate the public variables
Set sht1 = Workbooks("Test.xlsm").Sheets("Sheet1")
Set sht2 = Workbooks("Test.xlsm").Sheets("Sheet2")
Set sht3 = Workbooks("Test.xlsm").Sheets("Sheet3")
End Sub
Now you can refer these global worksheets in your sub.
For example:
Sub test()
MsgBox sht1.Range("A1").Value
MsgBox sht2.Range("A1").Value
MsgBox sht3.Range("A1").Value
End Sub
If you create a Module say ExcelMod and within that Module you have a public function or subroutine Initialize() and another one called Terminate() you can initialize and terminate Module level variables using those routines. For example I have used this before: (Note that module variables are the first thing declared at the top of the module.)
Dim excelApp As Object, wb As Workbook, ws As Worksheet
Sub Initialize()
Set excelApp = CreateObject("Excel.Application")
Set wb = Workbooks.Open("C:\SomeOtherBook.xlsx")
End Sub
Sub Terminate()
Set excelApp = Nothing
Set wb = Nothing
End Sub
The variables are part of the entire module and only get initialized and terminated with these subroutines. You can pass the variables in and out of the module as you wish and use them in ALL of this modules subroutines without having to set again. If you need to use in another module you will need to pass it to that module as you normally would.
Also as others have mentioned you can use the workbook_Open event to call the initialization sub to create the objects and set them only once if needed.
Is this what you are after?
If I understand your question correctly, you are creating a code that should work on the application level and not on workbook level. In this case why don't you create an add-in.
All the code inside the add-in will have access to all the open workbooks at application level.
You might want to create an Add-In, or use a Class module to work with properties, ...
But I'm not sure it'll be that cleaner than a simple declaration in a regular module and a call to that procedure at workbook's open will do the trick just fine too .
(I have been using this method for quite some times and haven't been bothered)
So you can use this in a (dedicated or not) regular module :
'Set the path to your files
Public Const DESTBOOK = "M:\My Documents\MSC Thesis\Italy\Merged\DURUM IT yields merged.xlsm"
Public Const LOCBOOK = "M:\My Documents\MSC Thesis\Italy\Merged\locXws.xlsx"
'Declare all global and public variables
Global Locations As Workbook
Global MergeBook As Workbook
Global TotalRowsMerged As String
'Set all variable (Procedure call from Workbook_Open)
Sub Set_All_Global_Variables()
Set Locations = Set_Wbk(LOCBOOK)
Set MergeBook = Set_Wbk(DESTBOOK)
TotalRowsMerged = MergeBook.Worksheets("Sheet1").UsedRange.Rows.Count
'...
End Sub
'Function to check if the workbook is already open or not
Function Set_Wbk(ByVal Wbk_Path As String) As Workbook
On Error Resume Next
Set Set_Wbk = Workbooks(Dir(Wbk_Path))
On Error GoTo 0
If Set_Wbk Is Nothing Then
Set Set_Wbk = Workbooks.Open(Wbk_Path)
End If
End Function
And call the procedure setting all the variables in the ThisWorkbook module :
Private Sub Workbook_Open()
Set_All_Global_Variables
End Sub

VBA leave object created opened after procedure finished

Observe the follow code:
Sub test()
Dim myObj as Object
Set myObj = CreateObject("ABC.Application")
myObj.visible = TRUE
End Sub
The application ABC was closed after the procedure finished. How do i leave it opened ? I tried Set myObj = Nothing but didn't work.
The problem is in the Dim myObj As Object statement, which is declared in local scope.
I try to explain with an example:
Dim a As Object '<-- global declaration
Sub mySub()
Dim b As Object 'local declaration
End Sub
The difference between a and b is the scope: while a has a global scope, b has a local one. Which means:
b will be collected by the garbage collector (hence destroyed) once the scope of the routine is gone (i.e. at the end of the execution)
a, instead, will not be collected as garbage after the execution because its scope is global (i.e. living until the hosting process lives).
You should be able to fix it declaring the Object globally:
Dim myObj as Object '<-- it will "survive" even after execution
Sub test()
Set myObj = CreateObject("ABC.Application")
myObj.visible = TRUE
End Sub

Best practice for creating a public object in Excel-VBA?

What is the best practice for creating an Excel-VBA data object (dictionary, list, etc.) which is accessible by all members of the application? Should it be declared as a separate module or a class module?
For example, I want to create a dictionary object which different subroutines will want to check a user input against (if it contains or not). Should this dictionary object be its own module, class module, or part of the module which contains the subroutines who use it?
Note: this question is an extension of Checking if a value is a member of a list
You can use following construction (declare your myList object as Public in the top of your module):
Public myList As Object
Sub Main()
Call InitializeList
'Do something with your Dictionary object
End Sub
Sub InitializeList()
If Not myList Is Nothing Then Exit Sub
Set myList = CreateObject("Scripting.Dictionary")
myList.Add "item1", 1
myList.Add "item2", 2
myList.Add "item3", 3
End Sub
VBA can be frustrating to people who are used to nice OOP-friendly languages like Java and C#. We need to accept VBA's limitations and simply do the best we can with what it offers.
What you're describing almost sounds like something you would declare as a Singleton in other languages.
My solution would be to create a "Main" module (not Class module). In there, create a private dictionary, and create a Public accessor function for it. This will allow your other methods - er - functions/subs to access it blindly.
Private pMyList as Scripting.Dictionary
Public Property Get MyList() as Scripting.Dictionary
If pMyList = Nothing Then
Set pMyList = new Scripting.Dictionary
pMyList("One") = "Red"
pMyList("Two") = "Blue"
pMyList("Three") = "Green"
EndIf
Set MyList = pMyList
End Property
Public Sub Cleanup
Set pMyList = Nothing
' To deallocate arrays, use:
' Erase pArray
End Sub
'--------------------------------
Public Sub SomeRandomSubInAnotherModule()
Dim theList As Scripting.Dictionary
Set theList = MyList ' If not yet initialized, will initialize
' Do whatever you need to do with theList
Set theList = Nothing ' Release the memory
End Sub
BTW, the "Cleanup" subroutine is just good practice. At the end of your macro, you should call the "Cleanup" subroutine to release memory that Excel may have allocated for any objects you've created. For Class Modules, you can put your cleanup code in
Public Sub Class_Terminate()
and it will be called automatically.
Note - the previous code would require you to add the "Microsoft Scripting Runtime" as a reference. This gives you the helpful type hints when you're working with the dictionary while you code. If you don't want to do that for some reason, use this code:
Private pMyList as Object
Public Property Get MyList() as Object
If pMyList = Nothing Then
Set pMyList = CreateObject("Scripting.Dictionary")
pMyList("One") = "Red"
pMyList("Two") = "Blue"
pMyList("Three") = "Green"
EndIf
Set MyList = pMyList
End Property
Public Sub Cleanup
Set pMyList = Nothing
End Sub