Storing range reference in a global variable - vba

I'm using several named ranges located in different worksheets. I need to read from and write to those ranges in many situations throughout my VBA code.
So my question is: what is the proper way to store those range references in global variables for quick access? Or is there a better way?
I tried declaring global variables:
Public WS_BOARD As Worksheet
Public RNG_BOARD As Range
and initializing them on Workbook_Open:
Private Sub Workbook_Open()
Set WS_BOARD = Worksheets("BOARD")
Set RNG_BOARD = WS_BOARD.Range("NR_BOARD")
End Sub
This works okay except that if my code crashes those global variables are reset to Nothing and cannot be used any further (i.e. in worksheet event handlers).
Of course, I can always use something like
Worksheets("BOARD").Range("NR_BOARD")
everywhere in my code but I think it will affect performance because it obviously needs to lookup objects using string names, let alone it being not DRY.

One way is to "load" them once into memory and then pass them as arguments into other subs/functions.
Passing Variables By Reference And By Value
Second, you can declare them as module-scope variables (something like public, but only in the module you declare them).
Scope of variables in Visual Basic for Applications
Third, your way.
Scope of variables in Visual Basic for Applications

Every worksheet has a Name property (the tab caption) and a CodeName property. If you use the CodeName in your code, you don't need a variable. Like a userform, a sheet's code name is auto instantiated when you use it. Select the worksheet under your project in the Project Explorer (Ctrl+R), go to the Propeties (F4) and change the (Name) to a meaninful CodeName. The Project Explorer will then show the sheet as
wshDataEntry (Data Entry)
if you made the CodeName wshDataEntry and the tab said Data Entry.
For Ranges, I used defined names in the spreadsheet. It's like a global variable that never goes out of scope. If you have, say, and interest rate cell that you need to read in various procedure, name it Interest_Rate. Then at the top of every procedure where you need it
Set rInterest = wshDataEntry.Range(gsNMINTEREST)
where gsNMINTEREST is a global string variable holding "Interest_Rate". If the cell ever moves, you only need to move your named range for the code to work. If you change the name to "LiborPlusFour" you only need to update your global variable. Pretty DRY.
Yes there is a slight performance hit making VBA look up the named range every time. I wouldn't worry about that unless you've calculated a performance problem and identified it as the drag.
One way that I keep global variables (of course I'm very judicious using them) in scope is an Initialize procedure.
Public Sub Initialize()
If gclsApp Is Nothing Then
Set gclsApp = New CApp
'do some other setup
End If
End Sub
Then I call the Initialize procedure at the top of any module I need it. If it's Not Nothing, then it doesn't reset the variable. I'm not sure the performance implications of checking for Nothing and just looking up a named range, though.

Global variables are generally not a good idea. In some cases they're necessary, but this isn't one of those cases.
I don't think you should worry about the performance impact of looking up a few ranges by their string name. I have sheets with hundreds of named ranges, and I have never, ever observed this having a negative impact on performance.
Do you have specific evidence indicating that this is slowing down your code? If not, then don't waste time worrying about it.
I often do this to define ranges:
Type RangesType
board As Range
plank As Range
wood As Range
End Type
Function GetRanges() As RangesType
With GetRanges
Set .board = Range("board")
Set .plank = Range("plank")
Set .wood = Range("wood")
End With
End Function
Then I use them like this:
Sub something()
Dim rngs As RangesType: rngs = GetRanges
rngs.board = "My favourite board"
rngs.wood = "Chestnut"
'etc.
End Sub
Yes, ranges will get fetched by their name repeatedly if you have many subs, and no, this isn't likely to have any noticeable effect on performance.

Related

Variable in a form won't keep its value after being used in the call to another form

I have a form with a variable in it called "VigilTable." This variable gets its value from the calling string OpenArgs property.
Among other things, I use this variable in the call string when opening other forms.
But it only works the first call.
MsgBox VigilTable before the call will always show "Spring2022" or whatever on the first call but always comes up blank on succeeding calls (and I get "invalid use of NULL" when the called form attempts to extract the value from OpenArgs). The variable is dimmed as String in the General section of the form's VBA code.
So what's happening here? And can I fix it?
Thanks.
Ok, so you delcared a variable at the form level (code module) for that given form.
and we assume that say on form load, you set this varible to the OpenArgs of the form on form load.
So, say like this:
Option Compare Database
Option Explicit
Public MyTest As String
Private Sub Form_Load()
MyTest = Me.OpenArgs
End Sub
Well, I can't say having a variable helps all that much, since any and all code in that form can use me.OpenArgs.
but, do keep in mind the following:
ONLY VBA code in the form can freely use that variable. It is NOT global to the applcation, but only code in the given form.
However, other VBA code outside of the form can in fact use this variable. But ONLY as long as the form is open.
So, in the forms code, you can go;
MsgBox MyTest
But, for VBA outside of the form, then you can get use of the value like this:
Msgbox forms!cityTest.MyTest
However, do keep in mind that any un-handled error will (and does) blow out all global and local variables. So, maybe you have a un-handled error.
Of course if you compile (and deploy) a compiled accDB->accDE, then any errors does NOT re-set these local and global variables.
but, for the most part, that "value" should persist ONLY as long as the form is open, and if you close that form, then of course the values and variables for that form will go out of scope (not exist).
Now, you could consider moving the variable declare to a standard code module, and then it would be really global in nature, but for the most part, such code is not recommended, since it hard to debug, and such code is not very modular, or even easy to maintain over time.
So, this suggests that some error in VBA code is occurring, and when that does occur, then all such variables are re-set (but, the noted exception is if you compile down to an accDE - and any and all variables will thus persist - and even persist their values when VBA errors are encountered.
For a string variable, a more robust solution not influenced by any error, should be writing/reading in/from Registry. You can use the, let as say, variable (the string from Registry) from any workbook/application able to read Registry.
Declare some Public constants on top of a standard module (in the declarations area):
Public Const MyApp As String = "ExcelVar"
Public Const Sett As String = "Settings"
Public Const VigilTable As String = "VT"
Then, save the variable value from any module/form:
SaveSetting MyApp, Sett, VigilTable , "Spring2022" 'Save the string in Regisgtry
It can be read in the next way:
Dim myVal as String
myVal = GetSetting(MyApp, Sett, VigilTable , "No value") 'read the Registry
If myVal = "No value" Then MsgBox "Nothing recorded in Registry, yet": Exit Sub
Debug.print myVal
Actually, this proved not to be the the answer at all.
It was suggested that I declare my variables as constants in the Standard module but I declared them as variables. It appeared at first to work, at least through one entire session, then it ceased to work and I don't know why.
If I declare as constants instead, will I still be able to change them at-will? That matters because I re-use them with different values at different times.
I didn't do constants but declaring VigilName in the Standard module and deleting all other declarations of it fixed both problems.
While I was at it I declared several other variables that are as generally used and deleted all other declarations of them as well so that at least they'll be consistently used throughout (probably save me some troubleshooting later.
Thanks to all!

Defining variables on Excel VBA

Dears,
I'm starting a new project using Excel VBA and I would like to declare some variables to be loaded during the system's initialization, but during the utilization it may be changed. How can I do it?
Example:
When the system is loaded, the variable RADIO must be equal to "OFF", but during the utilization the user will change this value to "ON", and even to "OFF" again.
This information (RADIO) will be used during the system's utilization in some other pages.
How can I do it?
Thank you so much
Bruno Lelli
Like Tim Williams said, you need a global variable. Best practice is to have it declared in the declaration block (very first lines) of a code mudule.
To have it available within the code module only:
Private booRadio as Boolean
To have it available within the whole VBA project, i.e. all modules, user forms and for the workbook and worksheet events:
Public booRadio as Boolean
When VBA starts, all Boolean variables get initialized to be False (likewise, all Integer or Long get initialized to be 0, String to be "", etc.). This can be used to have a known initial status after system start.
Or - which is better to read in code reviews and like-I-feel more robust - you use an event to initilize the startup status. E.g. like ashleedawg said, you may use the workbook_open event for this. In that case, you need your variable dacelared with the Public statement, if you want to access it outside the ThisWorkbook code.
EDIT:
Copy this code into the ThisWorkbook code module:
Private Sub Workbook_Open()
booRadio = True
End Sub
This will use the event Workbook_Open to initialize the variable upon every opening of the Excel file, because that event gets automatially raised then.

VBA Best Practices: working with small subs and functions, passing values between routines

I learned from internet that our vba macros (subs and functions) should be more or less the size of the screen.
But all my code is written exactly in the opposite: a have a huuuge main sub that calls functions and small subs.
My question is: if I decide to convert my main huge sub into small subs (sub1, sub2, sub3, etc), then what should be the best way to keep the value of a variable that was assigned in sub1 and use it in sub2, that was assigned in sub2 and use it in sub3, and so on? If I have no answer to this question, I am going to save those values in a sheet, but maybe this way is going to cause me trouble that I cannot see by now. Thanks for any help.
A Sub or a Function should have a single and clear purpose, told by its name. If you break down your code in such chunks, you will know what information those methods will need and deliver. At the same time they will become smaller. If you have to choose between size and clarity, go for clarity.
Do NOT put your values in a sheet. That is just a different and less efficient way of using global variables, which is something you should avoid (not at all cost, but almost).
Both methods below would work. In my personal opinion, method 1 keeps code more simple (you avoid to pass big bunches of parameters to each macro) but depending on your need you might choose one or the other.
Global variables
Your code would look something like this:
'Global variables declaration
Dim a As Integer
Dim b As Integer
'
Public Sub mainRoutine()
setA '<-- call first macro
setB '<-- call second macro
MsgBox a + b '<-- show A + B (2 + 3 = 5)
End Sub
'code of sub-routines
Private Sub setA()
a = 2 '<-- since a is global, it will keep the value
End Sub
Private Sub setB()
b = 3 '<-- since b is global, it will keep the value
End Sub
Please note: global means that the value of this variable will live for all execution of your program. Memory will be released once the program ends.
Pass variables as ByRef parameters
Public Sub mainRoutine()
Dim a As Integer '<-- a is local
a = 2 '<-- setting a to 2
setA a '<-- passing a as ByRef parameter
MsgBox a '<-- a is now 3
End Sub
Private Sub setA(ByRef a As Integer)
a = 3 '<-- setting a as 3.
End Sub
Of course, the above method would only allow to keep a = 2/3 during the execution of mainRoutine. But if at some point you execute another macro called by another stack (for example another button in the spreadsheet), you wouldn't be able to access the value of a as you would in method 1.
Important: no, variables on the spreadsheet is not a good idea, unless you don’t need to keep the value after closing and reopening the spreadsheet (in that case you would be using the spreadsheet as a sort of database).
VBA can be considered as an object oriented programming language (OOP).It has 3 of the 4 pillars of OOP:
Abstraction
Encapsulation
Polymorphism
This is discussed in Is VBA an OOP language, and does it support polymorphism?
OOP means understanding the SOLID principles.
Single Responsibility Principle
Open Closed Principle
Liskov Substitution Principle
Interface Segregation Principle
Dependency Inversion Principle
Best practice is to adhere to these principles and the first is the Single Responsibility Principle.
This states that your functions/subs should have one responsibility and focus on doing one thing.
Advantages (amongst others):
The reduced code complexity and dependencies mean:
1) Easier to debug as your track down which sub/function the error is raised in
2) Units tests can be more easily be built around code that is based on single responsibility.
3) Can be more easily slotted into a wider code base
To use variables between subs/functions consider:
1) Either public variables, and/or
2) Passing variables as arguments
Other info:
OOP VBA pt.1: Debunking Stuff

