What does the Call keyword do in VB6? - vba

There's some code in our project that looks a bit like this:
Private Sub Method1()
Call InnerMethod
End Sub
Private Sub Method2()
InnerMethod
End Sub
Private Sub InnerMethod()
'' stuff
End Sub
What's the advantage of doing Method1 over Method2?

From the MSDN:
You are not required to use the Call
keyword when calling a procedure.
However, if you use the Call keyword
to call a procedure that requires
arguments, argumentlist must be
enclosed in parentheses. If you omit
the Call keyword, you also must omit
the parentheses around argumentlist.
If you use either Call syntax to call
any intrinsic or user-defined
function, the function's return value
is discarded.
For example:
Sub Proc1()
Debug.Print "Hello World"
End Sub
Sub Proc2(text As String)
Debug.Print "Hello " & text
End Sub
In the immediate window, if you enter
Proc1
then "Hello World" prints. If you enter
Call Proc1
then "Hello World" prints. If you enter
Proc2 "World"
then "Hello World" prints. If you enter
Call Proc2 "World"
you get a compile error. You would have to enter
Call Proc2("World")

Call does nothing special other than call the method. It is a hang over from the old days of Basic when all lines had to start with a keyword. "Let" is another of these keywords, which was always put before an assignment, but is no longer required.
Method1 and Method2 do the exact same thing.

I have found a major difference about 'call' keyword with functions that having, ByRef Arguments (I have found this in MS-Access VBA editor). If you are calling the function without 'Call' keyword, ByRef aruments will not set for the calle. For Ex:
Private Function Test(Optional ByRef refArg As String) As Boolean
refArg = "Test"
Test = True
End Function
If you call the function without the Call keyword like
Dim a As String
Test(a)
a will be an empty string, after the call returns
If you call the function with the Call keyword like
Dim a As String
Call Test(a)
a will contain the string Test
The detailed explanation provided in the following link:
Cannot use parentheses when calling a Sub

There's no difference.

Here's a post which describes when you need to use call vs not using it and when to parentheses around your parameters.
You can also read more about call from MSDN. Essentially the main difference is that when you use call to call a function you can't access the return value.

Related

VBA Option Explicit fails to detect undeclared variables in Function

