We have this utility method in a VB6 COM library for executing parameterised SQL:
Public Sub ExecSQL(ByVal strSQL As String, ParamArray varParams() As Variant)
'snip - ADODB data access
End Sub
Where varParams is a two dimensional array of SQL parameter information. Example usage would be:
ExecSQL("SELECT * FROM People WHERE Name = ?", Array("#p1", adVarChar, 10, "Smith"))
This is tried and tested code and works fine in normal usage. I am now in an unusual situation where the SQL string is a configurable value and could contain any number of parameters, so what I need to do is pass an unknown number of arguments in to the ParamArray. My attempt so far (simplified) is:
Function ExecConfigurableSql(sqlString As String, parameterValues() As String)
Dim parameters() As Variant
ReDim parameters(UBound(parameterValues)) As Variant
For i = 0 To UBound(parameterValues)
parameters(i) = Array("#p" + CStr(i), adVarChar, 1000, parameterValues(i))
Next
ExecSQL(sqlString, parameters) 'Type Mismatch
End Function
The attempt to execute the SQL throws a Type Mismatch error. Is there a way to pass an Array in to a function which expects a ParamArray? Or am I making an altogether separate mistake somewhere?
This is what the parameters look like with a dynamically built up array:
And this is what they look like when passed with comma-separated ParamArray syntax (which works):
The structure looks the same to me.
First, you never needed ParamArray in ExecSQL() in the first place as you're always passing one argument on the stack in addition to strSQL, i.e. an array of variants, which is
Array("#p1", adVarChar, 10, "Smith")
in the second listing. ParamArray is used to pass an undefined number or parameters on the stack, i.e. to be able to make calls like:
ExecSQL "SELECT * FROM People WHERE Name = ?", "#p1", adVarChar
ExecSQL "SELECT * FROM People WHERE Name = ?", "#p1", adVarChar, 10, "Smith"
ExecSQL "SELECT * FROM People WHERE Name = ?", "#p1"
ParamArray just take all the arguments passed on the stack and put them in an array for you.
So, you could have defined ExecSQL() as follow and it would have been the same thing provided your code get adapted to one-less Array() layer around varParams:
Public Sub ExecSQL(ByVal strSQL As String, varParams() As Variant)
' snip - ADODB data access '
End Sub
That being said:
Currently, the code in ExecConfigurableSql() transforms an array of strings (I presume field names), into the format expected by ExecSQL(), except that the (outer) array can (and will) contain more than one element of the sort Array("#p1", adVarChar, 10, "Smith").
Can ExecSQL() handle this? => Problem #1
[by the way, are all the fields 1000-char long??]
Problem #2: parameters is fine when you look at it from within ExecConfigurableSql(), but once you pass if to ExecSQL(), the ParamArray wraps it inside another array, so you really end up with (once in ExecSQL()) with something like this:
Now, you have to put the (unknown number of) parameters in an array, 'cause, well, you can't pass them on the stack to ParamArray since you don't know in advance the number of them. So you cannot remove the extra Array() wrapping from there.
You could get rid of the ParamArray in ExecSQL(), but that would break your existing ExecSQL() calls (for which varParams would only be wrapped once in an Array() instead of twice).
Knowing all this, you have two choices:
(1) Keep the declares as-is, and have ExecConfigurableSql() make multiple ExecSQL() calls within a For loop (btw, you declared it as a Sub so I presume it doesn't return any value); e.g.
Function ExecConfigurableSql(sqlString As String, parameterValues() As String)
For i = 0 To UBound(parameterValues)
Call ExecSQL(sqlString, Array("#p" + CStr(i), adVarChar, 1000, parameterValues(i))
Next
End Function
or
(2) Do the other way around, to improve the logic & consistency
Since you can still play with the declaration & implementation of ExecConfigurableSql(), change the second parameter to expect an array of Array("#p1", adVarChar, 10, "Smith")-like members (and not just field names).
Function ExecConfigurableSql(sqlString As String, varParamsArray() As Variant)
Dim varParams() As Variant
For i = 0 To UBound(varParamsArray)
varParams = varParamsArray(i)
' snip - ADODB data access '
Next i
End Function
Take the code from within ExecSQL() and put it in ExecConfigurableSql() where indicated - IMPORTANT: You have to update your code to account for the fact that parameters have one less Array() wrapped around them.
For ExecSQL(), remove the ParamArray keyword and treat the method as a special case of ExecConfigurableSql() where only one member is provided, i.e.:
Function ExecSQL(sqlString As String, varParams() As Variant)
Call ExecConfigurableSql(sqlString, Array(varParams))
End Function
Related
I'm wondering if its possible to pass both lower and upper bound array sizes (using the "To" keyword) via arguments. Ultimately, I would like to do something like this:
sub foo
call bar(2 To 5)
end sub
sub bar(arrayDimensions)
dim myArray() as long
redim myArray(arrayDimensions)
end sub
But VBA throws a fit if I use the "To" keyword like this. Is there another easier alternative? Or am I doing something wrong? I know I could pass two arguments as a work around, but I would rather not do that if there's a better way.
Thanks in advance for any help.
Edit to clarify why I would like to do this instead of using two arguments.
I made an array class that I can use to store my arrays in and easily modify them (eg. myArray.fill(0)). But I want the user interface with this array class to be the same as just using a plan old array.
sub foo
dim regularArray() as long
redim regularArray(5)
regularArray(3) = 250
dim myArray as ArrayClass
set myArray = factory.newArrayClass(5)
myArray(3) = 250
end sub
This works great using default properties. My constructor is set up to either receive a one long argument to define the size of a 1D array. Or it can receive two long arguments to define the size of a 2D array. Or is can receive one range argument to build an array based on excel data. Or it can receive an array for its argument to instantiate with an array.
Right now my code works great as it is. But if I wanted to add a lower bound when I instantiate the class, lets say for a 2D array, then I have to add two more optional arguments, one for each dimensions.
That then leads to the question of: do two long arguments represent a 2D array, or does it represent the lower and upper bound of a 1D array. So it would get hairy in a hurry.
Passing two arguments is not a a work around, it is the way to do what you want.
Sub foo()
Call bar(2, 5)
End Sub
Sub bar(a As Long, b As Long)
Dim myArray() As Long
ReDim myArray(a To b)
End Sub
This works equally well, I guess:
Sub foo2()
Dim myArray(2 To 5) As Long
Call bar2(myArray)
End Sub
Sub bar2(myArray() As Long)
'Do stuff with myArray
End Sub
From a design point of view, it isn't clear why "foo" knows what dimensions are needed in "bar", and why "bar" can't set these dimensions (since "bar" is the function that is doing the work with "myArray" anyway). It is very unusual to see a construction like this. Also, for what its worth, I have never seen an array in practice that did not have a lower bound of either 0 or 1, so that is also a bit unusual.
You are trying to make things easier for your users which is to your credit. Unfortunately, this can make the programmers life a bit more complicated.
There are two alternative strategies.
1 use multiple constructors. Its not a sin to have multiple methods to construct your array so you might have methods such as
factory.newArrayClassBySize(5)
factory.newArrayClassOneDim(5,10)
factory.newArrayClassTwoDim(5,10,3,22)
factory.newArrayClassByArray(InputArray)
factory.newArrayClassByRange(inputXlRange)
This is, by far ,is my preferred method as it makes it very explicit what the code is doing.
An alternative strategy is to have 7 optional parameters where the name of the parameters makes it clear what the the constructor expects and will do. The limitation of this method is that users have to pay attention to the intellisense for the parameters
e.g.
Public Function newArrayClass _
( _
Optional Size as variant = Empty, _
Optional Lbound1 as variant = Empty, _
Optional Ubound1 as variant = Empty, _
Optional Lbound2 as variant = Empty, _
Optional Ubound2 as Variant = Empty, _
Optional XlRange as variant = Empty, _
Optional BasedOn as variant = Empty _
) as ArrayClass
Using the named parameters requires you todo more work to work out if a correct set of parameters has been provided.
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...
I am modifying a legacy project that must run in VB6, and I have OOP experience but not VB.
Anyway I thought this would be straightforward, I need to add data to a hashmap.
I can call the retreive data function but not the set data function.
Here is the set data function (or sub I guess).
Sub SetSetting(Name As String, Value)
Member of MBCSettings.MBCSettings
Store the named setting in the current settings map
So if I try to set something like this:
gobjMBCSettings.SetSetting("UserName", "223")
I get: Compiler Error, Expected "="
Is my object broken or am I missing something obvious? Thanks.
Ah VB6... yes.
In order to invoke a method the standard way you do not use parentheses:
gobjMBCSettings.SetSetting "UserName", "223"
If you want to use parentheses, tack in the call command:
Call gobjMBCSettings.SetSetting("UserName", "223")
It should be noted that using parentheses around ByRef argument without the Call keyword, the argument will be sent in as ByVal.
Public Sub MySub(ByRef foo As String)
foo = "some text"
End Sub
Dim bar As String
bar = "myText"
MySub(bar)
' bar is "myText"
Call MySub(bar)
' bar is "some text"
It only complained because you were passing in multiple parameters wrapped with a single set of parentheses. Using () to force ByVal also works in VB.NET.
So I have a UDT set up like this:
Public Type UserInfo
name as string
username as string
active_time as double
End Type
I then create an array of this type:
Dim list_of_users() as UserInfo
'Populate array here
What I want to do is pass the active_time values as an array into a separate function. Something like:
'StdDev function defined elsewhere
standard_dev_all = StdDev(list_of_users().active_time)
Is this even possible? I suppose I could modify the function to deal with my UDT, but I have many more values than just active_time and it seemed like that would make it pretty messy.
You cannot pass it as
UDTvariable.Element
' or
UDTvariable().Element
into a function. I mean,
the first one is not valid as the index of the array element (not the element's Element) hasn't been specified.
The second one is invalid as well.
The solution is to pass on to your function
Answer = Stdev(UDTVariable)
and inside the function, you do this
Function Stdev(ByRef UDTVariableTypeName UDTVariable)
for i = 1 to N
Something = UDTVariable(i).Element
' so on and so forth
next i
Stdev = SomeAnswer
End Function
You may omit writing ByRef as that is the default way of passing arguments, but I've kept it for the sake of clarity.
I have a generic question to see if anyone can help me with a better solution.
I have a .NET method that takes in 20+ boolean values, passed in individually.
For each parameter that is true I need to add a value to list.
Is there a more efficient way to add the values to the list besides have an if statement for each boolean?
Example:
Public Function Example(ByVal pblnBool1 as boolean, _
ByVal pblnBool2 as boolean, _
ByVal pblnBool3 as boolean)
If pblnBool1 then
list += "A"
End If
If pblnBool2 then
list += "B"
End If
End Function
Obviously this code isn't correct but it shows what I'm trying to do.
Anyone have any ideas?
Thanks
First off, having 20+ params sucks.
Secondly, you can use the ParamArray keyword to declare that you want the values passed to you in an array. (I don't think this is CLS compliant, meaning some languages won't be able to call your function without bundling the values into an array. But VB and C# can both easily work with each other's param arrays.) If you don't want to do that, you can always create the array yourself in your function. But i'd rather let the language and/or framework do that for me.
This is not optimized or anything; it's just an example.
sub Example(paramarray bools() as Boolean)
static vals() as String = {"A", "B", "C"}
if bools.Length > vals.Length then
throw new ArgumentException(String.Format( _
"Too many params! ({0} max, {1} passed)", _
vals.Length, bools.Length _
))
end if
for i as Integer = 0 to bools.Length - 1
if bools(i) then list += vals(i)
next
end sub
I assume that list is some member variable, since it's not defined in your code. If you intend for it to be the return value, then declare it in the function, and return it at the end. (And of course, turn sub Example(...) and end sub into function Example(...) as String and end function).
If you have 32 or fewer booleans to pass, you can use an instance of BitVector32. This allows to pass them all in a single integer. It provides methods for setting and retrieving the values.
Just make an array of boolean values and do a for...each loop through it. Or if you need to be selective, a 2-D array.