VBA Assign value (only) to variable - vba

I've hit a snag, and my searching hasn't helped so far.
I have a variant being passed into a function which I then intend to copy, perform some calculations, take another copy, perform some other calculations then compare the results of the two copies...
However, when I perform the calculations on one copy, the original variant is also manipulated... so after two copies, and two calculations I end up with 3 variants that are equal to each other and different to the original... Not what I intended.
I expect this is happening because when I use NewVar = OldVar I'm actually taking a reference to the original object. What I actually want, is to make an independent duplicate of the original object - i.e. copy the value of the variable similar to byval in a function delcaration.
My code is linked here: https://1drv.ms/x/s!AiPgb0BH-YZ_ga956eMmJbSdihGjyg.
If you put a break on line 67 of modMain, then watch CutList(1).QTY (the original variable), and CutTrial.RemainingCuts(1).QTY you'll see that both the QTY values decrement when you step through line 67... I want CutList(1).QTY to remain unchanged, and CutTrial.RemainingCuts(1).QTY to decement only.
Any suggestions?

Make sure the functions definition is as follows
Public function DoMagic(ByVal variable as something)
Don't use ByRef or as you found it will modify the reference.
If you are using an object, array or collection you will need to first copy it before using it.
Eg something like the following:
Public Function Clone() As Class1
Set Clone = New Class1
Clone.prop1 = prop1
Clone.PrivateThing = PrivateThing
End Function

Related

vb.net Passing a list through a function without list value changed [duplicate]

This question already has answers here:
Argument passed ByVal to VB.NET Function and manipulated there
(2 answers)
Closed 4 years ago.
The list value changeed here when passed ByVal why
,it must be not changed.
Private Sub Button6_Click(sender As Object, e As EventArgs) Handles Button6.Click
Dim value As Integer = 1
Dim value2 As New List(Of Decimal)
value2.Add(1)
value2.Add(2)
' The integer value doesn't change here when passed ByVal.
Example1(value)
Console.WriteLine(value)
' The list value changeed here when passed ByVal.
Example3(value2)
Console.WriteLine(value)
End Sub
Sub Example1(ByVal test As Integer)
test = 10
End Sub
Sub Example3(ByVal test As List(Of Decimal))
test.Add(3)
End Sub
its solved ,the solution is making new copy:
Sub Example3(ByVal test As List(Of Decimal))
Dim testnew As New List(Of Decimal)
testnew.AddRange(test)
testnew.Add(3)
End Sub
You need to do some reading on value types and reference types and passing method arguments by value and by reference. They are related but not the same thing. When you pass a method argument by value, you create a copy of the variable being passed. If the variable is a value type, i.e. a structure, then that means creating a copy of the value. If the variable is a reference type, i.e. a class, then that means creating a copy of the reference. The thing is, the original reference and the copy still both refer to the same object.
The reason that reference types exist is that you wouldn't want to create copies of large objects every time you assigned them somewhere. In the case of passing a collection to a method, it's almost always the case that any change you make inside the method you will want to be reflected outside. In the rare case that you don't, it's up to you to create a copy of the collection first and pass that in.
When you pass a value type by value, you create a copy of the value. That means that no changes you make inside the method can affect the original variable. You can assign a new value to the parameter or you can set a property of the value and the change will not be reflected outside the method. Of course, value types should generally be immutable and so setting a property should not be possible, but there are times that that "rule" gets broken.
When you pass a reference type by value, you create a copy of the reference. That means that assigning a different object to the parameter inside the method will not affect the original variable. There is still only one object though, referred to by the original variable and the parameter. As such, if you set a property of that object via the parameter then that change will be reflected in the original variable, because it's the same object.
When you pass a value type by reference, you create a new reference to the value. That means that any changes you make inside the method will affect the original variable. You can assign a new value to the parameter or you can set a property of the value and the change will be reflected outside the method.
When you pass a reference type by reference, you create a new reference to the original reference. That means that assigning a different object to the parameter inside the method will affect the original variable. There's still just one object, so setting a property on the parameter will still affect the original variable too.
Those are the only four possibilities: value type by value, reference type by value, value type by reference and reference type by reference. In none of those scenarios is a copy of a reference type object made so in none of those scenarios can you set a property of a reference type object via a method parameter and have that change not be reflected in the original variable.
If you want a copy of the original object then it's up to you to create one explicitly. Whether you do that inside the method or outside really depends on the specific circumstances. That means that you need to change your code to this:
Sub Example3(ByVal test As List(Of Decimal))
Dim copy = test.ToList()
copy.Add(3)
End Sub
or this:
Dim copy = value2.ToList()
Example3(copy)
Let me just repeat the important point here: there is NO WAY to pass a reference type object to a method, modify the object via the parameter inside the method and have that not affect the original variable (assigning a different object to the parameter is NOT modifying the object). If you want a modification inside the method to not affect the original variable then you need a copy of the object and the ONLY way that will happen is if YOU do it explicitly.

