better way to call global config variables from another modules - vba

I have three modules in my VBA project:
module 1: config_variable
module 2: main_script1
module 3: main_script2
In "config_variable", the codes like this:
Public dir_source1 As String
Public dir_source2 As String
Public dir_source3 As String
Sub global_config()
dir_source1 = range("A2").value
dir_source2 = range("A3").value
dir_source3 = range("A4").value
End Sub
I manage all configurations in this script. And I call this "global_config" Sub for each Sub in module 2 and module 3 to get global variables. Like this:
module 2 - main_script1:
Sub my_sub1()
' call global variables
Call config_variable.global_config
' main codes for my_sub1
.....
End Sub
Sub my_sub2()
' call global variables
Call config_variable.global_config
' main codes for my_sub2
.....
End Sub
module 3 - main_script2:
Sub my_sub3()
' call global variables
Call config_variable.global_config
' main codes for my_sub3
.....
End Sub
The problem is that I need to call the line "Call config_variable.global_config" in the beginning of each Sub.
Is it possible I can just run this line once and every Sub in the same modules can catch the global variables? Like this:
' call global variables on top of each modules
Call config_variable.global_config
Sub my_sub1()
' main codes for my_sub1
.....
End Sub
Sub my_sub2()
' main codes for my_sub2
.....
End Sub

Call the setting of the global variables when you launch your workbook or project file:
Call config_variable.global_config
Of course, if the range values may change during a session, you will have to reset the global variabels after such a change.

If settings values are stored in sheet cells, there is no need to move them to global variables at all. You can take those values directly from the cells. For convenience, you can name these cells and refer to them, for short, with [] (syntactic sugar = Evaluate()). If the settings are not called very often, this method may be suitable, requiring no variables to be updated. For example:
Sub run_once() ' or make names manually
For Each x In Array("dir_source1:A2", "dir_source2:A3", "dir_source3:A4")
y = Split(x, ":")
ThisWorkbook.Names.Add y(0), ThisWorkbook.Worksheets("Settings").Range(y(1))
Next
End Sub
Sub UsageExample()
Debug.Print "dir_source1 = " & [dir_source1] ' equivalent for ThisWorkbook.Worksheets("Settings").Range("A2")
End Sub

If "config_variable" is a standard module (as it looks to be), you can simple call the sub as global_config. It is not Private and it will be called. But, I should suggest you to call it only if one of the Public variable is "" (nullString):
If dir_source1 = "" Then global_config
In this way, even if an error occurred and the Public variable lost their value, they will be used only after global_config runs and give values to them.

Related

VBA use string for range

I need help for my VBA tool.
In my program I define a string keyword:
Public Sub SetKeyword()
Call Define_Variables.variables
Call Database.Allocation
strKeyword="KINDACC"
Now I would like to set the name of the range keyword with my string keyword, so it looks like this:
Public Sub SetKeyword()
Call Define_Variables.variables
Call Database.Allocation
strKeyword="KINDACC"
rngKeyword= r_KINDACC
End Sub
In my module "Database" I have those r_strKeywords already defined:
Sub Allocation()
'General accident information
...
Set r_KINDACC = Worksheets("Database").Range("N4:N14")
End sub
So later with my GUI I would like to define a string-keyword and my program automatically defines my range-keyword with my string keyword:
Public Sub SetKeyword()
Call Define_Variables.variables
Call Database.Allocation
strKeyword=blabla
rngKeyword= r_strKeyword
End Sub
Is that possible?
I hope it's not too confusing
thanks!!
Viktoria

How do I make a function return a value to be used immediately as part of the calling script?

