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

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

Related

ByVal in Excel event-handler

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

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 to declare public variable while it's requires if function to avoid null refrence in vb [duplicate]

I am trying to pass an equipment object to a form object and then use that equipment object in a click event from a button on the form. But I don't know how to properly reference the equipment object within the button event.
I set up the new form instance using:
Public Sub New(ByRef thisEquip As classEquipment)
Me.InitializeComponent()
Me.Text = thisEquip.equipName & " Tests"
End Sub
and set up the button click event like this:
Private Sub btnUpdateAndClose_Click(sender As Object, e As EventArgs) Handles btnUpdateAndClose.Click
Call updateTestList(thisEquip)
End Sub
But the 'thisEquip' object is not recognized. I think this is because the sender is the button and not the form itself. However, I don't know how to reference the equipment object from the form.
The Scope depends on where a variable is declared. You might have missed something skimming the link - each scope level summary includes the phrase in which it is declared.
Now look at your constructor:
Public Sub New(ByRef thisEquip As classEquipment)
thisEquip is declared as an argument to the constructor. Thus, it only exists in that procedure. The fact that the procedure is in a form or that thisEquip is mentioned in the form (or module or anything else) is incidental. While it is true that the constructor is special in several ways, in matters of Scope, it is just another procedure.
Form Level Scope
To save a reference to it for use elsewhere:
Public Class Form1
' declare a variable to hold the reference
Private myEquip As classEquipment
' declare an array
Private myImgs As Image()
Public Sub New(ByRef thisEquip As classEquipment)
InitializeComponent()
...
myEquip = thisEquip ' assign param to the var
' assign array of images to the Form level var
' via a temp array
myImgs = New Image() {My.Resources.add,
My.Resources.ballblack, My.Resources.ballblue,
My.Resources.ballgreen}
End Sub
Declared at the form level, it has form/class level scope. You can now reference myEquip or myImgs anywhere in the form. Do Not Use Dim when merely assigning something to a form level object - it will create a new local, but identically named variable.
Other common scope levels:
Procedure Level Scope
Private myFoo as Int32
Private Sub DoSomething()
Dim myBar As String
myBar = "Ziggy"
...
Dim myFoo As Int32 = 7
End Sub
This is more often called local scope. I am using procedure level because it compares and contrasts better to the other terms.
myBar is declared in the DoSomething method, so it has procedure level scope - it only exists in that method. Trying to use it elsewhere will result in an error. This is similar to the constructor example above with the main difference being that the thisEquip object was passed as a parameter rather than declared locally.
This leads some to get confused: the Dim myFoo in the method declares (creates!) a new, local-only myFoo variable which has no relation to the Form/Class level variable of the same name. The local version shadows out the other. Part of the confusion for this seems to be that some think they need to (re) use Dim before they can use a variable. You do not.
Block Level Scope
Directly from MSDN:
If n < 1291 Then
Dim cube As Integer
cube = n ^ 3
End If
A fair number of VB statements create a block scope (For Each/Next, If/End If and Using/End Using). Variables declared inside a Block, have a scope limited to that block. Basically, (almost) anything which results in indentation creates a Block Scope.
Private Sub .....
Dim cube As Int32
If n < 1291 Then
cube = n ^ 3
End If
Now, cube can be used elsewhere in the procedure: its scope has been changed from Block to Local.
For more details, see MSDN:
- Scope In Visual Basic
- Value Types vs Reference Types

Interface does not behave like an Object?