How to find out where a function is being called from in MS Access?

I am working on an MS Access database application that was created by someone else. There is one particular line of code (a Function) that will randomly get called and I have no idea why it is being called or what it does. I have searched (ctrl+F) the entire project for something that calls this function but I can't find it. How can I find out why this Function is being called? (See below). Thank you!
Public Function Concat(strIOSC As String, strFeature As String) As String
Static strLastIOSC As String
Static strFeatures As String
If strIOSC = strLastIOSC Then
strFeatures = strFeatures & ", " & strFeature
Else
strLastIOSC = strIOSC
strFeatures = strFeature
End If
Concat = strFeatures
End Function
If you have only searched the scripts and modules, then your scope is too narrow.
A public function like this can also be used in expressions, so you need to check queries, reports, form controls, macros, and possibly even tables if you use calculated fields. Depending on the size of the database, and how often the function is called, you can either search manually in a targeted way or possibly use a public sub to output something searchable. This sub can get you started. I think it outputs every possible location for expressions. Unfortunately, each object will have its own text file which will need to be searched separately unless you build a sub to do that too.
As for what your function does, it looks like it logs each input using the Static strLastIOSC variable, compares to the arguments passed on the second function call, and if they match it concatenates the two strFeature inputs together and outputs the result.
So basically the first argument tells the function whether this is the beginning of a new concatenation instance, or the continuation of an existing instance. The second argument is the item to be concatenated.
The Static keyword means that the value is stored even after the function runs so it can compare the last call with the current call to determine whether to add the second argument to the one saved from before, or clear the memory and prepare for a new concatenation.
Given its design, it's probably being used in a query/report/form, where strIOSC is likely a primary key field or a field in a GROUP BY.

Integer in VB.net turns into String in VBA

I have a list of objects that are "keys" into a table - the keys are simply the items in the first column. They can be Integers or Strings, depending on what DB table we read it from. Since we use them a lot, we cache that column in an ArrayList called "Keys".
We wrote cover methods to return Row, one that uses strings and the other integers. If you call the integer version it simply returns that row by index. If you call the string, it looks down Keys for a match, and uses that index to pull out the row.
Ok, so now I pass Keys to Excel, pull out one of them in a loop, and ask it what it is...
TypeName(DB.Keys(i))
And the object returns...
Long
Great, the keys must have been integers! So now I'll try to get the row for that key, by calling the accessor method, Row...
DB.Row(DB.Keys(i))
And it calls into the version that takes a String.
Whoa!
Can anyone think of a reason that VBA calling back into our VB.net DLL ends up calling the wrong accessor?
ADDED CODE: I can't figure out how to put the code in as a reply, so I'm editing this. Here is the code in the VB.net class:
Public Function Row(ByVal K As String) As DataRow
Dim R As DtaRow = DB.Tables(0).Rows(K)
Return New DataRow(R)
End Function
Public Function Accounts(ByVal K As Integer) As DataRow
Dim R As DtaRow = DB.Tables(0).Rows(K)
Return New DataRow(R)
End Function
If you're wondering, there's two versions of Rows, which take strings or ints.
This code works perfectly from VB.net. You can ask for a row by key string or by the row number, that invokes the proper Row, which calls the proper Rows, and out comes the proper answer.
But in VBA, it always calls the method with the string input. If I rename it to RowIHATEYOUALL then I can call the Integer version just fine. But when there are two of them, differing only in signature, no such luck.
And the A and i (see comments) was my typo.
The interop layer does not support overloaded methods. Whenever you call Row, the first declared method with that name is used. The .NET overload resolution algorithm does not apply here.
Other overloads are exposed to VBA as Row_2, Row_3, etc. Thus, the following code should do what you expect:
DB.Row_2(DB.Keys(i))
This implicit dependency on the order of declaration has a high potential for error. Thus, I would suggest to either
give the methods unique names if they are called from VBA,
or, if you want to retain the "nice" overloaded version for .NET, add compatibility methods for VBA:
<Obsolete("Compatibility method for VBA, use Row instead.")>
Public Function RowByKeyVBA(ByVal K As String) As DataRow
Return Row(K)
End Function
<Obsolete("Compatibility method for VBA, use Row instead.")>
Public Function RowByNumberVBA(ByVal K As Integer) As DataRow
Return Row(K)
End Function
Further information can be found in the following question:
COM->.NET - can't access overloaded method
Following Heinzi's notes (above) I fixed this by making three method signatures for each call, one takes an Object and then attempts to figure out what it is, the others take the String and Integers. Within VB/C#/etc the proper String or Integer methods get called as expected, from VBA the Object version is called, as Heinzi noted
This causes the very minor issue that a user may have a "number like value" that is actually a String. For instance, the array keys might be "User3232" or "3232", both of which are String objects in the table. So you have to be careful, simply asking if the Object can be converted to an Int32 is not enough. This is unlikely to be something that effects most users.

Scope issue in Access VBA Dictionary?

