Passing values from Function to Subroutine - excel

I have been looking around for an answer to that, but have been unable to locate one that explains it well enough that I understand.
I have no experience with functions, and very little with VBA overall. So if I have a subroutine I am executing and then call a function that I pass parameters into, once that function runs how do I get the result back into the main subroutine to be used?
This is for Access where I am pulling a number from one record set then passing that to a function to be used for an insert to create a new number. I then need that number passed back to the subroutine to be used.

Here you have two alternatives.
Run the code using F8 key to see what happens and make sure to activate the Locals Window so you see how variables values change
1- Return the value directly from a function
Run the DoSomethingReturnFromFunction sub
' Return value from function
Public Sub DoSomethingReturnFromFunction()
' Define a parameter
Dim myParameter As String
myParameter = "SomeValue"
' Call a function and store its value into a variable
Dim myResult As Long
myResult = MyFunction(myParameter)
' Print the result variable
Debug.Print myResult
End Sub
Private Function MyFunction(ByVal myParameter as String) As Long
Dim result As Long
Select Case myParameter
Case "SomeValue"
result = 1
Case Else
result = 2
End Select
' Assign the result value to the function
MyFunction = result
End Function
Result: Debug.Print myResult prints to the inmediate window the value returned from the function
2- Change variable value passed ByRef inside another function
Run the DoSomethingReturnFromByRef sub
' Change variable value passed ByRef inside another function
Public Sub DoSomethingReturnFromByRef()
' Call a function
Dim myByRefParameter As Long
MySub myByRefParameter
' Print the result
Debug.Print myByRefParameter
End Sub
Private Sub MySub(ByRef myByRefParameter As Long)
' Change the value of the variable passed ByRef inside the procedure
myByRefParameter = 1
End Sub
Result: Debug.Print myByRefParameter prints to the inmediate window the value that is stored in the myByRefParameter variable originally declared in the DoSomethingReturnFromByRef procedure
Let me know if it's clear

Related

Passing parameter into function to create variable name

I want to use global variables in my workbook and in the ThisWorkbook code. I declared the following varaibles
Public position_1 as string
Public position_2 as string
If I want to see the value of those variables I believe they need to be fully qualified so
Debug.Print ThisWorkbook.position_1
Debug.Print ThisWorkbook.position_2
I have written a UDF which I will pass in an integer to represent which variable I am looking for. I will only be passing in a single number and not a full variable name. I am trying to find a way to use this integer to concatenate with "position_" to display the value of the global variable, ThisWorkbook.position_1, ThisWorkbook.position_2, etc.
Function Test_Global_Var(position as Integer)
Dim variable_name As String
variable_name = "position_" & position
Debug.Print ThisWorkbook.variable_name
End Function
So when I call
Test_Global_Var(1)
my immediate window should display the value of
ThisWorkbook.position_1
The code below produces the following debug output
2 values defined.
ThisWorkbook.Position(0)
First Value
ThisWorkbook.Position(1)
Second Value
It uses a private array in the workbook named m_position. The contents are accessed by a global property ThisWorkbook.Position(index).
In a module have the following code:
Option Explicit
Public Sub Test()
If ThisWorkbook.NoValues Then
ThisWorkbook.FillValues "First Value", "Second Value"
End If
Debug.Print CStr(ThisWorkbook.Count) & " values defined."
Test_Global_Var 0
Test_Global_Var 1
End Sub
Public Sub Test_Global_Var(ByVal index As Long)
' Part of a UDF
Debug.Print "ThisWorkbook.Position(" & CStr(index) & ")"
Debug.Print ThisWorkbook.Position(index)
End Sub
In ThisWorkbook have the following code:
Option Explicit
Private m_position() As Variant
Private Sub Workbook_Open()
Call DefaultValues
End Sub
Public Property Get Position(ByVal index As Long) As Variant
Position = m_position(index)
End Property
Public Sub DefaultValues()
m_position = Array("First", "Second")
End Sub
Public Sub FillValues(ParamArray args() As Variant)
m_position = args
End Sub
Public Property Get Count() As Long
Count = UBound(m_position) - LBound(m_position) + 1
End Property
Public Property Get NoValues() As Boolean
On Error GoTo ArrUndefined:
Dim n As Long
n = UBound(m_position)
NoValues = False
On Error GoTo 0
Exit Sub
ArrUndefined:
NoValues = True
On Error GoTo 0
End Property
PS. In VBA never use Integer, but instead use Long. Integer is a 16bit type, while Long is the standard 32bit type that all other programming languages consider as an integer.
It is possible to consider a global dictionary variable and pass data through it from the UDF.
First add reference to Microsoft Scripting Runtime:
Thus, build the dictionary like this:
Public myDictionary As Dictionary
To initialize the myDictionary variable, consider adding it to a Workbook_Open event:
Private Sub Workbook_Open()
Set myDictionary = New Dictionary
End Sub
Then the UDF would look like this:
Public Function FillDicitonary(myVal As Long) As String
If myDictionary.Exists(myVal) Then
myDictionary(myVal) = "position " & myVal
Else
myDictionary.Add myVal, "position " & myVal
End If
FillDicitonary = "Filled with " & myVal
End Function
And it would overwrite every key in the dictionary, if it exists. At the end, the values could be printed:
Public Sub PrintDictionary()
Dim myKey As Variant
For Each myKey In myDictionary
Debug.Print myDictionary(myKey)
Next
End Sub