Global variable loses its value

On this Access form I am working on I have a global variable that take its value from another form on its Form_Load event. For some reason unknown to me the variable "loses its value" (becomes = "") after some time elapses or some event occurs. I have not been able to notice anything in particular that triggers this behaviour. Are global variables reset after some time of "inactivity" on the form ?
Here is how I set the global variables I am talking about:
Private Sub Form_Load()
'...
Set prev_form = Form_Identification.Form
PasswordSybase = prev_form.Password.Value & vbNullString
UserSybase = prev_form.UserID.Value & vbNullString
'...
End Sub
An alternate solution (Only 2007 and onwards) I've started using is TempVars instead of globals in the odd situation I "needed" something global. It's a collection and it persists for the duration of the application unless you explicitly release it. So in some cases I feel its more useful than globals and in some cases worse.
TempVars.Add myVarName, myVarValue ' To initialize
TempVars.Item(myVarName) = newVarValue ' To reference and assign a new value
TempVars.Remove(myVarName) ' To release
Quick search should show you more lot references, but I've included link to a basic one
http://blogs.office.com/b/microsoft-access/archive/2010/09/27/power-tip-maximize-the-user-of-tempvars-in-access-2007-and-2010.aspx
I do hope that visitors see this post, as it provides an important additional piece.
Even if you declare a variable globally, it appears that - in the event that you set that variable's value in a form module - that value is lost when you UNLOAD the form.
The solution (in my case) was as simple as replacing:
Unload Me
...with...
Me.Hide
The variables (and objects) that I set in that code module then retained their values through the entire lifetime of the application instance.
This may help:
https://accessexperts.com/blog/2011/01/12/multi-session-global-variables/
Juan Soto explains how to use a local table to keep variables and how to call them when needed. It may serve your purpose in 2000 since TempVars isn't an option. You could always delete the variables "on close" of the database so that UID and PWD aren't kept.
You can create a "fake" global variable by
creating a form (e.g. named frmGlobal)
make sure the form is always open but hidden
create a TextBox for each global variable you want (e.g. tVar1)
in your code, reference as e.g. Form_frmGlobal.tVar1
The disadvantage is that an unbound text box may not give you a specific data type you want
The two ways around that are
in your code, explicitly convert the textbox to the data type when referencing the global variable
e.g Clng(Form_frmGlobal.tVar1)
another option is create a one-row table and bind your textboxes on your hidden form to the table, so your data types are enforced
A bonus of this method is you can use for persistent storage between sessions
Caveat: make sure this table is local to a single user only, in the front end database file (don't want to put it in the back end database because of multi-users over-writing each other). This assumes you are using front end + back end separated databases, with distribution of front end to each user's workstation.
I see nothing in that statement that tells me it's a global variable. You set global variables above ALL Subs/Functions and below an Options Compare statement in a module, by stating:
PUBLIC X as string
Any other variable is only good until the sub or function has completed.
Also, Global variables MUST be declared on a PROPER MODULE. You can't declare them on a form's module.

Is it possible to declare a public variable in vba and assign a default value?

I want to do this but it won't compile:
Public MyVariable as Integer = 123
What's the best way of achieving this?
.NET has spoiled us :)
Your declaration is not valid for VBA.
Only constants can be given a value upon application load. You declare them like so:
Public Const APOSTROPHE_KEYCODE = 222
Here's a sample declaration from one of my vba projects:
If you're looking for something where you declare a public variable and then want to initialize its value, you need to create a Workbook_Open sub and do your initialization there.
Example:
Private Sub Workbook_Open()
Dim iAnswer As Integer
InitializeListSheetDataColumns_S
HideAllMonths_S
If sheetSetupInfo.Range("D6").Value = "Enter Facility Name" Then
iAnswer = MsgBox("It appears you have not yet set up this workbook. Would you like to do so now?", vbYesNo)
If iAnswer = vbYes Then
sheetSetupInfo.Activate
sheetSetupInfo.Range("D6").Select
Exit Sub
End If
End If
Application.Calculation = xlCalculationAutomatic
sheetGeneralInfo.Activate
Load frmInfoSheet
frmInfoSheet.Show
End Sub
Make sure you declare the sub in the Workbook Object itself:
Just to offer you a different angle -
I find it's not a good idea to maintain public variables between function calls. Any variables you need to use should be stored in Subs and Functions and passed as parameters. Once the code is done running, you shouldn't expect the VBA Project to maintain the values of any variables.
The reason for this is that there is just a huge slew of things that can inadvertently reset the VBA Project while using the workbook. When this happens, any public variables get reset to 0.
If you need a value to be stored outside of your subs and functions, I highly recommend using a hidden worksheet with named ranges for any information that needs to persist.
Sure you know, but if its a constant then const MyVariable as Integer = 123 otherwise your out of luck; the variable must be assigned an initial value elsewhere.
You could:
public property get myIntegerThing() as integer
myIntegerThing= 123
end property
In a Class module then globally create it;
public cMyStuff as new MyStuffClass
So cMyStuff.myIntegerThing is available immediately.
Little-Known Fact: A named range can refer to a value instead of specific cells.
This could be leveraged to act like a "global variable", plus you can refer to the value from VBA and in a worksheet cell, and the assigned value will even persist after closing & re-opening the workbook!
To "declare" the name myVariable and assign it a value of 123:
ThisWorkbook.Names.Add "myVariable", 123
To retrieve the value (for example to display the value in a MsgBox):
MsgBox [myVariable]
Alternatively, you could refer to the name with a string: (identical result as square brackets)
MsgBox Evaluate("myVariable")
To use the value on a worksheet just use it's name in your formula as-is:
=myVariable
In fact, you could even store function expressions: (sort of like in JavaScript)
(Admittedly, I can't actually think of a situation where this would be beneficial - but I don't use them in JS either.)
ThisWorkbook.Names.Add "myDay", "=if(isodd(day(today())),""on day"",""off day"")"
Square brackets are just a shortcut for the Evaluate method. I've heard that using them is considered messy or "hacky", but I've had no issues and their use in Excel is supported by Microsoft.
There is probably also a way use the Range function to refer to these names, but I don't see any advantage so I didn't look very deeply into it.
More info:
Microsoft Office Dev Center: Names.Add method (Excel)
Microsoft Office Dev Center: Application.Evaluate method (Excel)
As told above, To declare global accessible variables you can do it outside functions preceded with the public keyword.
And, since the affectation is NOT PERMITTED outside the procedures, you can, for example, create a sub called InitGlobals that initializes your public variables, then you just call this subroutine at the beginning of your statements
Here is an example of it:
Public Coordinates(3) as Double
Public Heat as double
Public Weight as double
Sub InitGlobals()
Coordinates(1)=10.5
Coordinates(2)=22.54
Coordinates(3)=-100.5
Heat=25.5
Weight=70
End Sub
Sub MyWorkSGoesHere()
Call InitGlobals
'Now you can do your work using your global variables initialized as you wanted them to be.
End Sub
You can define the variable in General Declarations and then initialise it in the first event that fires in your environment.
Alternatively, you could create yourself a class with the relevant properties and initialise them in the Initialise method
This is what I do when I need Initialized Global Constants:
1. Add a module called Globals
2. Add Properties like this into the Globals module:
Property Get PSIStartRow() As Integer
PSIStartRow = Sheets("FOB Prices").Range("F1").Value
End Property
Property Get PSIStartCell() As String
PSIStartCell = "B" & PSIStartRow
End Property
there is one way to properly solve your question. i have the same concern with you for a long time. after searching and learning for a long time, finally i get a solution for this kind of question.
The solution is that no need to declare the variable and no need to set value to the variable, and even no need VBA code. Just need the "named range" in excel itself.
For example, the "A1" cell content is "hello, world". and we define the "A1" cell a name as "hello", that is, the "A1" cell have a name now, it's called "hello".
In VBA code, we just need use this method [hello], then we can get the "A1" value.
Sub test()
msgbox [hello]
end sub
the msgbox will show "Hello, word".
this way, we get a global variable without any declaration or assignment. it can be used in any Sub or Function.
we can define many named range in excel, and in VBA code we just use [] method to get the range value.
in fact, the [hello] is a abbreviation of the function Evaluate["Hell"], but it's more shorter.
It's been quite a while, but this may satisfy you :
Public MyVariable as Integer: MyVariable = 123
It's a bit ugly since you have to retype the variable name, but it's on one line.