I'm having a strange problem. I'm new to Access and VBA, so it may be a stupid mistake.
Private backColorCycle As Integer
Private doneRows As New Dictionary
Private Sub AlternateGroupColor()
If Not doneRows.Exists(Me.JCH_Shape) Then
'... some stuff that assigns a value to backColorCycle
Else
'... some stuff that assigns a value to backColorCycle
End If
doneRows.Item(Me.JCH_Shape) = backColorCycle
Detail.BackColor = QBColor(doneRows.Item(Me.JCH_Shape))
GroupHeader0.BackColor = QBColor(doneRows.Item(Me.JCH_Shape))
End Sub
AlternateGroupColor() is an event handler that is called repeatedly (by the OnFormat event in Access). Me.JCH_Shape, a string, cycles through a set of values twice: it might be A, B, C, A, B, C as the function is called, so I want to know when a value has been encountered already. I hoped to determine this by storing the value in doneRows and checking to see if the value already exists. However, even though I've checked that Me.JCH_Shape does indeed have different values and doneRows.Item(Me.JCH_Shape) does return the value I expect at the end of the function, doneRows.Count is never greater than 1. It seems as if the changes I make to the dictionary in the sub are reset every time it is called, and I'm not sure why. It feels like a scope problem, but I can't understand why this would happen to a variable that is a member of the class and not just the function.
Any help would be appreciated. Thanks.
A Dictionary allows objects to serve as its key. Me.JCH_Shape is an object on your report, so when you do
doneRows.Item(Me.JCH_Shape) = backColorCycle
you are repeatedly re-assigning the value to the Dictionary entry whose key is the Me.JCH_Shape object itself. If you want to store Dictionary items based on the current value of the Me.JCH_Shape object you need to use
doneRows.Item(Me.JCH_Shape.Value) = backColorCycle

VB .NET - how can I create an array of references to doubles?

I have a bunch of type Double variables in my program, say for example
Dim Area as Double = 0
Dim Perimeter as Double = 0
Somewhere in my program I want to calculate these values, so I define
Public Sub TheSquare(ByRef TheArea as Double, ByRef ThePerim as Double, ByVal TheSide as Double)
TheArea = TheSide^2
ThePerim = 4 * TheSide
End Sub
and somewhere in the program I'm collecting side lengths and calculating the area and perimeter; say
While True
S = GetSideValueFromSomewhere()
TheSquare(Area, Perimeter, S)
End
In my real program, I have, say, 20 quantities that I want to calculate. Obviously each one has a different equation. But in the end I want to output all 20 to a file, so to save typing, I create an array of the quantities, like this:
Dim TypingSaver() as Double = {Area, Perimeter}
so that I can dump values to file with a three-line for-loop instead of copying and pasting 20 variable names.
This does exactly what I want if Area and Perimeter were reference types like Objects. But since they are Doubles, TypingSaver contains only their values, not references to the actual variables. So after I use my TheSquare function the values of Area and Perimeter are correctly updated but TypingSaver just constains whatever the values of Area and Perimeter were when I declared the array.
So the question is: how can I create an array of references to doubles in VB.NET?
With approach that you are doing you can't do this, since as soon as you created array you copied all variables to the array and any changes that you are doing on variables are not reflected on array variables (like you pointed out).
What I would recommend create another class that will contain all your variables (20 variables name) as properties (get and set) and then override ToString method which will return list of all your variables. So when you need to dump those variables you will call ToString() method and it will return current values of all your parameters.
There is a nasty way to do this. All numeric types are value types, but arrays are reference types. So I can change all my Doubles to arrays of doubles, like this:
Dim Area(0) as Double
Dim Perimeter(0) as Double
So now Area and Perimeter are 1-element arrays of double. My "looping array" then becomes
Dim TypingSaver() as Array = {Area, Perimeter}
Now TypingSaver stores references to Area and Perimeter. For me this was an easy change because I could search-and-replace for the Double declaration, change the type for TypingSaver, then in two other places I had to change direct access of this form:
TypingSaver(1) = 7
to
TypingSaver(1).SetValue(7, 0)
Not pretty, but it keeps my code consistent in that I have other "looping arrays" for other objects that are all related to each other.
Although it is not clear in my question the real solution is to restructure everything so instead of storing everything in arrays I crate a class which has all the objects I need and create a single array of that, as suggested in part by Blezden.
The single-element array idea was actually terrible. The code became horrendously slow. There is another work around a friend suggested: create a wrapper class like this:
Public Class DoubleWrapper
Public Value As Double
End Class
Then when an array of DoubleWrappers is created it will be by reference, of course.
I understand this is not ideal but what I was looking for was a workaround until I have the time to rewrite the code from scratch.
Fairly similar to your most recent answer, you'll want to wrap your types as "Nullable", which basically makes it an object that could be null, but also a reference type.
Ie. Dim testDouble as Nullable(Of Double) or Dim testDouble2 as Double?
See:
Nullable(Of T) Structure - MSDN
What is the integer reference type in C#?