I have always used Option Explicit in each module. I never gave it much thought, until now.
Code:
Option Explicit
Function ParseJSON(ByVal strJSON As String) As String
strJSON = "New String"
'MsgBox (strJSON)
ParseJSON = strJSON
End Function
Sub Test()
Dim strJSON As String
strJSON = "Old String"
MsgBox (ParseJSON(strJSON))
MsgBox (strJSON)
End Sub
This piece of code is just a test and has nothing to do with JSON. When I run the code, I expected it to throw an error as strJSON is never declared in ParseJSON, and it should be a new variable as the original one is passed ByVal and thus cannot be changed, the last MsgBox() confirms this.
Is there anything that I didn't get? My hunch points to the ByVal part., or maybe Option Explicit only checks Sub?
the point of Option Explicit is to have you explicitly declare all variables you're using, and that's what actually happens in Function ParseJSON(ByVal strJSON As String) As String: that strJSON As String is declaring variable strJSON you're going to use inside the function (and it's also declaring it as of String type)
then, you're also giving it a value passed by the calling sub, and that ByVal simply means that whatever value the function strJSON variable is going to assume it won't affect the calling sub variable (if any, and that may be incidentally named after strJSON, but it's distinct from function strJSON) you passed the value of
that's why if you try
Function ParseJSON(ByVal strJSON As String) As String
strJSON = Range("A1:A3")
'MsgBox (strJSON)
ParseJSON = strJSON
End Function
you'll get a run time type mismatch error as soon as the strJSON = Range("A1:A3") line is processed
and that's why if you try
Sub Test()
Dim strJSON As String
strJSON = "Old String"
MsgBox (ParseJSON(Range("A1:A3")))
MsgBox (strJSON)
End Sub
you'll get the same run time type mismatch error as soon as the MsgBox (ParseJSON(Range("A1:A3"))) line is processed
You ARE declaring properly.
In the Test function you declare it as a string with the DIM statement.
And by making it an argument in the function you are also declaring it as a string, for use in the function.
Because the function uses it byVal, and changes you make IN the function, will not affect the value of the string.
It you want to change the value of it, it has to be passed byRef.
ByRef effectively passed the actual variable. - It could be known by a different name even, but any changes will also change the original variable.
byVal is effectively passing a copy of the variable, which you can play about with but not change the original.
Imagine as a piece of paper.
byRef you hand over the actual piece of paper to be marked up
byVal you hand over a photocopy, but keep the original to yourself.

"Invalid procedure call or argument" when calling a sub without parenthesing one parameter

I wrote a COM-Visible DLL with this piece of code (VB.NET)
' .NET Class with implementing an interface to make it visible
Public Class VisibleClass
Public Sub callableSub(ByVal names As ACustomCollection, _
Optional ByVal doSomething As Boolean = True) _
Implements IVisibleClass.visibleCallableSub
Me.doSub(names.process, doSomething)
End Sub
End Class
' Interface for COM-visibility
<InterfaceType(ComInterfaceType.InterfaceIsDual)> _
Public Interface IVisibleClass
Sub visibleCallableSub(ByVal names As ACustomCollection, _
Optional ByVal doSomething As Boolean = True)
End Interface
Here is also the ASP web page creating the object and invoking its methods :
' stuff.asp
Dim visibleObject
Dim aCustomCollection
Set visibleObject = getNewVisibleInstance (aCollection)
Set aCustomCollection = createEmptyCollection
aCustomCollection.add someStuffs
aCustomCollection.add otherStuffs
aCustomCollection.add lastStuffs
' These calls work:
visibleObject.visibleCallableSub(aCustomCollection)
visibleObject.visibleCallableSub(aCustomCollection), False
visibleObject.visibleCallableSub(aCustomCollection), True
Call document.fetchPropertyCollection ((properties), false)
' These ones don't:
visibleObject.visibleCallableSub someparams
visibleObject.visibleCallableSub someparams, False
visibleObject.visibleCallableSub someparams, True
Call document.fetchPropertyCollection (properties, false)
Non working calls produce the following error :
Invalid procedure call or argument
I don't understand why I have to put parenthesis. I know this tells the interpreter to make a copy before passing theme, but not why it's mandatory.
Note : It's the same issue than this one, i.e. about passing a reference where a copy is required. However, the question was told like it's due to "passing the return value of another function", which make it harder to reach through research.
If the called Sub/Function asks for a value (ByValue), you can't pass a reference ('pointer'). Putting the 'make a copy' parentheses around the argument makes sure the called Sub/Function gets a value.
To make your intention explicit, you should write:
visibleObject.visibleCallableSub (aCustomCollection), False
Cf. same error, parentheses, adventures.

Why can't I wrap Await in parenthesis in a statement?

In C#, I have some code that looks like:
(await GetBoolAsync()).ShouldBeTrue();
ShouldBeTrue() is an extension method from the Shouldly library that operates on Booleans.
VB.NET doesn't seem to like wrapping the Await keyword in parenthesis:
(Await GetBoolAsync()).ShouldBeTrue()
The compiler reports a syntax error on the opening parenthesis at the beginning of the line. I can work around it by declaring an intermediate variable, but is there a way to achieve this in one line like C#?
A full console application to reproduce this:
Imports System.Runtime.CompilerServices
Module Module1
Sub Main
End Sub
Async Function Test() As Task
(Await GetBoolAsync()).ShouldBeTrue()
End Function
Function GetBoolAsync() As Task(Of Boolean)
Return Task.FromResult(True)
End Function
<Extension()>
Public Sub ShouldBeTrue(x As Boolean)
End Sub
End Module
The error is very unhelpful:
error BC30035: Syntax error.
There is a special syntax for doing so - you should use the Call keyword:
Async Function Test() As Task
Call (Await GetBoolAsync()).ShouldBeTrue()
End Function
That's because you cannot directly call a member of non-literal expression in Visual Basic, like you would in C#:
(5).ToString() 'It is wrong!
And in this particular case this includes the result of Await.
Hope that helps!

Can an Excel VBA dictionary be used to call a function?

I have a large number of functions that I need to call which each have the same arguments and I'd like to be able to centralize them to avoid unwieldy blocks of code. Obviously I could use a wrapper function to just call all the others, but I don't always call all of them. My next thought is that I could place the functions in a list or dictionary and call them from there, much like in Python:
def foo():
return "foo"
myDict = { "foo": foo }
myDict["foo"]()
Returns foo
Is something similar possible in VBA? If so, what is the simplest way of doing it?
If the functions are inside objects/Classes then you can call them by name directly.
The below code works inside SHEET1 because it's an object.
It will not work in a MODULE as that is outside the capabilities of CALLBYNAME function.
So you can store the Names in the Key of the dictionary and then just use the key (no function pointer is needed)
Public Sub Something(arg1 As String)
MsgBox arg1
End Sub
Public Sub test()
CallByName Sheet1, "Something", VbMethod, "data 1"
End Sub
There does not appear to be a really straight forward way of doing this, however, it can be done using a combo of a list and the CallByName function.
You can make a list which contains all the function names as strings:
myFunctions = ( "foo" "bar" "baz" )
You can then iterate over the array to call the functions:
For Each functionName in myFunctions
CallByName Sheet1, functionName, VbMethod, listOfArguments
Next functionName
Functions can be filtered out with If statements

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.