Excel Visual Basic call function as stand-alone routine - vba

I'll get straight to the point; I'm trying to define a Function in Visual Basic which can simply be called without having to have something on the 'other side of the equation' as it were. Essentially I want to be able to define a routine which can be passed a series of variables and executes a routine based on those variables.
I currently have the following Function defined:
Function ImportData(WebAddress As String, OutputCell As Range)
With ActiveSheet.QueryTables.Add(Connection:= _
"URL;" & WebAddress & _
"bin/excelget.exe?TICKER=msft", _
Destination:=OutputCell)
.BackgroundQuery = True
.TablesOnlyFromHTML = True
.Refresh BackgroundQuery:=False
.SaveData = True
End With
End Function
What I want to be able to do is simply call this Function but not necessarily make something equal to or use this Function to manipulate something. So, for example, I'd like to be able to do something like the following:
Private Sub ExampleButton_Click()
ImportData("http://www.exampleURL.com/examplejsonhtmlformat","A3")
End Sub
When this Function is called, it simply steps through the Function using the variables defined. There is already an output defined in OutputCell so a cell doesn't need to 'equal' the output of this Function.
If anybody has any input, it would be much appreciated.
Thanks.

You want to make it a Sub - it is exactly what you describe: code that can be called but that doesn't return a value. Note that you don't put the parameters of a sub in parentheses when you call it. If you have
Sub myTest(a,b)
Then you call it with
myTest thing1, thing2
And NOT with
myTest(thing1, thing2)
Update based on excellent comments from #hnk and #Ioannis:
It is possible to call a Sub with
Call myTest(thing1, thing2)
But there is a subtlety, which has to do with the difference between passing a variable by value or by reference. When you pass by value, you make a copy: changing the parameter in the program does not change the original. However, when you pass by reference, it becomes possible to change the value of the parameter inside the sub - and that becomes the new value of the parameter after the sub returns. I'd the prototype says you expect the value to be passed by reference:
Sub MyTest(ByRef a)
Then you can override this behavior as follows:
Call with Passing by
MyTest a Reference
MyTest (a) Value
Call MyTest(a) Reference
Call MyTest((a)) Value
In general it is better to be explicit in the function prototype - specify if you want byVal or byRef and if the calling program gets it wrong you get warned bu the compiler. More info at Hidden features of VBA
If you are not at least a little bit confused or at least annoyed at Microsoft after this, you were not paying attention...
afterword it was pointed out by Rory that it is possible to call functions without assigning their return value. So you can have either
X = myFunc(y)
Or
myFunc y
Both will call the function - but note that when you don't expect a return value you don't use parentheses. Oh Microsoft, what were you thinking...

Related

Subroutine will not compile

I'm using Access VBA, and I keep getting
Compile error: Argument not optional
whenever I try to pass a collection into a function. What is going on?
Private Sub btnTest_Click()
Dim GarbageLanguages As New Collection
GarbageLanguages.Add "VBA"
PrintCollectionCount (GarbageLanguages) '<-- error happens here
End Sub
Public Sub PrintCollectionCount(c As Collection)
Debug.Print c.Count
End Sub
Short Answer
Remove the parentheses from the following line:
PrintCollectionCount (GarbageLanguages)
Long Answer
For better or worse (mostly worse), VBA has both functions and subroutines:
Function - expression that must return a value
Subroutine - statement that cannot return a value
Unfortunately, using each of them requires slightly different syntax. Suprisingly, this is not a valid subroutine call:
Subroutine(arguments)
Instead, you need to use one of these two options:
Call Subroutine(arguments)
Subroutine arguments
It's even more unfortunate that when you use the wrong syntax, all you get is extremely cryptic error messages. Finally, it's also hard to get used to not using parenthesis because single arguments that are primitive types instead of objects actually work fine:
Subroutine(SomeString) ' works
Subroutine(SomeInteger) ' works
Subroutine(SomeObject) ' does not work
Subroutine(SomeString, SomeInteger) ' does not work
Aside from memorizing the awful error messages, you can try to train yourself to look out for whenever a space gets automatically inserted after the subroutine's name. This:
Subroutine(argument)
gets changed to this:
Subroutine (argument) '<-- RED FLAG

How do I pass a range obj variable to a sub in Excel VBA (2016) [duplicate]

