ByVal in Excel event-handler - vba

I am a little confused about the usage of ByVal keyword in some event-handlers in Excel.
The NewSheet event example
(Copied from Excel 2019 Power Programming with VBA, Wiley)
Private Sub Workbook_NewSheet(ByVal Sh As Object)
If TypeName(Sh) = "Worksheet" Then
Sh.Cells.ColumnWidth = 35
Sh.Range("A1") = "Sheet added " & Now()
End If
End Sub
This code works as expected, as seems trivial. But after some thought I am getting more and more confused.
Based on my understanding ByVal means Sh is just copy of the the original worksheet object, and procedures using ByVal arguments won't result in change to the original object. In other words, what the code does should have no effect.
Only when references are passed to procedures will they be able to modify objects referred to by those references.
Am I missing something there? Thanks
P.S. Most of my understanding of passing by reference/value comes from other programming languages such as C#. There might be some peculiarity in VBA I am not aware of.

Since you're passing an Object, ByVal passes a copy (the value) of the reference. The copy (value) of the reference still points to what the original reference is pointing to. Hence your method works and changes what the original reference is pointing to.
If you know Java, this is similar in nature to Java, where everything can be said to 'pass by value' but when you pass an Object as a parameter, what you are actually doing is passing a copy of the reference, and not a copy of what the reference is pointing to.

ByVal in those cases prevents the change of the value in the original reference itself.
Please see next:
Module Module1
Sub Main()
' Declare an instance of the class and assign a value to its field.
Dim c1 As New Class1()
c1.Field = 5
Console.WriteLine(c1.Field)
' Output: 5
' ByVal does not prevent changing the value of a field or property.
ChangeFieldValue(c1)
Console.WriteLine(c1.Field)
' Output: 500
' ByVal does prevent changing the value of c1 itself.
ChangeClassReference(c1)
Console.WriteLine(c1.Field)
' Output: 500
Console.ReadKey()
End Sub
Public Sub ChangeFieldValue(ByVal cls As Class1)
cls.Field = 500
End Sub
Public Sub ChangeClassReference(ByVal cls As Class1)
cls = New Class1()
cls.Field = 1000
End Sub
Public Class Class1
Public Field As Integer
End Class
End Module

Related

Using a delegate for thread start inside another Sub

What I've got is something like this:
Private Sub GiantLegacySub()
... lots of variables and legacy code...
Dim somethingNew = New Func(of String, Boolean)(
Function(stringy as String) As Boolean
... new code that uses the legacy variables ...
End Function)
Dim t = New Thread(AddressOf somethingNew)
End Sub
I am getting an error indicating that somethingNew is being seen as variable name and not a method name and is thus unacceptable by AddressOf. ( I know that somethingNew is a variable, just one that happens to contain a pointer to a method).
Is there a way to do this? I need to leave it inside of GiantLegacySub because of the shear volume of variables in its scope.
Based on Craig's guidance, this was the answer:
Private Sub GiantLegacySub()
... lots of variables and legacy code...
Dim somethingNew = Sub(stringy as String)
... new code that uses the legacy variables ...
End Sub
Dim t = New Thread(somethingNew)
t.Start(someStringForStringy)
End Sub

Calling Variables

