Can UDT's be used as method parameters in any way? - vba

For years I've been avoiding the use of Public Type UDT's in VBA, because they're hard to pass around and I never really bothered trying to understand why.. until now - it was simply easier to just create a class module and work with actual objects instead.
But recently I gave it a shot, and once I figured they had to be passed ByRef (as an array would), things started to look like I could start using them.
So I defined a Public Type in a standard module, got this compile error:
So I moved the Public Type into a class module, made the class PublicNotCreatable, and then got this compile error:
Here's some code to reproduce the compile error.
Class module "Something":
Option Explicit
' cannot define a public user-defined type within an object module
Public Type TSomething
Foo As Integer
End Type
Public Function Create(ByRef info As TSomething) As Something
End Function
If you move the definition of TSomething to a standard module, you'll get the other compiler error, telling you that the public UDT must be defined in a public object module (i.e. a class module)... which takes you back to square one.
So if you cannot define a Public Type in a class module, why would the compiler throw a fit and even mention "public user defined types defined in public object modules" if such a thing can't legally exist?
Did it work in VB6 and the compiler message is a remnant of that version? Or is the reason somewhere in how COM works? Is it just me or the two error messages are contradicting each other? Or there's something I'm not understanding?
Obviously I'm misusing/abusing UDT's here. So what are they supposed to be used for, if not for passing a "record" to some method?

From standard module it works without any error. Following code threw no error.
Public Type TEST_TYPE
Prop1 As String
End Type
Public Function fTest(ByRef param1 As TEST_TYPE) As String
param1.Prop1 = "Hello from function"
End Function
Public Sub sTest(ByRef param1 As TEST_TYPE)
param1.Prop1 = "Hello from Sub"
End Sub
Public Sub caller()
Dim p As TEST_TYPE
'/Call Sub
Call sTest(p)
MsgBox p.Prop1
'/Call Function
Call fTest(p)
MsgBox p.Prop1
End Sub
One issue with UDT is about Forward referencing. So this will not compile, apart from that It works perfectly fine with standard modules.
Public Type TEST_TYPE
Prop1 As String
Prop2 As TEST_TYPE2 '/ Fails due to Forward referencing. TEST_TYPE2 should be declared before this UDT.
End Type
Public Type TEST_TYPE2
Prop3 As String
End Type
Edit:
However, the work around to use the UDT in class is Friend
VBA Code for Class
'/ Using UDT in VBA-Class
Private Type TEST_TYPE3
Prop3 As String
End Type
Public Sub caller()
Dim p As TEST_TYPE3
p.Prop3 = "Hello from Class"
Call testClassUDT(p)
End Sub
Friend Sub testClassUDT(p As TEST_TYPE3)
MsgBox p.Prop3
End Sub