This question already has an answer here:
Array argument must be ByRef
(1 answer)
Closed 6 years ago.
Given the following code:
I can not seem to successfully pass a Range Object Variable from one sub-function to another. I spent an entire day researching, and experimenting before I swallowed pride and came here.
Please read the comments below, and reply with any ideas you have regarding why the LAST two lines will not behave.
Public Sub doSomethingToRows(ROI As Range)
*'do Something with the cell values within the supplied range*
End Sub
'
Public Sub testDoAltRows()
Dim RegionOfInterest As Range 'is this an object or not?
'*The following yields: Class doesn't support Automation (Error 430)*
'*Set RegionOfInterest = New Worksheet 'this just gives an error*
Set RegionOfInterest = Worksheets("Sheet1").Range("A1")
RegionOfInterest.Value = 1234.56 '*okay, updates cell A1*
Set RegionOfInterest = Worksheets("Sheet1").Range("B5:D15")
RegionOfInterest.Columns(2).Value = "~~~~~~" '*okay*
'doSomethingToRows (RegionOfInterest) 'why do I get "OBJECT IS REQUIRED" error?
doSomethingToRows (Worksheets("Sheet1").Range("B5:C15")) 'but this executes okay
End Sub
From the msdn documentation of the Call keyword statement,
Remarks
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.
To pass a whole array to a procedure, use the array name followed by
empty parentheses.
From a practical standpoint, even though Subs can be called with or without the "Call" keyword, it makes sense to pick one way and stick with it as part of your coding style. I agree with Comintern - it is my opinion, based on observation of modern VBA code, that using the "Call" keyword should be considered deprecated. Instead, invoke Subs without parenthesis around the argument list.
And now the answer to the important question:
Why does your code throw an error?
Take for example the following Subroutine:
Public Sub ShowSum(arg1 As Long, arg2 As Long)
MsgBox arg1 + arg2
End Sub
We have established that, if not using the Call keyword, Subs must be invoked like so:
ShowSum 45, 37
What happens if it were instead called like ShowSum(45, 37)? Well, you wouldn't even be able to compile as VBA immediately complains "Expected =". This is because the VBA parser sees the parenthesis and decides that this must be a Function call, and it therefore expects you to be handling the return value with an "=" assignment statement.
What about a Sub with only one argument? For example:
Public Sub ShowNum(arg1 As Long)
MsgBox arg1
End Sub
The correct way to call this Sub is ShowNum 45. But what if you typed this into the VBA IDE: ShowNum(45)? As soon as you move the cursor off of the line, you'll notice that VBA adds a space between the Sub name and the opening parenthesis, giving you a crucial clue as to how the line of code is actually being interpreted:
ShowNum (45)
VBA is not treating those parenthesis as if they surrounded the argument list - it is instead treating them as grouping parenthesis. MOST of the time, this wouldn't matter, but it does in the case of Objects which have a default member.
To see the problem this causes, try running the following:
Dim v As Variant
Set v = Range("A1")
Set v = (Range("A1")) '<--- type mismatch here
Notice that you get a "Type Mismatch" on the marked line. Now add those two statements to the watch window and look at the "Type" column:
+-------------+-----+--------------+
| Expression |Value| Type |
+-------------+-----+--------------+
|Range("A1") | |Object/Range |
|(Range("A1"))| |Variant/String|
+-------------+-----+--------------+
When you surround an Object with grouping parenthesis, its default property is evaluated - in the case of the Range object, it is the Value property.
So it's really just a coincidence that VBA allowed you to get away with "putting parenthesis around the argumentlist" - really, VBA just interprets this as grouping parenthesis and evaluates the value accordingly. You can see by trying the same thing on a Sub with multiple parameters that it is invalid in VBA to invoke a Sub with parenthesis around the argument list.
#PaulG
Try this:
Public Sub Main()
Debug.Print TypeName(Range("A1"))
Debug.Print TypeName((Range("A1")))
End Sub
okay, I knew after I posted this question I'd be struck by lighting and receive an answer.
When passing an object VARIABLE to a sub-function and wishing to use parentheses "()", one must use CALL! Thus the correction to my code sample is:
**CALL doSomethingToRows(RegionOfInterest)**
Thank you!
Maybe we're talking about different things, but here's an example to make it a bit clearer what I mean.
Option Explicit
Sub TestDisplay()
Dim r As Range
'Create some range object
Set r = Range("A1")
'Invoke with Call.
Call DisplaySomething(r)
'Invoke without Call.
DisplaySomething r
End Sub
Sub DisplaySomething(ByVal Data As Range)
Debug.Print "Hi my type is " & TypeName(Data)
End Sub
Both calls work perfectly. One with Call and the other without.
Edit:
#Conintern. Thanks for explaining that. I see what is meant now.
However, I still respectively disagree.
If I declare the following:
Function DisplaySomething(ByVal Data As String)
DisplaySomething = "Hi my type is " & TypeName(Data)
End Function
and invoke it:
Debug.print DisplaySomething(Range("A1"))
I believe that Excel has been clever and converted to a string. It can do that by invoking the Default Parameter and can convert to a string.
However, as in the original parameter example, If I declare the following:
Function DisplaySomething(ByVal Data As Range)
DisplaySomething = "Hi my type is " & TypeName(Data)
End Function
There is no call on the Default Parameter, however it is called, because Excel was able to resolve it to that type.
Function DisplaySomething(ByVal Data As Double)
DisplaySomething = "Hi my type is " & TypeName(Data)
End Function
will return a double because it was able to coerce to a double.
Indeed in those examples the Default was called.
But in this example we are defining as Range. No Default called there however it is invoked - brackets or no brackets.
I believe this is more to do with Excel and data coercion.
Similar to the following:
Public Function Test(ByVal i As String) As Integer
Test = i
End Function
and invoking with:
Debug.print Test("1")
BTW, yes I know this isn't an object without a Default parmeter. Im pointing out data coercion. Excel does its best to resolve it.
Could be wrong mind you...

