I've encountered a very strange bug in VBA and wondered if anyone could shed some light?
I'm calling a worksheet function like this:
Dim lMyRow As Long
lMyRow = WorksheetFunction.Match(vItemID, rngMyRange.Columns(1), 0)
This is intended to get the row of the item I pass in. Under certain circumstances (although I can't pin down exactly when), odd things happen to the call to the Match function.
If I execute that line in the immediate window, I get the following:
lMyRow = WorksheetFunction.Match(vItemID, rngMyRange.Columns(1), 0)
?lMyRow
10
i.e. the lookup works, and lMyRow gets a value assigned to it. If I let that statement execute in the actual code, I lMyRow gets a value of 0.
This seems very odd! I don't understand how executing something in the immediate window can succeed in assigning a value, where the same call, at the same point in program execution can give a value of 0 when it runs normally in code!
The only thing I can think of is that it's some odd casting thing, but I get the same behaviour taking if the variable to which I'm assigning is an int, a double, or even a string.
I don't even know where to begin with this - help!!
The only difference between the immediate window and normal code run is the scope.
Code in the immediate window runs in the current application scope.
If nothing is currently running this means a global scope.
The code when put in a VBA function is restricted to the function scope.
So my guess is that one of your variables is out of scope.
I would put a breakpoint in your function on that line and add watches to find out which variable is not set.
And if you don't have Option Explicit at the top of your vba code module, you should add it.
You're not assigning the function name so that function will always return zero (if you're expecting a Long). It seems you should have
makeTheLookup = lMyRow
at the end of your function.
I don't know if you are still looking at this or not but I would have written it this way:
Function makeTheLookup(vItemID As Variant, rngMyRange as Range)as Long
makeTheLookUp = WorksheetFunction.Match(vItemID, rngMyRange.Columns(1), 0)
End Function
I cannot reproduce the problem with Excel 2007.
This was the code I used:
Sub test()
Dim vItemID As Variant
Dim lMyRow As Long
Dim rngMyRange As Range
Set rngMyRange = ActiveWorkbook.Sheets(1).Range("A1:Z256")
vItemID = 8
lMyRow = WorksheetFunction.Match(vItemID, rngMyRange.Columns(1), 0)
Debug.Print lMyRow
End Sub
It may sound stupid but are you sure that all parameters of the Match function are the same in your macro and in the immediate window? Maybe the range object has changed?
Thanks for the answers guys - I should have been slightly more specific in the way I'm making the call below:
Function makeTheLookup(vItemID As Variant, rngMyRange as Range)
Dim lMyRow As Long
lMyRow = WorksheetFunction.Match(vItemID, rngMyRange.Columns(1), 0)
End Function
The odd thing is, I'm passing the two parameters into the function so I can't see any way they could be different inside and outside of the function. That said, I'm still entirely clueless as to what's causing this, particularly since it's a really intermittent problem
Is there any easy way of comparing the range object in the function context to the range object in the Immediate window context and telling if they're different? Given that range is a reference type, it feels like I should just be able to compare two pointers, but I've got no idea how to do that in VBA!
I'm using Excel 2007 by the way, although I'm not sure if that makes any difference.
As will has mentioned above, It most definitely seems like a problem of scope. Or an Obscure Bug.
I would try to use Debug.print statements, as well as Watch, and see if they match up, and take it from there.
Related
I am attempting to reduce the amount of clutter on my code by creating "shortcuts" if you will
For instance, I always have to type
ThisWorkBook.ActiveSheet.Range
Is there a way for me to define the above to create a less wordy macro? I have tried convert to range and string and the former returns an error (but I could still get intellisense recognize and attempt to autofill) while the string version doesnt work.
Just like in any programming language, you can use variables to store data
For example:
Dim myrange As Range: Set myrange = Sheets("Sheet1").Range("B5")
Alternatively, if you will be working with the same object multiple times, you can use the With keyword
For example. instead of writing you want to work with table every time on every new line you can do
With Sheets("Sheet1").ListObjects("Table1")
.ListRows.Add
.ListColumns(2).Range(3) = "Hello World!"
' ... and so on
End With
Also, please on a sidenote: Avoid using Select/ActiveSheet/ActiveWorkbook and so on!
More info on how to here
You can create functions or customized properties, which are always evaluated when called
Property Get pARng As Range
Set pARng = ThisWorkBook.ActiveSheet.Range
End Property
Function fARng As Range
Set fARng = ThisWorkBook.ActiveSheet.Range
End Function
'Usage
Sub xxx
'...
pARng.Rows(1).Activate
'Same as ThisWorkBook.ActiveSheet.Range.Rows(1).Activate
fARng.Rows(1).Activate
'using function instead achieves same result
End Sub
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 trying to write a simple UDF, which is supposed to loop through each of the cells of a given_row, and pick up those cells which do not have any precedent ranges, and add up the values.
Here is the code:
Public Function TotalActualDeductions(given_row As Integer) As Integer
Total_Actual_Deduction = 0
For present_column = 4 To 153
Set precedent_range = Nothing
If Cells(2, present_column) = "TDS (Replace computed figure when actually deducted)" Then
On Error Resume Next
Set precedent_range = Cells(given_row, present_column).Precedents
If precedent_range Is Nothing Then
Total_Actual_Deduction = Total_Actual_Deduction + Cells(given_row, present_column)
End If
End If
Next
TotalActualDeductions = Total_Actual_Deduction
End Function
If I try to run it by modifying the top declaration, from:
Public Function TotalActualDeductions(given_row As Integer) As Integer
to:
Public Function TotalActualDeductions()
given_row = 4
then it runs successfully, and as expected. It picks up exactly those cells which match the criteria, and all is good.
However, when I try to run this as a proper UDF from the worksheet, it does not work. Apparently, excel treats those cells which have no precedents, as though they have precedents, when I call the function from the worksheet.
I don't know why this happens, or if the range.precedents property is supposed to behave like this.
How can this be fixed?
After a lot of searching, I encountered this:
When called from an Excel VBA UDF, Range.Precedents returns the range and not its precedents. Is there a workaround?
Apparently, "any call to .Precedents in a call stack that includes a UDF gets handled differntly".
So, what I did was use Range.Formula Like "=[a-zA-Z]" , because it satisfied my simple purpose. It is in no way an ideal alternative to the range.precedents property.
Foe a more detailed explanation, please visit that link.
I had a line of VBA code that basically looked like this:
MyControls.Add(Factory.CreateMyControl(param1, param2))
Where Factory.CreateMyControl is just a sneaky way to allow the new instance of my class module being returned to have a constructor.
It was working without any issues for several weeks. Suddenly, it begins throwing the error object doesn't support this property or method which is baffling me because everything looks like it always has.
After stepping into and through the code, I finally narrowed it down to the line above, and found the issue. The issue was the pair of parentheses surrounding the parameter(s) for the Add function. When I changed the code to the following:
MyControls.Add Factory.CreateMyControl(param1, param2)
It worked just as it always had before the unexpected break.
I now understand that this is the basic syntax in VBA for calling Sub's with parameters: to simply include all parameters in a comma-separated fashion without any parentheses (unless you're setting a Function's return value to another variable or using it's value for some other purpose).
My real question is, why did this suddenly just stop working?
Is it a common occurrence using VBA in Office 2007 for code that once worked to break without warning?
Or could this have been caused by some kind of patch that happened without my knowledge?
With parentheses around, the Sub's parameters per default were passed ByVal instead of ByRef. Without parentheses around, the default is ByRef
Example:
Dim r As Range
Sub test(v As Variant)
MsgBox TypeName(v)
End Sub
Sub main()
Set r = Range("A1")
test r 'ByRef; TypeName is Range
test (r) 'ByVal; TypeName is not Range but the type of the content of A1
End Sub
I need to automatize a process which execute various (a lot) user-defined function with different input parameters.
I am using the solution of timer API found in I don't want my Excel Add-In to return an array (instead I need a UDF to change other cells) .
My question is the following: "Does anybody can explain to me HOW IT IS WORKING?" If I debug this code in order to understand and change what I need, I simply go crazy.
1) Let say that I am passing to the public function AddTwoNumbers 14 and 45. While inside AddTwoNumber, the Application.Caller and the Application.Caller.Address are chached into a collection (ok, easier than vectors in order not to bother with type). Application.Caller is kind of a structured object where I can find the function called as a string (in this case "my_function"), for example in Application.Caller.Formula.
!!! Nowhere in the collection mCalculatedCells I can find the result 59 stored.
2)Ok, fair enough. Now I pass through the two UDF routines, set the timers, kill the timers.
As soon as I am inside the AfterUDFRoutine2 sub, the mCalculatedCell(1) (the first -- and sole -- item of my collection) has MAGICALLY (??!?!?!!!??) obtained in its Text field exactly the result "59" and apparently the command Set Cell = mCalculatedCells(1) (where on the left I have a Range and on the right I have ... I don't know) is able to put this result "59" into the variable Cell that afterward I can write with the .Offset(0,1) Range property on the cell to the right.
I would like to understand this point because I would like to give MORE task to to inside a single collection or able to wait for the current task to be finished before asking for a new one (otherwise I am over-writing the 59 with the other result). Indeed I read somewhere that all the tasks scheduled with the API setTimer will wait for all the callback to be execute before execute itself (or something like this).
As you can see I am at a loss. Any help would be really really welcomed.
In the following I try to be more specific on what (as a whole)
I am planning to achieved.
To be more specific, I have the function
public function my_function(input1 as string, Field2 as string) as double
/*some code */
end function
I have (let's say) 10 different strings to be given as Field2.
My strategy is as follow:
1)I defined (with a xlw wrapper from a C++ code) the grid of all my input values
2)define as string all the functions "my_function" with the various inputs
3)use the nested API timer as in the link to write my functions IN THE RIGHT CELLS as FORMULAS (not string anymore)
3)use a macro to build the entire worksheet and then retrieve the results.
4)use my xlw wrapper xll to process further my data.
You may wonder WHY should I pass through Excel instead of doing everything in C++. (Sometime I ask myself the same thing...) The prototype my_function that I gave above has inside some Excel Add-In that I need to use and they work only inside Excel.
It is working pretty well IN THE CASE I HAVE ONLY 1 "instance" of my_function to write for the give grid of input. I can even put inside the same worksheet more grids, then use the API trick to write various different my_functions for the different grids and then make a full calculation rebuild of the entire worksheet to obtain the result. It works.
However, as soon as I want to give more tasks inside the same API trick (because for the same grid of input I need more calls to my_function) I am not able to proceed any further.
After Axel Richter's comment I would like to ad some other information
#Axel Richter
Thank you very much for your reply.
Sorry for that, almost surely I wasn't clear with my purposes.
Here I try to sketch an example, I use integer for simplicity and let's say that my_function works pretty much as the SUM function of Excel (even if being an Excel native function I could call SUM directly into VBA but it is for the sake of an example).
If I have these inputs:
input1 = "14.5"
a vector of different values for Field2, for instance (11;0.52;45139)
and then I want to write somewhere my_function (which makes the sum of the two values given as input).
I have to write down in a cell =my_function(14.5;11), in the other =my_function(14.5;0.52) and in a third one =my_function(14.5;45139).
These input changes any time I need to refresh my data, then I cannot use directly a sub (I think) and, in any case, as far as I understand, in writing directly without the trick I linked, I will always obtain strings : something like '=my_function(14.5;0.52). Once evaluated (for example by a full rebuild or going over the written cell and make F2 + return) will give me only the string "=my_function(14.5;0.52)" and not its result.
I tried at the beginning to use an Evaluate method which works well as soon as I write something like 14.5+0.52, but it doesn't work as soon as a function (nor a user-defined function) is used instead.
This is "as far as I can understand". In the case you can enlighten me (and maybe show an easier track to follow), it would be simply GREAT.
So far the comments are correct in that they repeat the simple point that a User-Defined Function called a worksheet can only return a value, and all other actions that might inject values elsewhere into the worksheet calculation tree are forbidden.
That's not the end of the story. You'll notice that there are add-ins, like the Reuters Eikon market data service and Bloomberg for Excel, that provide functions which exist in a single cell but which write blocks of data onto the sheet outside the calling cell.
These functions use the RTD (Real Time Data) API, which is documented on MSDN:
How to create a RTD server for Excel
How to set up and use the RTD function in Excel
You may find this link useful, too:
Excel RTD Servers: Minimal C# Implementation
However, RTD is all about COM servers outside Excel.exe, you have to write them in another language (usually C# or C++), and that isn't the question you asked: you want to do this in VBA.
But I have, at least, made a token effort to give the 'right' answer.
Now for the 'wrong' answer, and actually doing something Microsoft would rather you didn't do. You can't just call a function, call a subroutine or method from the function, and write to the secondary target using the subroutine: Excel will follow the chain and detect that you're injecting values into the sheet calculation, and the write will fail.
You have to insert a break into that chain; and this means using events, or a timer call, or (as in RTD) an external process.
I can suggest two methods that will actually work:
1: Monitor the cell in the Worksheet_Change event:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim strFunc As String strFunc = "NukeThePrimaryTargets" If Left(Target.Formula, Len(strFunc) + 1) = strFunc Then Call NukeTheSecondaryTargets End If End Sub
Alternatively...
2: Use the Timer callback API:
However, I'm not posting code for that: it's complex, clunky, and it takes a lot of testing (so I'd end up posting untested code on StackOverflow). But it does actually work.
I can give you an example of a tested Timer Callback in VBA:
Using the VBA InputBox for passwords and hiding the user's keyboard input with asterisks.
But this is for an unrelated task. Feel free to adapt it if you wish.
Edited with following requirements: It is necessary to run a user defined worksheet function, because there are addins called in this function and those work only within a Excel sheet. The function has to run multiple times with different parameters and its results have to be gotten from the sheet.
So this is my solution now:
Public Function my_function(input1 As Double, input2 As Double) As Double
my_function = input1 + input2
End Function
Private Function getMy_Function_Results(input1 As Double, input2() As Double) As Variant
Dim results() As Double
'set the Formulas
With Worksheets(1)
For i = LBound(input2) To UBound(input2)
strFormula = "=my_function(" & Str(input1) & ", " & Str(input2(i)) & ")"
.Cells(1, i + 1).Formula = strFormula
Next
'get the Results
.Calculate
For i = LBound(input2) To UBound(input2)
ReDim Preserve results(i)
results(i) = .Cells(1, i + 1).Value
Next
End With
getMy_Function_Results = results
End Function
Sub test()
Dim dFieldInput2() As Double
Dim dInput1 As Double
dInput1 = Val(InputBox("Value for input1"))
dInput = 0
iIter = 0
Do
dInput = InputBox("Values for fieldInput2; 0=END")
If Val(dInput) <> 0 Then
ReDim Preserve dFieldInput2(iIter)
dFieldInput2(iIter) = Val(dInput)
iIter = iIter + 1
End If
Loop While dInput <> 0
On Error GoTo noFieldInput2
i = UBound(dFieldInput2)
On Error GoTo 0
vResults = getMy_Function_Results(dInput1, dFieldInput2)
For i = LBound(vResults) To UBound(vResults)
MsgBox vResults(i)
Next
noFieldInput2:
End Sub
The user can input first a value input1 and then input multiple fieldInput2 until he inputs the value 0. Then the results will be calculated and presented.
Greetings
Axel