Excel VBA Global Range variable out of scope after sub procedure completes - excel

I am perplexed why my global variable within a module fall out of scope at the conclusion of a sub procedure.
I declare the range at the top of the module out side of all subproc and functions
as below
Option Explicit
Dim TIMEDATA As Range
Dim FREQDATA As Range
Const StartLoc = "B4"
Const flowLoc = "F4"
Const dtLoc = "J8"
In my subproc I define one of the ranges.
Public Sub PortandConvertData()
<SNIP>
Set TIMEDATA = calcSheet.Range(Cells(2, 2).Address, Cells(2 + dataSize, 2).Address)
End Sub
After the sub completes in the watch window I see the variable TIMEDATA go from
Range/Range to Range and the value go from correct to simply out of context.
I want to store data in the module rather than pasting in a sheet or something.
Any help is much appreciated

Make sure that the Context in the Watch properties includes the Procedure/Module you are actually watching. You can make sure by setting the context to All Modules:
From the Watches panel: Right Click the Expression --> Edit Watch --> From the Context group set Procedure/Module to All.
If this is not the actual issue, then you are having the same issue I'm having from Access VBA.
This thread explains the same:
ThisWorkbook not holding global variable value to cancel ontime()

Related

WorkbookOpen event sometimes not firing - event used for set global variables

I have a really basic problem with my projects and I would like to know which approach is the best. I like to use (hated) globals, only for a few the most important objects in a workbook.
I am declaring e.g. my data tables in a such way:
'#Folder("Main")
Option Exclicit
Public tblDatabase As Listobject
Public tblReport As Listobject
Sub setMyTables()
Set tblDatabase = wsDatabase.ListObjects("tDatabase")
Set tblReport = wsReport.ListObjects("tReport")
End Sub
In the past I used this macro before actions on the table, e.g.:
Function getIdFromDatabaseTable() As Variant
' set variable-object to use
setMyTables <-- I used to table-setting-sub in every
macro which requires one of my table
' get ID from table
Dim arr As Variant
arr = tblDatabase.ListColumns("ID").DataBodyRange.Value2
' assign array to function result
getIdFromDataTable = arr
End Function
But why I had to begin almost every macro with calling setMyTables() macro? So I've started to use workbook open event to set my object variables:
[code in ordinary Module]
'#Folder("Main")
Option Exclicit
Public tblDatabase As Listobject
Public tblReport As Listobject
And call setMyTables() macro in Workbook_Open() event code. And here my problem is:
[TLTR] Setting variable-objects in Workbook-Open event seems unrielable. It seems it is not firing sometimes. I am sure that no macro error would reset the project and 'clear' already set variables, because sometimes it throws error on the very first macro run. It is not working occasionally and I don't know what pattern behind it is, I send Excel workbooks to my clients, and it's hard to debug what's realy going on there.
Additional comments
I've just read that this could happen if file is not in trusted localizations, I would like get to know best approach to handle declaring the most used objects globally (if possible without modifying someones trusted folders or another local-PC settings).
I know that I can set a 'flag' bool variable such as wasWorkbookOpenEventFired, but I would have to call checking function or make ifs on almost every Sub or Function in a workbook. So I think it isn't good solution too. Thanks for hints!
You'd have more robust results if you define public functions which each return a specific table, and use those instead of global variables:
Function DatabaseTable() As ListObject
Static rv As ListObject '<< cache the table here
'if your code gets reset then this will just re-cache the table
If rv Is Nothing then Set rv = wsDatabase.ListObjects("tDatabase")
Set DatabaseTable = rv
End Function

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)

Defining cross-module variables with values

I'm developing a bunch of Excel Macros for making my life easier. One part of different macros is inserting a picture into sheets. For this reason, I would like to save the path to the images in a global location and then access it via a variable (so that I don't have to manually adjust the paths in every macro if it changes). I use one module per macro
In my own module "Variables" I defined a variable as Public or Global and then assigned a value via a sub. If I now access this variable via another module, I get an empty MsgBox.
For test purposes I use a string which I want to display via an MsgBox.
Modul 1:
Public test As String
Sub variablen()
test = "String for Test "
End Sub
Modul 2:
Public Sub testpublic()
MsgBox (test)
End Sub
I recommend to use a constant instead of a variable:
Module 1
Option Explicit
Public Const MyPath As String = "C:\Temp"
Module 2
Option Explicit
Public Sub ShowPath()
MsgBox MyPath
End Sub
I also recommend to activate Option Explicit: In the VBA editor go to Tools › Options › Require Variable Declaration.
If you do it like you did test is empty until it was initialized by running the procedure variablen first. If you use Public Const no initialization is required.
so that I don't have to manually adjust the paths in every macro if it changes
If it ever needs to change, then it semantically isn't a Const. The key to writing code that you don't constantly need to modify is to separate the code from the data.
A file path that sometimes needs to change can be seen as some kind of configuration setting.
Have a module that is able to read the settings from wherever they are, and return the value of a setting given some key.
The settings themselves can live on a (hidden?) worksheet, in a ListObject table with Key and Value columns, and looked up with INDEX+MATCH functions (using the early-bound WorksheetFunction functions will throw run-time errors given a non-existing key string):
Option Explicit
Public Function GetSettingValue(ByVal key As String) As Variant
With SettingsSheet.ListObjects(1)
GetSettingValue = Application.WorksheetFunction.Index( _
.ListColumns("Value").DataBodyRange, _
Application.WorksheetFunction.Match(key, .ListColumns("Key").DataBodyRange, 0))
End With
End Function
The Variant will retain the subtype of the Value, so for a String value you get a Variant/String; for a Date value you get a Variant/Date, for a numeric value you get a Variant/Double, and for a TRUE/FALSE value you get a Variant/Boolean.
Now when the file path needs to change, your code does not:
Dim path As String
path = GetSettingValue("ImageFolderPath")
And if you need more settings, you have no code to add, either:
Dim otherThing As String
otherThing = GetSettingValue("OtherThing")
All you need to do is to make sure the string keys being used match the contents of the Key column in your SettingsSheet.