I have a little problem with an interface. A bunch of my classes implement the ILayoutObject interface. A method declares a variable as ILayoutObject (defaulting it as Nothing) and then runs some code which decides which object it should be. The problem is, the evaluation code runs in a method which receives the variable as a parameter and assigns an object to it. With objects, this would be no problem. The object would be affected by the changes in the method and everything would be OK. Howeverm, when using an interface, the variable in the calling code remains Nothing and behaves like a normal variable. Does anyone have any ideas on how to circumvent that? Alas, due to code structure I am unable to use ByRef or functions :(
Here is some code:
Protected LayoutHandler As Dictionary(Of String, Action(Of Constants.OptionsEntryStructure, ILayoutElements)) = New Dictionary(Of String, Action(Of Constants.OptionsEntryStructure, ILayoutElements)) From
{
{Constants.KeyLayoutType, AddressOf KeyLayoutType}
}
Sub MakeLayOuts
Dim LayoutElement As ILayoutElements = Nothing
Dim Value = "SomeValues"
Dim key = "Key"
LayoutHandler(key)(Value, LayoutElement)
' LayoutElement remains nothing.....
End Sub
Protected Sub KeyLayoutType(elem As Constants.OptionsEntryStructure, Layout As ILayoutElements)
Layout = New LayoutObject 'which would implement the interface
End Sub
You need to declare the parameter as ByRef if you want to alter the object to which the variable in the calling code points to:
Protected Sub KeyLayoutType(elem As Constants.OptionsEntryStructure, ByRef Layout As ILayoutElements)
Layout = New LayoutObject 'which would implement the interface
End Sub
This is true with any reference type (classes). The fact that they are referenced with an interface makes no difference.
If you can't use ByRef, and you can't use a function to return the new object, then your only other real option would be to request a type of object which has the layout object as a property. For instance:
Public Interface ILayoutElementContainer
Public Property LayoutElement As ILayoutElements
End Interface
Protected Sub KeyLayoutType(elem As Constants.OptionsEntryStructure, Container As ILayoutElementContainer)
Container.LayoutElement = New LayoutObject 'which would implement the interface
End Sub

.net dynamic loading

I've seen some other responses about this and they talk about interfaces but I'm pretty sure you can do this with classes and base classes but I can't this to work.
Public Class Behavior
Private _name As String
Public ReadOnly Property Name As String
Get
Return _name
End Get
End Property
Public Property EditorUpdate As Boolean
Public Sub New(ByVal name As String)
_name = name
EditorUpdate = False
End Sub
Public Overridable Sub Update()
End Sub
' runs right away in editor mode. also runs when in stand alone game mode right away
Public Overridable Sub Start()
End Sub
' runs after game mode is done and right before back in editor mode
Public Overridable Sub Finish()
End Sub
' runs right when put into game mode
Public Overridable Sub Initialize()
End Sub
' runs when the game is complete in stand alone mode to clean up
Public Overridable Sub Destroy()
End Sub
End Class
Public Class CharacterController
Inherits Behavior.Behavior
Public Sub New()
MyBase.New("Character Controller")
End Sub
Public Overrides Sub Update()
' TODO: call UpdateController()
' THINK: how can UpdateController() get the controller entity it's attached to?
' Behaviors need a way to get the entity they are attached to. Have that set when it's assigned in the ctor?
End Sub
End Class
Dim plugins() As String
Dim asm As Assembly
plugins = Directory.GetFileSystemEntries(Path.Combine(Application.StartupPath, "Plugins"), "*.dll")
For i As Integer = 0 To plugins.Length - 1
asm = Assembly.LoadFrom(plugins(i))
For Each t As Type In asm.GetTypes
If t.IsPublic Then
If t.BaseType.Name = "Behavior" Then
behaviorTypes.Add(t.Name, t)
Dim b As Behavior.Behavior
b = CType(Activator.CreateInstance(t), Behavior.Behavior)
'Dim o As Object = Activator.CreateInstance(t)
End If
End If
Next
Next
When it tries to convert whatever Activator.CreateInstance(t) returns to the base class of type Behavior I'm getting invalid cast exception. That type should be of CharacterController which is defined as a child of Behavior so why wouldn't it let me cast that? I've done something like this before but I can't find my code. What am I missing?
This may not be an answer to your question (it also might resolve your exception -- who knows), but it is something that needs to be pointed out. These lines:
If t.IsPublic Then
If t.BaseType.Name = "Behavior" Then
Should really be changed to one conditional like this one:
If t.IsPublic AndAlso (Not t.IsAbstract) AndAlso _
GetType(Behavior.Behavior).IsAssignableFrom(t) Then
Otherwise, if somebody defines a random type called "Behavior" in their own assembly and derives it from another type, your code will think it is a plugin. Additionally, if someone derives your Behavior type and then derives that type (two levels of inheritance) this code will incorrectly skip over that type. Using the IsAssignableFrom method is a quick and easy way to ensure that one type does actually derive from the specific type you want (instead of any type that shares the same name), even if there is another type in between your types in the inheritance tree. The additional check against t.IsAbstract will also ensure that you don't try to instantiate an abstract subtype of your base plugin type.
This works for me:
Dim ctor As Reflection.ConstructorInfo = _
t.GetConstructor(New System.Type() {})
Dim o As Object = ctor.Invoke(New Object() {})
Dim plugin As Plugin = TryCast(o, Plugin)
(If I find t, I invoke the parameterless constructor.)
[I just realized this is probably what Activator.CreateInstance does, so I replaced my code with yours and it worked your way -- so this probably won't help you]