ByVal vs ByRef VBA

I've tried to attempt something that was answered by JaredPar ByRef vs ByVal Clarification
ByVal in VB.NET means that a copy of the provided value will be sent
to the function. For value types (Integer, Single, etc.) this will
provide a shallow copy of the value. With larger types this can be
inefficient. For reference types though (String, class instances) a
copy of the reference is passed. Because a copy is passed in mutations
to the parameter via = it won't be visible to the calling function.
ByRef in VB.NET means that a reference to the original value will be
sent to the function (1). It's almost like the original value is being
directly used within the function. Operations like = will affect the
original value and be immediately visible in the calling function.
And I've tried to test it with the following code and I can't seem to get it to work use the ByRef to change the value of the cell to 0 if it's 1
Here's my below code that I'm testing with, what am I doing wrong? The value of Range("A1") is still 1
Sub test1()
Dim trythis As Boolean
trythis = False
If (Sheets("TESTING").Range("A1").value = 1) Then
testingRoutine (trythis)
If (trythis) Then
Debug.Print "Value changed to 0"
Sheets("TESTING").Range("A1").value = 0
End If
End If
End Sub
Private Function testingRoutine(ByRef trythis As Boolean)
Debug.Print "Ran Function"
trythis = True
End Function
VB subroutines don't require braces around the argument list. However, if you pass a single argument and you enclose that in braces, you are passing an expression . Expressions cannot be passed by reference. They are evaluated and their result is passed. Therefore you must remove the braces in the call testingRoutine (trythis) and write testingRoutine trythis
Note: if you call a function without using its return value, it must be written as a procedure call (without braces around the argument list). As an example:
myVal = myFunction (trythis) ' trythis will be passed by reference
myFunction (trythis) ' trythis will be seen as an expression
myFunction trythis ' trythis will be passed by reference
myVal = mySub (trythis) ' invalid: mySub is not a function
mySub (trythis) ' trythis will be seen as an expression
mySub trythis ' trythis will be passed by reference
Of course, the problem will be clear immediately when a function or sub has more than one parameter because a comma cannot appear in an expression.
Change this:
testingRoutine (trythis)
to this:
testingRoutine trythis
then try it.
Also, look what happens if I change it to this, where the function is being used as a function (returning something)
Sub test1()
Dim trythis As Boolean
Dim testit As Boolean
trythis = False
If (Sheets("Sheet1").Range("A1").Value = 1) Then
testit = testingRoutine(trythis)
If (trythis) Then
Debug.Print "Value changed to 0"
Sheets("TESTING").Range("A1").Value = 0
End If
End If
End Sub
Private Function testingRoutine(ByRef trythis As Boolean) As Boolean
Debug.Print "Ran Function"
trythis = True
End Function
I would do it this way.
Sub test1()
Dim trythis As Boolean
trythis = False
If (Sheets("TESTING").Range("A1").value = 1) Then
tr = testingRoutine(trythis)
If tr Then
Debug.Print "Value changed to 0"
Sheets("TESTING").Range("A1").value = 0
End If
End If
End Sub
Private Function testingRoutine(ByRef trythis As Boolean)
Debug.Print "Ran Function"
testingRoutine = True
End Function
Because trythis is not a global variable, changing it in the function will do nothing in the sub. It will just define trythis as True in the function and stay in that scope. To get trythis to be affected by the function and read by the sub you have to assign it to a variable within the sub.
i can't remember where i read this, but i know that if you add the bracets () to arguments when calling a sub/function, it is a specific (not an error) way microsoft implented to workaround byref calls (to byval).
example : lets say you call a sub who has ONLY byref, and 2 arguments:
call Sub1 (a), (b)
is a proper way to force Byvalarguments, even if they were initially coded as Byval

Compile error "expected: end of statement"