I want to make a function that I can wrap around certain things and change the output based on the current user.
Assume there is a master user called "av". If the person currently using the DB is "av", then I need to alter the VBA code to allow him better priveleges.
Public Function masteruser(input_parm)
If GetUser = "av" Then
masteruser = ""
Else
masteruser = input_parm
End Function
I plan on using the function above like this:
Public Sub blah_FormTimer()
'rest of code
masteruser(Application.Quit)
'rest of code
End Sub
In the sub above, if the current user is "av", then the Application.Quit will effectively be removed. If not, then the code reads Application.Quit like normal.
How do I handle the input parameter in such a way as to return it to work with the rest of the VBA script like normal?
You can first create a wrapper sub/function, because Application.Quit is a method call and not a function call:
Public Sub QuitApplication()
Application.Quit
End Sub
And then use the following code:
Public Function masteruser(input_parm As String)
If GetUser = "av" Then
masteruser = ""
Else
Application.Run input_parm
End If
End Function
And of course
Public Sub blah_FormTimer()
'rest of code
masteruser "QuitApplication"
'rest of code
End Sub
You can of course create a function that creates a module, creates a wrapper sub/function, then pastes the code that you pass it in that function and executes it, and then deletes that module, but I wouldn't suggest it.
You can also make one especially for method calls, which would prevent the need for making wrapper functions for them:
Public Function masterusermethod(objObject as object, strMethod As String)
If GetUser = "av" Then
masteruser = ""
Else
CallByName objObject, strMethod, VbMethod
End If
End Function
(If anyone knows a more efficient way to do this, I'd like to know)

Excel VBA - QueryTable AfterRefresh function not being called after Refresh completes

I am developing an Excel (2010+) Application using VBA and have run into an issue where the AfterRefresh event function is not being invoked once the query finishes executing.
I have not been able to find many decent resources or documentation for how to have this event function triggered in a Class Module. I decided to use the Class Module design route instead of putting the event handlers in the worksheet after receiving a response to an earlier question about QueryTables (found here Excel VBA AfterRefresh).
Here is the code for my Class Module called CQtEvents
Option Explicit
Private WithEvents mQryTble As Excel.QueryTable
Private msOldSql As String
' Properties
Public Property Set QryTble(ByVal QryTable As QueryTable): Set mQryTble = QryTable:
End Property
Public Property Get QryTble() As QueryTable: Set QryTble = mQryTble:
End Property
Public Property Let OldSql(ByVal sOldSql As String): msOldSql = sOldSql:
End Property
Public Property Get OldSql() As String: OldSql = msOldSql:
End Property
Private Sub Class_Initialize()
MsgBox "CQtEvents init"
End Sub
' Resets the query sql to the original unmodified sql statement
' This method is invoked when the Refresh thread finishes executing
Private Sub mQryTble_AfterRefresh(ByVal Success As Boolean)
' Problem is here
' This function is never called :( Even if the query successfully runs
Me.QryTble.CommandText = Me.OldSql
End Sub
Here is a quick snapshot of the code the creates an instance of this class, finds a relevant QueryTable, then calls Refresh
Option Explicit
Sub RefreshDataQuery()
'Dependencies: Microsoft Scripting Runtime (Tools->References) for Dictionary (HashTable) object
'From MGLOBALS
cacheSheetName = "Cache"
Set cacheSheet = Worksheets(cacheSheetName)
Dim querySheet As Worksheet
Dim interface As Worksheet
Dim classQtEvents As CQtEvents
Set querySheet = Worksheets("QTable")
Set interface = Worksheets("Interface")
Set classQtEvents = New CQtEvents
Dim qt As QueryTable
Dim qtDict As New Scripting.Dictionary
Set qtDict = UtilFunctions.CollectAllQueryTablesToDict
Set qt = qtDict.Item("Query from fred2")
''' Building SQL Query String '''
Dim sqlQueryString As String
sqlQueryString = qt.CommandText
Set classQtEvents.QryTble = qt
classQtEvents.OldSql = sqlQueryString ' Cache the original query string
QueryBuilder.BuildSQLQueryStringFromInterface interface, sqlQueryString
' Test message
MsgBox sqlQueryString
qt.CommandText = sqlQueryString
If Not qt Is Nothing Then
qt.Refresh
Else
' ... Error handling code here...
End If
''' CLEAN UP '''
' Free the dictionary
Set qtDict = Nothing
End Sub
Also here is a screenshot of the Module structure http://imgur.com/8fUcfLV
My first thought on what might be the issue was passing the QueryTable by value. I am not the most experienced VBA developer, but I reasoned this would create a copy and be calling the event on an unrelated table. However, this was not the case and passing by Reference did not fix the problem either.
Also the query is confirmed to run successfully as the data is correctly showing up and being refreshed.
EDIT
I added the BeforeRefresh event function to CQtEvents class Module and confirmed this function is called once Refresh is called
Private Sub mQryTble_BeforeRefresh(Cancel As Boolean)
MsgBox "Start of BeforeRefresh"
End Sub
How might I alter this code get my QueryTable from the QTableModule's RefreshDataQuery() Sub routine to have the AfterRefresh function invoked when the query is successfully ran?
How to catch the AfterRefresh event of QueryTable?
Explanation: in your situation, before event was fired you lost reference of your QueryTable by setting it to nothing when you made cleaning or procedure ended.
General solution: you must be sure that your code is still running and/or you need to keep any references to your QueryTable.
1st solution. When calling QT.Refresh method set the parameter to false in this way:
qt.Refresh false
which will stop further code execution until your qt is refreshed. But I don't consider this solution to be the best one.
2nd solution. Make your classQtEvents variable public and after RefreshDataQuery sub is finished check the status with some other code.
in you CQtEvents class module add the following public variable:
Public Refreshed As Boolean
in your BeforeRefresh event add this:
Refreshed = False
in your AfterRefresh event add this line of code:
Refreshed = True
Make your classQtEvents variable declaration public. Put this before Sub RefreshDataQuery()
Public classQtEvents as CQtEvents
but remove appropriate declaration from within your sub.
Now, even your sub is finished you will be able to check status of refreshment by checking .Refreshed property. You could do it in Immediate or within other Sub. This should work for Immediate:
Debug.Print classQtEvents.Refreshed
3rd solution. (a bit similar to 1st one) Follow steps 1 to 3 from 2nd solution. After you call qt.Refresh method you could add this loop which will stop further code execution until qt is refreshed:
'your code
If Not qt Is Nothing Then
qt.Refresh
Else
' ... Error handling code here...
End If
'checking
Do Until classQtEvents.Refreshed
DoEvents
Loop
Final remark. I hope I didn't mixed up qt variable with classQtEvents variable. I didn't tried and tested any solution using your variables but wrote all above with referenced to code I use.
A github repo that demonstrates the minimum code needed to get this working can be found here.
As mentioned, if your event handler isn't in scope, or your QueryTable reference is lost, you won't catch the event. The key factors to ensuring you catch the event are:
Declare a global variable of your event-handling class module's type outside of any subroutines/methods, at the top of a file (I chose the ThisWorkbook file).
Add a Workbook_Open event handler and instantiate that variable there, so that it is available immediately and will remain in scope (since it's global).
At that point, or at any downstream point when you have a QueryTable you're interested in, pass that QueryTable to the global instance to wire up its events.
(It took me a couple tries to figure this out myself, when someone pointed me in this direction as an answer to this question.)

Why does my function's name appear twice in the "locals" window?

I have created a Class Module in which I have defined a function. Whenever that function is called, it is listed twice in the locals window. Only the second one's value changes, the first one stays either empty or "zero", depending on its type, until the end of the code's execution. I don't have this problem with functions defined in standard modules. Did I do something wrong, is this a bug, or is there a logical reason behind this?
Contents of the TestClass class module:
Public Value As Double
Function AddFive() As Double
AddFive = Me.Value + 5
End Function
Contents of the standard module:
Sub TestSub()
Dim TestObject As New TestClass
TestObject.Value = 2
MsgBox TestObject.AddFive
End Sub
Here is a screenshot showing that, when the code is executed line-by-line, the function's value is listed twice in the locals window, and only the second value has changed after the function's code was executed.
(link to screenshot)
I'm using VBA for Excel 2010.
Thanks in advance.
The issue is more in how you are doing it. If you have a function that just adds 5 to an internal variable of a class object, then it's technically a void (Sub in VBA) since you don't need a return value.
Your code should be:
CLASS
Public Value As Double
Sub AddFive()
Me.Value = Me.Value + 5
End Sub
MODULE
Sub test()
Dim testObject As New TestClass
testObject.Value = 2
testObject.AddFive
MsgBox testObject.Value
End Sub
I can imagine there could be a number of reasons why there are 2 variables created, but I find it a bit pointless to go into why there is unexpected behavior since you are doing improper code.
If you want, you can even write class function that will show it's value + 5 in a msgbox and this would not create an extra variable either. But that is strange and I think you want the code above. But here it is regardless:
CLASS
Public Value As Double
Sub ShowPlusFive()
MsgBox Me.Value + 5
End Sub
MODULE
Sub test()
Dim testObject As New TestClass
testObject.Value = 2
testObject.ShowPlusFive
End Sub

Calling a Sub or Function contained in a module using "CallByName" in VB/VBA

It is easy to call a function inside a classModule using CallByName
How about functions inside standard module?
''#inside class module
''#classModule name: clsExample
Function classFunc1()
MsgBox "I'm class module 1"
End Function
''#
''#inside standard module
''#Module name: module1
Function Func1()
MsgBox "I'm standard module 1"
End Function
''#
''# The main sub
Sub Main()
''# to call function inside class module
dim clsObj as New clsExample
Call CallByName(clsObj,"ClassFunc1")
''# here's the question... how to call a function inside a standard module
''# how to declare the object "stdObj" in reference to module1?
Call CallByName(stdObj,"Func1") ''# is this correct?
End Sub
I think jtolle's response addressed the question best - the small reference to Application.Run may be the answer. The questioner doesn't want to use simply func1 or Module1.func1 - the reason one would want to use CallByName in the first place is that the desired function.sub name is not known at compile time. In this case, Application.Run does work, e.g.:
Dim ModuleName As String
Dim FuncName As String
Module1Name = "Module1"
FuncName = "func1"
Application.Run ModuleName & "." & FuncName
You can also prepend the Project Name before the ModuleName and add another period ".".
Unfortunately, Application.Run does not return any values, so while you can call a function, you won't get its return value.
Although it is an old question and OP asked for CallByName in a standard module, the correct pieces of advice are scattered through answers and comments, and some may not be that accurate, at least in 2020.
As SlowLearner stated, Application.run DOES return a Variant, and in that way both branchs below are equivalent, except by handling errors, as commented around Horowitz's answer:
Dim LoadEnumAndDataFrom as Variant
'FunctionName returns a Variant Array
if fCallByName then
LoadEnumAndDataFrom = CallByName(ClassObj, "FunctionNameAtClass", VbMethod)
else
'After moving back function for a standard module
LoadEnumAndDataFrom = Application.Run("StandardModuleName" & "." & "FunctionNameAtStandard")
endif
I actually just did this above and had no errors at all, tested in Word, Excel and Access, and all return the same Array.
Unfortunately, there is an exception: Outlook's object Model is too protected and it does not have the Run method.
CallByName works only with class objects.
If your subroutine is in a standard module, you can do this:
Sub Main()
Module1.Func1
End Sub
If it's a function, then you'll probably want to capture the return value; something like this:
Sub Main()
Dim var
var = Module1.Func1
End Sub
Modules in VB6 and VBA are something like static classes, but unfortunately VB doesn't accept Module1 as an object. You can write Module1.Func1 like C.Func1 (C being an instance of some Class1), but this is obviously done by the Compiler, not at runtime.
Idea: Convert the Module1 to a class, Create a "Public Module1 as Module1" in your Startup-module and "Set Module1 = New Module1" in your "Sub Main".
Unfortunately it is not possible to prepend the ProjectName before the ModuleName and add another period "." In MS Word this throws a runtime error 438. The call is restricted to the use of simply ModuleName.ProcName.