Here's a Type being passed as a parameter to a class method, and being returned by a class method.
First the class SomeClass (doesn't need to be PublicNotCreatable)
Option Explicit
Sub test(foo As TFooBar)
Dim s As String
s = foo.foo
End Sub
Function ReturnTFoo() As TFooBar
ReturnTFoo.bar = "bar"
ReturnTFoo.foo = " bar"
End Function
And the Module:
Option Explicit
Public Type TFooBar
foo As String
bar As String
End Type
Sub test()
Dim c As SomeClass
Set c = New SomeClass
Dim t1 As TFooBar
Dim t2 As TFooBar
t1.bar = "bar"
t1.foo = "Foo"
c.test t1
t2 = c.ReturnTFoo
End Sub

Related

Compile error: Only user-defined types defined in public object modules can be coerced to or from a variant or passed to late-bound functions

I'm struggling with a little bit of VBa and Excel. I need to create a structure in VBa, which is a Type. The problem I have is, I get an error message when I try to execute the code! I feel I need to explain how I have arrived where I am in case I've made an error.
I have read that to create a type, it needs to be made public. As such I created a new Class (under Class Modules). In Class1, I wrote
Public Type SpiderKeyPair
IsComplete As Boolean
Key As String
End Type
And within ThisWorkbook I have the following
Public Sub Test()
Dim skp As SpiderKeyPair
skp.IsComplete = True
skp.Key = "abc"
End Sub
There is no other code. The issue I have is I get the error message
Cannot define a public user-defined type within an object module
If I make the type private I don't get that error, but of course I can't access any of the type's properties (to use .NET terminology).
If I move the code from Class1 into Module1 it works, but, I need to store this into a collection and this is where it's gone wrong and where I am stuck.
I've updated my Test to
Private m_spiderKeys As Collection
Public Sub Test()
Dim sKey As SpiderKeyPair
sKey.IsComplete = False
sKey.Key = "abc"
m_spiderKeys.Add (sKey) 'FAILS HERE
End Sub
Only user-defined types defined in public object modules can be coerced to or from a variant or passed to late-bound functions
I have looked into this but I don't understand what it is I need to do... How do I add the SpiderKeyPair to my collection?
Had the exact same problem and wasted a lot of time because the error information is misleading. I miss having List<>.
In Visual Basic you can't really treat everything as an object. You have Structures and Classes which have a difference at memory allocation: https://learn.microsoft.com/en-us/dotnet/visual-basic/programming-guide/language-features/data-types/structures-and-classes
A Type is a structure (so are Arrays), so you if you want a "List" of them you better use an Array and all that comes with it.
If you want to use a Collection to store a "List", you need to create a Class for the object to be handled.
Not amazing... but it is what the language has available.
You seem to be missing basics of OOP or mistaking VBA and VB.NET. Or I do not understand what are you trying to do. Anyhow, try the following:
In a module write this:
Option Explicit
Public Sub Test()
Dim skpObj As SpiderKeyPair
Dim m_spiderKeys As New Collection
Dim lngCounter As Long
For lngCounter = 1 To 4
Set skpObj = New SpiderKeyPair
skpObj.Key = "test" & lngCounter
skpObj.IsComplete = CBool(lngCounter Mod 2 = 0)
m_spiderKeys.Add skpObj
Next lngCounter
For Each skpObj In m_spiderKeys
Debug.Print "-----------------"
Debug.Print skpObj.IsComplete
Debug.Print skpObj.Key
Debug.Print "-----------------"
Next skpObj
End Sub
In a class, named SpiderKeyPair write this:
Option Explicit
Private m_bIsComplete As Boolean
Private m_sKey As String
Public Property Get IsComplete() As Boolean
IsComplete = m_bIsComplete
End Property
Public Property Get Key() As String
Key = m_sKey
End Property
Public Property Let Key(ByVal sNewValue As String)
m_sKey = sNewValue
End Property
Public Property Let IsComplete(ByVal bNewValue As Boolean)
m_bIsComplete = bNewValue
End Property
When you run the Test Sub in the module you get this:
Falsch
test1
-----------------
-----------------
Wahr
test2
Pay attention to how you initialize new objects. It happens with the word New. Collections are objects and should be initialized as well with New.

Is scoping broken in VBA?

Say you have this code in a module called Module1:
Option Explicit
Private Type TSomething
Foo As Integer
Bar As Integer
End Type
Public Something As TSomething
In equivalent C# code if you made the Something field public, the code would no longer compile, because of inconsistent accessibility - the type of the field being less accessible than the field itself. Which makes sense.
However in VBA you could have this code in Module2:
Sub DoSomething()
Module1.Something.Bar = 42
Debug.Print Module1.Something.Bar
End Sub
And you get IntelliSense while typing it, and it compiles, and it runs, and it outputs 42.
Why? How does it even work, from a COM standpoint? Is it part of the language specs?
As per my comment, VBA exposes a private Type, just like it exposes a Private Enum.
VBA assumes you can make use of the TypeInfo in the consuming context, but it won't allow you to declare or create instances of those types or enums.
This C++ answer is partly informative:
Access Control is applied to names
The access specifier for the name has nothing to do with it's type
But it's perhaps useful to think of a Private Type in a standard module, as something like a "PublicNotCreatable" class. If you provide a public wrapper, then the type is accessible outside the host module.
But VBA handles things differently when the Type is in a Public Class Module!
Here's your Module1 expanded:
Option Explicit
Private Type TSomething
Foo As Integer
Bar As Integer
End Type
Public Type TOtherThing
Foo As Integer
Bar As Integer
End Type
Public Type TWrapperThing
Something As TSomething
End Type
Public Something As TSomething
Public Otherthing As TOtherThing
Public Wrapperthing As TWrapperThing
Public Function GetSomething() As TSomething
GetSomething.Foo = 1
End Function
Public Function GetOtherthing() As TOtherThing
GetOtherthing.Foo = 1
End Function
And Module2 expanded:
Option Explicit
Sub DoThings()
'Compile Error: User-defined type not defined
'Dim oSomething As TSomething
Dim vSomething As Variant
Dim oOtherthing As Module1.TOtherThing
Dim vOtherthing As Variant
Dim oWrapperthing As Module1.TWrapperThing
Module1.Something.Foo = 42
Module1.Otherthing.Foo = 42
Module1.Wrapperthing.Something.Foo = 42
'Compile Error: Only user-defined types defined in public object modules can be coerced to or from a variant or passed to late-bound functions
'vSomething = Module1.Something
'vOtherthing = Module1.Otherthing
oOtherthing = Module1.Otherthing
oOtherthing.Foo = 43
'Is 43 > 42?
Debug.Assert oOtherthing.Foo > Module1.Otherthing.Foo
'Compile Errors: "GetSomething" User-defined type not defined
'Module1.GetSomething.Foo = 42
'Module1.GetSomething().Foo = 42
Module1.GetOtherthing.Foo = 42
Module1.GetOtherthing().Foo = 42
End Sub

How do I determine if a class member exists?

I have a public subroutine that is called by many classes. However, I now need to do something in that subroutine that only pertains to a small number of the classes that call it. So instead of going back and adding the property to all of the existing classes, I would like to simply check to see see if that class has that property and if so, then do something with it. But I can't seem to figure out how to simply check for the existence of the member without getting an error.
For example:
Public Class_1
Public a1 as string = ""
Public Sub New()
' when a button is clicked call subroutine "check()"
End Sub
End Class
Public Class_2
Public a1 as string = ""
Public a2 as integer = 0
Public Sub New()
' when a button is clicked call subroutine "check()"
End Sub
End Class
Public Class whatever
Public Sub check(sender as object)
If sender.a2 = 0 then
' do something
End if
End Sub
End Class
I have tried such things as
If not(sender.a2 is nothing) then
If isnothing(sender.a2) then
But I can't get past the fact that I get an error simply by using "sender.a2" since a2 is not always a member of the sender.
How can I check to see if a2 is a member of the sending class without using "sender.a2"?
If you want to see that a field exists you need this:
Dim fi As FieldInfo = sender.GetType().GetField("nameOfFieldHere")
If fi IsNot Nothing
'field exists now get the value
Dim o As Object = fi.GetValue(sender)
End If
Take a look at the documentation Type.GetField Method (String)
There are also overloads available too.
You can test that the Object you are referencing is of a certain type before attempting to use it. Once you've determined it's the right type, you can safely cast to it and then use the right properties like so:
If (TypeOf sender Is Class_2) Then
Dim castObj As Class_2 = CType(sender, Class_2)
'We can now access castObj.a2
End If
If there are multiple classes with the property, it would be sensible to create an Interface which states they have the a2 property and have them all implement it. You can then test their type against the new Interface instead of Class_2.
An alternative option is to use class inheritance to implement a default public method for all classes, and override it in your special Class_2 case for your subclasses.

why are overloaded private shared function not accessible when called from shared function in same class

Came across something I found interesting and would love an explanation.
Edit
This question is not meant to be answered with what should be done to fix it. I know the fixes. I want an explanation of why the compiler does what it does. Ex. Are the private functions not considered given this scenario?
Problem
I have a class that has a public shared(static) function called WhatIs. WhatIs takes a parameter that has a collection of objects. the code iterates over this collection and calls a WhatIs function that has a parameter matching type of what the object is.
When executed, an InvalidCastException exception is thrown because the execution is trying to call the WhatIs function that started this, not the one for the type provided.
That's weird, but what made it odd to me was when you change the private shared functions to public shared then it works fine.
Even odder, when you explicit cast the object then it works even if the function is private.
What?! someone please explain
Code
the guts:
Public Class House
Public Property Furniture As ICollection(Of Object)
Public Sub New()
Furniture = New List(Of Object)
End Sub
End Class
Public Class Chair
Public Property IsComfortable As Boolean
End Class
Public Class Table
Public Seats As Integer
End Class
Public Class HouseExaminer
Public Shared Function WhatIs(thing As House) As String
Dim isA As String = "a house that contains "
For Each item In thing.Furniture
isA &= WhatIs(item)
Next
Return isA
End Function
Private Shared Function WhatIs(thing As Chair) As String
Return "a " & If(thing.IsComfortable, "comfortable", "uncomfortable") & " chair "
End Function
Private Shared Function WhatIs(thing As Table) As String
Return "a table that seats " & thing.Seats & " iguanas"
End Function
End Class
to test
Imports System.Text
Imports Microsoft.VisualStudio.TestTools.UnitTesting
Imports stuff
<TestClass()>
Public Class HouseExaminerTests
<TestMethod()>
Public Sub TestWhatIs()
Dim given As New House()
Dim expected As String
Dim actual As String
given.Furniture.Add(New Chair() With {.IsComfortable = True})
given.Furniture.Add(New Table() With {.Seats = 4})
expected = "a house that contains a comfortable chair a table that seats 4 iguanas"
actual = HouseExaminer.WhatIs(given)
Assert.Equals(expected, actual)
End Sub
End Class
result
debug the test and you get this:
InvalidCastException
Method invocation failed because 'Public Shared Function WhatIs(thing As stuff.House) As String' cannot be called with these arguments:
Argument matching parameter 'thing' cannot convert from 'Chair' to 'House'.
These changes make it work but why?!
make em public
change the private shared functions in HouseExaminer to public, rerun test. spoiler, it works
explicitly cast the objects
change them back to private then replace
isA &= WhatIs(item)
with
If TypeOf item Is Chair Then isA &= WhatIs(CType(item, Chair))
If TypeOf item Is Table Then isA &= WhatIs(CType(item, Table))
rerun test, and what do u know, it works
Firstly, it looks like you have implicit conversions turned on. That is the start of the issue. Secondly, you define Furniture as a List(of Object). Your first call to WhatIs is succeeding. The compiler is making a best guess as to which overload to use when passing what it sees as simply Object as it iterates through thing.Furniture, and it determines the public static version of the WhatIs method to be the most appropriate. It then attempts to implicitly convert Object to House, and inevitably fails.
Why does casting work? Because it takes the guess work out of determining which overload to use.
Moral of the story is: Don't make the compiler guess. Implicit conversion can lead to tricky bugs.
Edit: Why doesn't the compiler see the other overloaded functions?
The compiler has to determine the correct overload to use at compile time. It does not wait until runtime to determine which overload to use, and therefore doesn't have the luxury of inspecting the type of the object to determine the most appropriate overload.
Since the compiler only knows that furniture is a List(Of Object), technically (with implicit conversion turned on) all three of the overloads are deemed "appropriate," but the compiler must choose one. It ranks the possible overload candidates, and chooses the public version ahead of the private ones.
Use always
Option Strict On
You cannot make it more flexible by adding Methods equal in name, just with different parametertypes.
Update
Private Function ShowMe(data As Integer) As String
Return data.ToString
End Function
Private Function ShowMe(data As String) As String
Return data
End Function
Private Function ShowMe(data As Double) As String
Return data.ToString
End Function
Dim bla As New List(Of Object)
if you then call
bla.Add(12)
bla.Add("hi")
bla.Add(1.2)
Dim text As String
text = ShowMe(bla(0))
text = ShowMe(bla(1))
text = ShowMe(bla(2))
then the compiler will always complain that the correct method does not exist, because the correct method is not selected by checking the type, instead it is selected by the definition, for which type the container is defined for.
Private Function ShowMe(data As Object) As String
Return data.ToString
End Function
this would be called for all integer, doubles and strings. If it is not available, then some methods are used that can do some kind of automatic conversion. Thats why you can put an integer in a float, or put a number in a string.
One way would be to check for its type and do an explizit type conversion
For Each ele As Object In bla
If TypeOf ele Is Integer Then
text = ShowMe(CInt(ele))
ElseIf TypeOf ele Is Double Then
text = ShowMe(CDbl(ele))
Else
text = ShowMe(CStr(ele))
End If
Next
But this is still not so clean. If you want to access properties that all objects should support, then put them in a container and define the type as something that assures that those properties exist.

Can't use Type in VB

Is there anything wrong with the following code ? It failed on Form_Load() line , and complains about it.
Private Sub Form_Load()
Type Human
Name As String
End Type
Dim stu As Student
With Human:
.Name = "Someone"
End With
Debug.Print ("Name: " & stu.Name)
End Sub
You have two options:
1
Create a new class
Private Class Human
Public Name As String
End Class
(Obviously it would be better to wrap the Name in a public property, but for simplicity, exposing it as a public variable is easier.)
2
Create a new struct:
Structure Human
Dim Name As String
End Structure
Note
It should be noted that both of these options must be done outside of the function, not within Form_Load function
The keyword is no longer Type; it is Structure now. Type was used in VB6 and earlier, but not in .NET.