I have the following calcScores function written:
Function calcScores(category As String) As Integer
Dim count As Integer
count = 0
For Each Ctl In UserForm1.Controls
If Ctl.Tag = category And TypeName(Ctl) = "CheckBox" Then
Dim box As MSForms.CheckBox
Set box = Ctl
If box.Value = True Then
count = count + 1
End If
End If
Next
calcScores = count
End Function
This function takes a tag named "category" as a string and then checks the form for all check boxes with that tag and counts the ones that are checked. I know it works and counts the right number, because I have slightly edited it to output it's value to a label on the form instead of returning it.
When I try to call it in another function like this:
Function sortScores()
Dim scores(0 to 5) as Integer
scores(0) = calcScores "rChk"
**CODE CONTINUES**
End Function
I get an error that says "Expected: End of Statement" as soon as I leave the line that assigns the function's return to scores(0). calcScores is assigned before sortScores, and was succesfully called in a sub before using the same syntax.
Any idea what the error could be?
Call you function like this
scores(0) = calcScores("rChk")
Functions are called like that. Subs are called by
subName argument

Passing nested array as parameter for function in VBA results in compile error "Expected: ="

I am attempting to Construct an array (testSheets) that holds a file name and a file path as strings. I then wish to contain this array inside of another array (shtsTmpl). Then, I want to pass the strings within the nested array into a function's parameters (copySheets) to simply return the combined file path in a msgbox.
This first bit of code successfully returns "TSTSheets.xlsx" in a msg box.
Sub prepSheets()
Dim testSheets() As Variant
ReDim testSheets(0)
testSheets(0) = "TSTSheets.xlsx"
Dim shtsTmpl() As Variant
shtsTmpl = Array(testSheets)
copySheets (shtsTmpl(0)(0))
End Sub
Function copySheets(srcName As String)
MsgBox (srcName)
End Function
This second bit of code returns a compile error that says "Expected: =" at the line that calls the copySheets function and I do not understand why.
Sub testSheets()
Dim testSheets() As Variant
ReDim testSheets(1)
testSheets(0) = "TSTSheets.xlsx"
testSheets(1) = "C:\"
Dim shtsTmpl() As Variant
shtsTmpl = Array(testSheets)
copySheets (shtsTmpl(0)(0),shtsTmpl(0)(1))
End Sub
Function copySheets(srcName As String, srcPath As String)
MsgBox (srcPath & srcName)
End Function
Can someone please explain to me why the code compiles correctly when the function has one parameter and incorrectly when the function has two parameters?
Please let me know if you need further explanation.
Thank you.

VBA: Creating session persistent objects (hash) in Excel

Is it possible in a VBA function (UDF) to create an object that has global scope? I.e persists beyond the runtime of the function? I would want to stick it in a hash with a unique key that I can pass to other functions. I know you can do this in c#/c++ dll's.
The motivation is a heavy duty piece of processing that I don't want to repeat across hundreds of function calls: I want to cache the results so I only need to do once. E.g let's imagine I have a UDF which builds the results object in Cell A1:
=CreateResultsObject(arg1, arg2, arg3...)
The function does the heavy work and returns a unique ID string (the key for the object stored in the persistent hash). Cell A1 now contains this string value which I can then pass to other functions: they can then access the cached object in the hash with the key.
Is this possible? If so how?
The variables you declare in a module are persistent.
This code in a module might go into the direction you want:
Option Explicit
Dim col As New Collection
Public Function GetValue(ByVal strName As String) As String
GetValue = col.Item(strName)
End Function
Public Sub SetValue(ByVal strName As String, ByVal strValue As String)
col.Add strValue, strName
End Sub
Note:
For duplicate or missing names the code will fail.
Instead of a string value any kind of object could be passed by modifying the function signatures accordingly.
Addendum:
The same code with a bit more intelligence - for existing keys in the collection the value will be replaced instead of failing with an error.
Option Explicit
Dim col As New Collection
Public Function GetValue(ByVal strName As String) As String
GetValue = col.Item(strName)
End Function
Public Sub SetValue(ByVal strName As String, ByVal strValue As String)
If HasValue(strName) Then
col.Remove (strName)
End If
col.Add strValue, strName
End Sub
Private Function HasValue(ByVal strName As String) As Boolean
Dim val As Variant
Dim bRes As Boolean
bRes = True
On Error Resume Next
val = col.Item(strName)
If Err.Number <> 0 Then
bRes = False
Err.Clear
End If
On Error GoTo 0
HasValue = bRes
End Function
What about using a global variable within a module?
Something like this:
Option Explicit
Dim sHash As String
Function CreateResultsObject()
'very long code
sHash = "MyTest"
CreateResultsObject = "ok"
End Function
Function displayresultsobject()
displayresultsobject = sHash
End Function
Note that your Hash will be recalculated only when you call CreateResultsObject() in your worksheet and each time you ask for recalculation.

Resources