Calling a VBA form from a button causes UserForm_Initialize to run twice, breaking my code?

Hello wonderful VBA community,
I'm still really new to vba and am trying to learn a lot. Thank you in advance for looking through my code and my description of the issue I'm facing.
I have a button on a page that calls a new Userform.
CODE SNIPPET 1:
Sub btnShowDetails_Click()
Call frmShowDeets.ShowDeets
End Sub
... which calls the next bit of code in the 'frmShowDeets' UserForm:
CODE SNIPPET 2:
Public Sub ShowDeets()
Dim frm As frmShowDeets
Set frm = New frmShowDeets 'this line triggers the Userform_Initialize() event below
frm.Show
End Sub
... triggering:
CODE SNIPPET 3:
Private Sub UserForm_Initialize()
Dim comboBoxItem As Range
For Each comboBoxItem In ContactList.Range("tblContactList[CompanyName]")
'^refers to unique values in a named range
With Me.boxCompanySelection
.AddItem comboBoxItem.Value
End With
Next comboBoxItem
End Sub
So at this point, the form I want to display has values loaded in its one combobox for user selection. The user selects a company and the Combobox_Change event triggers other routines that pull information for that company.
CODE SNIPPET 4:
Public Sub boxCompanySelection_Change()
Call frmShowDeets.PullData
End Sub
Sub PullData()
Dim numCompanies As Long
numCompanies = ContactList.Range("B6").Value 'this holds a count of the rows in the named range
Dim FoundCell As Range
Set FoundCell = ContactList.Range("tblContactList[Company Name]").Find(What:=boxCompanySelection.Text, LookIn:=xlValues, LookAt:=xlWhole)
Dim CompanyRow As Long
CompanyRow = FoundCell.Row
With ContactList
'pull a bunch of the company's details
End With
End Sub
Here is where it gets weird... Once the form is shown and the user selects one of the combo box items, triggering the Combobox_Change event the code breaks because the 'What:=boxCompanySelection.Text' part of the Range().Find method reads as "" empty (even though Code Snippet 3 is meant to load in company names and Code Snippet 4 is only triggered when the user selects one of those company names from the combobox) and I shouldn't need to build something to handle 'not found' exceptions since the only possible values should be the ones pulled in from my named range.
From stepping through the code, I have determined that for some reason, Code Snippets 2 and 3 run TWICE before Snippet 4 is run. Does anyone know what about my code is causing this to happen? I'm thinking there's a disconnect between the form that is shown and loaded with combobox values and whatever Code Snippet 4 is reading data from.
What is weirder is that if I run the code starting from Code Snippet 2 (ignoring the button call in Code Snippet 1), the form works as intended and from what I can tell 2 and 3 are only run once.
The problem is probably something simple I'm overlooking but I just cannot figure out what it is. Thanks again!
You have to understand that a form is an object - exactly as any other class module, except a form happens to have a designer and a base class, so UserForm1 inherits the members of the UserForm class.
A form also has a default instance, and a lot of tutorials just happily skip over that very important but rather technical bit, which takes us exactly here on Stack Overflow, with a bug involving global state accidentally stored on the default instance.
Call frmShowDeets.ShowDeets
Assuming frmShowDeets is the name of the form class, and assuming this is the first reference to that form that gets to run, then the UserForm_Initialize handler of the default instance runs when the . dot operator executes and dereferences the object. Then the ShowDeets method runs.
Public Sub ShowDeets()
Dim frm As frmShowDeets
Set frm = New frmShowDeets 'this line triggers the Userform_Initialize() event below
frm.Show
End Sub
That line triggers UserForm_Initialize on the local instance named frm - which is an entirely separate object, of the same class. The Initialize handler runs whenever an instance of a class is, well, initialized, i.e. created. The Terminate handler runs when that instance is destroyed.
So ShowDeets is acting as some kind of "factory method" that creates & shows a new instance of the frmShowDeets class/form - in other words whatever happened on the default instance is irrelevant beyond that point: the object you're working with exists in the ShowDeets scope, is named frm, and gets destroyed as soon as it goes out of scope.
Remove the ShowDeets method altogether. Replace this:
Call frmShowDeets.ShowDeets
With this:
With New frmShowDeets
.Show
End With
Now the Initialize handler no longer runs on the default instance.
What you want, is to avoid using the default instance at all. Replace all frmShowDeets in the form's code-behind, with Me (see Understanding 'Me' (no flowers, no bees)), so that no state ever accidentally gets stored in the default instance.
Call frmShowDeets.PullData
Becomes simply:
Call Me.PullData
Or even:
PullData
Since Call is never required anywhere, and the Me qualifier is always implicit when you make a member call in a class module's code.

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.

Resources