Different ways of using a variable across different subroutines

I'm trying to set up a sub to be called upon and use the value of its result in the main sub. So far I've been using Function to carry over the value. However, I was wondering if there are any alternative ways of doing the same thing? I figured ByVal/ByRef is another way to do it by using a Sub instead of Function. My current codes are as follow:
Sub Main()
Dim i as Long
i = lr("A")
'some other calculations using i
End Sub
Function lr(Tar As String) As Long
Dim twb As Workbook
Set twb = ThisWorkbook
lr = ThisWorkbook.Sheets(1).Range(Tar & Rows.Count).End(xlUp).Row
End Function
My question is, How would I write this if I were to use a Sub instead of Function? Thanks!
So far I've been using Function to carry over the value.
Great, that's what functions are for! When you only need to return a single value, the best way is always going to be a function.
Things get fuzzier when you start needing to return two or more values. You could:
Use ByRef parameters and use them as "out" values.
This is "ok" for procedures (Sub), and confusing for functions - what determines which parameter gets to be the function's return value, and which parameters get to be passed ByRef? How does the calling code know whether or not to pass an initialized value to those ByRef parameters?
A naming convention can help here:
Public Sub Foo(ByVal foo1 As String, ByRef outBar1 As String, ByRef outBar2 As String)
An "out" prefix can tell the calling code that the parameter is an out value.
Scope the variables at a level that is accessible by both the caller and the callee.
This is a bad practice that can easily lead to spaghetti code. Avoid it - variables should have the smallest necessary scope possible, and be passed between methods/functions/procedures/modules, not just globally scoped and accessed by anyone at any given time!
Create a class to encapsulate all the values the function should return.
Definitely more object-oriented, results in much cleaner, readable code. The only downside is that VBA doesn't really encourage you to do this, and consistently doing that will result in a myriad of classes that you can't quite organize.

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.

Syntax: "Exit Sub" or "Return" in VB.NET subroutines

Both "Exit Sub" or "Return" seem to accomplish the same thing -- exit a subroutine. Is there any difference in how they work under the covers?
That is,
Private Sub exitNow()
Exit Sub
End Sub
or
Private Sub exitNow()
Return
End Sub
From the doc:
In a Sub or Set procedure, the Return statement is equivalent to an Exit Sub or Exit Property statement, and expression must not be supplied.
So they're the same in this context.
(Return (<value>) is used in functions and property.get's. Obviously slightly different in that context).
I tend to prefer Return over Exit Sub. Because once in a while you change from Sub to Function. In this case Exit Sub could be converted to Exit Function, but this assumes that there was a previous assignment to the function name (alike VB 6), which most probably didn't happen. Return would catch this situation - if the method should return a value, Return with no argument will fail at compile time.
If you inspect the IL output of the 2 statements, they are the same. However, since ’return’ is meant for pushing something back to the caller, so strictly speaking, ‘Exit Sub’ is more suitable for using in a Sub.
They are the same in this context.
However, from the code readability point of view, "Exit Sub" would be clearer since the "Return" indicates that something some value is being used as an output (which is not the case with Sub-routines).
First of all, Procedures comes with sub, we should know that we are working on specific procedures that don't return a specific value with the ability of passing some specific parameters or even without passing any parameter. Such as:
Print something().
Calculate the factorial of integer number CalcFact(X).
Do some processes for a specific task.
Function is a specific process programmed to achieve a specific task by also passing some specific parameters, and it has to return some value that can be used to to complete the overall task, such as validation the user name and user pass.
In short Sub Doesn't return value and we call it directly "Print HelloWorld()" , whereas functions do such as:
ValidUsersNameAndPass("Johne","jOhNe13042019") ' This could return a Boolean value.
ValidUsersNameAndPass("Johne","jOhNe13042019"); // This could return a Boolean value.
I wanted to confirm that they act the same in lambda expressions too, and they do:
Sub test()
Dim a As Action = Sub() Exit Sub
Dim b As Action = Sub() Return
a()
b()
MsgBox("Yes they do!")
End Sub
While there are exceptions like guard clauses, in most cases I would consider either a sign that the method is too long.