Can I call variables from outside the Public Sub which was defined in in Visual Basic?
This is rather basic-level stuff, but if you don't know what to search for, it can be hard to know how to find the information. This should get you started.
You don't "call" variables, you "call" procedures. A variable that's declared with the Dim keyword inside a procedure scope, is local to that scope it's declared in, so no.
This is best observed with Option Explicit specified.
Option Explicit
Public Sub Test1()
Dim foo As Long
End Sub
Public Sub Test2()
foo = 42 ' illegal: variable is not declared / not accessible in this scope
End Sub
The concept to understand here, is scoping. Use Dim to declare local variables. As the name says, such variables only exist in the scope they're declared in.
Next you have module scope. You can use the Dim keyword outside of a procedure scope, at the top of the module for this, but for consistency it's probably a better idea to use the Private keyword.
Option Explicit
Private foo As Long
Public Sub Test1()
foo = 42
End Sub
Public Sub Test2()
MsgBox foo
End Sub
This code will compile, and if Test1 is invoked before Test2, the Test2 call will pop 42 in a message box.
Then you have public scope, aka "global". You can use the obsolete Global keyword for those, but for consistency it's better to use the Public keyword.
Option Explicit
Public foo As Long
Public Sub Test1()
foo = 42
End Sub
Option Explicit
Public Sub Test2()
MsgBox foo
End Sub
The above will do exactly the same as the previous snippet, except now we have the two procedures in separate modules (standard modules - it's important). And the code will compile and run.
Rule of thumb, you don't need to declare global variables.
Variables should always be scoped as tightly as possible, and passed around as parameters. Parameters can be passed by value (ByVal) or, by reference (ByRef). If unspecified, ByRef is the [unfortunate] default.
Option Explicit
Public Sub Test()
Dim foo As Long
Assign foo
MsgBox foo
End Sub
Private Sub Assign(ByVal bar As Long)
bar = 42
End Sub
Running Test will pop a message box saying 0, because ByVal passes a copy of the value (or a copy of the pointer to the object reference, when we're talking about objects).
Contrast with:
Option Explicit
Public Sub Test()
Dim foo As Long
Assign foo
MsgBox foo
End Sub
Private Sub Assign(ByRef bar As Long) ' or implicit: (bar As Long)
bar = 42
End Sub
This will pop a message box saying 42, because ByRef passes a pointer to the value (or in the case of object references, the pointer itself). Note that this is usually not behavior you want to allow, hence most parameters should be passed by value.

How can I assign a value to a number of different variables in a collection using loops?

I have a problem that has been bugging me for a while now. Consider this code:
Public Class Class1
Dim VariableList as New List(of Object) From {MainForm.X, MainForm.Y,
SettingsForm.Z, SettingsForm.Textbox1.Text} '...etc.
Sub SetToZero()
For Each Element in VariableList
Element = 0
Next
End Sub
Sub SetToCustomValue(value As Double)
For Each Element in VariableList
Element = value
Next
End Sub
Sub LoadValuesFromFile()
Dim path As String = MainForm.GetPath()
For Each Element in VariableList
Element = File.Readline()
Next
End Sub
Sub SaveValuesToFile()
Dim path As String = MainForm.GetPath()
For Each Element in VariableList
Element = File.Writeline()
Next
End Sub
'and more similar functions/subs
As you can see, what this class does is that it takes lot of different variables from different places into a collection, and then various functions read or write values to every variable in that collection using loops. In this example, I have just a few variables, but most of the time there are dozens.
Reading the values is not a problem. Writing them, is, because when I declare that VariableList at the top of my class, that List just makes a copy of each variable, rather than maintaining a reference to it. Meaning that if, say, one of the functions modifies the MainForm.X in that List, the actual variable MainForm.X is not modified. To work with references, I would have to forgo loops, and assign every single variable manually, in every function. Which is obviously a lot of bad code. I want to declare that list of variables only once, and then use loops, like in this example code that I wrote above. My question is, how can I make such a container (List, Array, whatever) that would retain the references to the original variables in it, and make the code above possible?
There is no easy way to store pointers to variables in VB.NET. As a workaround, you can use a class to store your variables, as a class is always used as a pointer.
Here's an example of a way to achieve this with a ContainerClass which own a Dictionary of integers. One interest of this method would be that you can declare and name "variables" dynamically. In reality, they will be managed KeyValuePair. Once you have instantiated a copy of this class, you can use it to "manage" your variables by using this class as your pointer.
I included a loop which set all the integers to the same number just for fun, and to demonstrate the kind of manipulation which would end up having an effect similar to one of those described in your question.
Public Class Form2
'This is the container class which will be used to bypass the lack of pointers
'if you wanted to change a property, like the window width, it would be more difficult, but simples variables will be no trouble
Private variableContainer As New VariableContainer
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
variableContainer.AddVar("X", 5)
variableContainer.AddVar("Y", 15)
Debug.Print(variableContainer.GetVar("X"))
Debug.Print(variableContainer.GetVar("Y"))
variableContainer.SetAllVar(42)
Debug.Print("Next line will print 42")
Debug.Print(variableContainer.GetVar("X"))
End Sub
End Class
Public Class VariableContainer
'I know a public variable wouldn't need the fancy functions down there, but it's usually better to encapsulate, especially if you're working with a team
'and "future you" count as a teammate, never forget that...
Private list As New Dictionary(Of String, Integer)
Public Sub AddVar(ByVal name As String, ByVal value As Integer)
list.Add(name, value)
End Sub
Public Function GetVar(ByVal name As String) As Integer
If list.ContainsKey(name) Then
Return list(name)
Else
'I choose -1 arbitrarily, don't put too much thinking into this detail
Return -1
End If
End Function
Public Sub SetVar(ByVal name As String, ByVal num As Integer)
If list.ContainsKey(name) Then
list(name) = num
End If
End Sub
Public Sub SetAllVar(ByVal num As Integer)
Dim dict As New Dictionary(Of String, Integer)
For Each item As KeyValuePair(Of String, Integer) In list
dict.Add(item.Key, num)
Next
list = dict
End Sub
End Class
Have fun!

VBA - Safely store variable reference

I'm familiar with passing an argument to a procedure by reference. Alternately, ParamArray allows me the flexibility of passing 0 or more arguments to a procedure by reference as well. However, that approach made me wonder if there was a way to preserve a reference to one or more variables beyond the scope of a procedure. My first glimmer of hope was the VBA Array function when I saw it was declared like this:
Array(ParamArray ArgList() As Variant)
So, I put together the following test code:
Private Sub Test()
Dim a As Object
Dim b() As Variant
ParamArrayTest a
Debug.Print TypeName(a) ' Output is 'Dictionary'
b = Array(a) ' b should be like ParamArray ArgList()
Set b(0) = Nothing ' This should clear a
Debug.Print TypeName(a) ' Output is still 'Dictionary'
End Sub
Private Sub ParamArrayTest(ParamArray ArgList() As Variant)
Set ArgList(0) = CreateObject("Scripting.Dictionary")
End Sub
Unfortunately, this did not work as I expected. Despite the argument being passed into the Array function via ParamArray, it would appear that the returned array was by value and not by reference.
Further research led me to the undocumented VBA VarPtr / StrPtr / ObjPtr functions. I found numerous examples of using them in conjunction with the API RtlMoveMemory function. However, all of the articles I read strongly urged against using that approach since it could very easily crash the application. Some of my testing did indeed crash Access.
Another idea I had was to see if I could directly assign a reference of one variable to another:
Private Sub Test()
Dim a As Object
Dim b As Variant
b = ByRef a ' Throws a compiler error
End Sub
Suffice it to say, the compiler simply would not allow that. My question then is, can a variable reference be safely stored / preserved beyond the scope of a procedure (preferably in another variable)?
EDIT
I decided it would be more helpful if I shed some light on what I'm trying to build.
I'm currently in the process of creating a wrapper class which will pass all form / control events to a procedure in one of my modules. It will be used with 2 forms which have the same control structure but connect to different source tables. Bear in mind that the code is incomplete but should be sufficient to illustrate the problem I'm trying to overcome. Also, Database is my VBA project name.
There are four portions to the code:
Form_TEST_FORM - Form Module
Private Sub Form_Open(Cancel As Integer)
FormHub.InitForm Me, Cancel
End Sub
FormHub - Module
Public Sub InitForm( _
ByRef Form As Access.Form, _
ByRef Cancel As Integer _
)
Dim Evt As Database.EventHandler
Set Evt = New Database.EventHandler
Evt.InitFormObject Form, Cancel
FormList.Add Evt, Form.Name
End Sub
Private Function FormList() As VBA.Collection
Static Init As Boolean
Static Coll As VBA.Collection
If Not Init Then
Set Coll = New VBA.Collection
Init = True
End If
Set FormList = Coll
End Function
FormControl - Class Module
Public Ptr As Variant ' Pointer to form control variable
Public acType As Access.AcControlType
EventHandler - Class Module
Private WithEvents Form As Access.Form
Private WithEvents SForm As Access.SubForm
Private CtrlList As VBA.Collection
Private Sub Class_Initialize()
InitCtrlList
End Sub
Public Sub InitFormObject(FormObj As Access.Form, ByRef Cancel As Integer)
Dim ErrFlag As Boolean
Dim Ctrl As Access.Control
Dim FCtrl As Database.FormControl
On Error GoTo Proc_Err
Set Form = FormObj
If Form.Controls.Count <> CtrlList.Count Then
Err.Raise 1, , _
"Form has incorrect number of controls"
End If
' This is where I want to validate the form controls
' and also initialize my event variables.
For Each Ctrl In Form.Controls
If Not CtrlExists(FCtrl, Ctrl.Name) Then
Err.Raise 2, , _
"Invalid control name"
ElseIf FCtrl.acType <> Ctrl.ControlType Then
Err.Raise 3, , _
"Invalid control type"
Else
' Initialize the correct variable with it's
' pointer. This is the part I haven't been
' able to figure out yet.
Set FCtrl.Ptr = Ctrl
End If
Next
Proc_End:
On Error Resume Next
If ErrFlag Then
ClearEventVariables
End If
Set Ctrl = Nothing
Set FCtrl = Nothing
Exit Sub
Proc_Err:
ErrFlag = True
Debug.Print "InitFormObject " & _
"Error " & Err & ": " & Err.Description
Resume Proc_End
End Sub
Private Function CtrlExists( _
ByRef FCtrl As Database.FormControl, _
ByRef CtrlName As String _
) As Boolean
On Error Resume Next
Set FCtrl = CtrlList(CtrlName)
CtrlExists = Err = 0
End Function
Private Sub InitCtrlList()
Set CtrlList = New VBA.Collection
CtrlList.Add SetCtrlData(SForm, acSubform), "SForm"
End Sub
Private Function SetCtrlData( _
ByRef Ctrl As Access.Control, _
ByRef acType As Access.AcControlType _
) As Database.FormControl
Set SetCtrlData = New Database.FormControl
With SetCtrlData
' This assignment is where I need to keep a reference
' to the variable in the class. However, it doesn't
' work.
Set .Ptr = Ctrl
.acType = acType
End With
End Function
Private Sub ClearEventVariables()
Dim FormCtrl As Database.FormControl
Set Form = Nothing
For Each FormCtrl In CtrlList
' Assuming I was able to retain a reference to the
' class variable, this would clear it.
Set FormCtrl.Ptr = Nothing
Next
End Sub
Private Sub Class_Terminate()
ClearEventVariables
Set CtrlList = Nothing
End Sub
I only used 1 control in the code example for simplicity sake. But, the idea is to simplify how much code I would need to modify in order to add / remove controls should the form design change. Or, in the event I have to add more forms to the project.
If you need to reference only within a single module, declare as Public in module header. If you want to reference in any module, declare as Global in a general module header. Even array and recordset and connection objects can be declared this way. Be aware these variables will lose their values if code breaks in runtime.
Or look into TempVar object variables. They don't lose values if code breaks. But can only store number or text values, not objects.

User Defined Type (UDT) as parameter in public Sub in class module (VB6)

I've tried to solve this problem, but can't find any solution. I have a UDT defined in a normal module, and wanted to use it as parameter in a Public Sub in a Class Module. I then get a compile error:
Only public user defined types defined in public object modules can be used as parameters or return type for public procedures of class modules or as fields of public user defined types
I then try to move my UDT in the class, declared as Private. I get this compile error:
Private Enum and user defined types cannot be used as parameters or return types for public procedures, public data members, or fields of public user defined types.
I finaly try to declare it as Public in the class, and get this compile error:
Cannot define a Public user-defined type within a private object module.
So is there any way to have a public UDT used as a parameter in a public sub in a class?
Just define the sub as Friend scope. This compiles fine for me in a VB6 class.
Private Type testtype
x As String
End Type
Friend Sub testmethod(y As testtype)
End Sub
From your error messages it appears your class is private. If you do want your class to be public - i.e. you are making an ActiveX exe or DLL and you want clients to be able to access the sub - then just make both the type and the sub Public.
So is there any way to have a public
UDT used as a parameter in a public
sub in a class?
In a word, no. The closest you can come with just Classic VB code would be to create a class that replicates the UDT and use that instead. There are definitely advantages there, but you're hosed if you need to pass that to, say, an API as well.
Another option is to define the UDT in a typelib. If you do that, it can be used as a parameter for a public method.
Ok, here's how to do it, if I can get my cat to leave me alone, that is.
In Form1 (with one command button on it):
Option Explicit
'
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (ByVal dst As Long, ByVal src As Long, ByVal nBytes As Long)
'
Private Sub Command1_Click()
' Okay, this is what won't work in VB6:
' Dim MyUdt1 As MyUdtType ' Declare a variable with a publicly defined UDT (no problem).
' Form2.Show ' We could have created some object with a class. This was just easier for the demo.
' INSIDE OF FORM2:
' Public Sub MySub(MyUdt2 As MyUdtType) ' It won't even let you compile this.
' Msgbox MyUdt2.l
' MyUdt2.l = 5
' End Sub
' Form2.MySub MyUdt1 ' You'll never get this far.
' Unload Form2
' Msgbox MyUdt1.l
'
' The following is a way to get it done:
'
Dim MyUdt1 As MyUdtType ' Declare a variable with a publicly defined UDT (no problem).
Dim ReturnUdtPtr As Long ' Declare a variable for a return pointer.
MyUdt1.l = 3 ' Give the variable of our UDT some value.
Form2.Show ' Create our other object.
'
' Now we're ready to call our procedure in the object.
' This is all we really wanted to do all along.
' Notice that the VarPtr of the UDT is passed and not the actual UDT.
' This allows us to circumvent the no passing of UDTs to objects.
ReturnUdtPtr = Form2.MyFunction(VarPtr(MyUdt1))
'
' If we don't want anything back, we could have just used a SUB procedure.
' However, I wanted to give an example of how to go both directions.
' All of this would be exactly the same even if we had started out in a module (BAS).
CopyMemory VarPtr(MyUdt1), ReturnUdtPtr, Len(MyUdt1)
'
' We can now kill our other object (Unload Form2).
' We probably shouldn't kill it until we've copied our UDT data
' because the lifetime of our UDT will be technically ended when we do.
Unload Form2 ' Kill the other object. We're done with it.
MsgBox MyUdt1.l ' Make sure we got the UDT data back.
End Sub
In form2 (no controls needed). (This could have just as easily been an object created with a class.):
Option Explicit
'
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (ByVal dst As Long, ByVal src As Long, ByVal nBytes As Long)
'
Public Function MyFunction(ArgUdtPtr As Long) As Long
' Ok, this is how we get it done.
' There are a couple of things to notice right off the bat.
' First, the POINTER to the UDT is passed (using VarPtr) rather than the actual UDT.
' This way, we can circumvent the restriction of UDT not passed into objects.
' Second, the following MyUdt2 is declared as STATIC.
' This second point is important because the lifetime of MyUdt2 technically ends
' when we return from this function if it is just DIMmed.
' If we want to pass changes back to our caller, we will want to have a slightly longer lifetime.
Static MyUdt2 As MyUdtType
' Ok, we're here, so now we move the argument's UDT's data into our local UDT.
CopyMemory VarPtr(MyUdt2), ArgUdtPtr, Len(MyUdt2)
' Let's see if we got it.
MsgBox MyUdt2.l
' Now we might want to change it, and then pass back our changes.
MyUdt2.l = 5
' Once again, we pass back the pointer, because we can't get the actual UDT back.
' This is where the MyUdt2 being declared as Static becomes important.
MyFunction = VarPtr(MyUdt2)
End Function
And Finally, this goes in a module (BAS) file.
Option Explicit
'
' This is just the UDT that is used for the example.
Public Type MyUdtType
l As Long
End Type
'
Just Pass the UDT as Reference parametre and it will work. :)
'method in the class
Public Sub CreateFile(ByRef udt1 As UdtTest)
End Sub
I had the same error message and after checking the application, I found that in the property window for the class, the "Instancing" setting was set to "1 - Private" for the referenced object. I changed it to "5 - MultiUse" and got the same error message. I then went back to a version of the project module prior to when I added that referenced object and added it again the project - it defaulted to "1 - Private". I changed it to "5 - MultiUse" before doing anything else and closed the project to for it to update before compiling. I re-opened the project, verified it was still set to "5 - MultiUse", then compiled the project and it compiled cleanly without the error message.
When the error message was saying it didn't allow referencing a private object, the object really was private. Once I declared it not private, and the project module accepted that new setting, it compiled cleanly.
Define UDF (public type) in a module:
Public Type TPVArticulo
Referencia As String
Descripcion As String
PVP As Double
Dto As Double
End Type
and use Friend in class, module o frm:
Friend Function GetArticulo() As TPVArticulo
The UDT must be declared in a Public Object, like:
Public Class Sample
Public Strucutre UDT
Dim Value As Object
End Structure
End Class