I have created a simple VBA class with a parameterized constructor. The class has the VB_PredeclaredID=True. This development is being done on a Mac in Office 365. The code is below. (The code is not bulletproof. I created this simple example to show the problem that showed up in a more complex class.) When the 5th line of the Make procedure is executed, the Class_Terminate handler is invoked for the object created in the 2nd line, i.e., the one controlling the "with" block. Class_Terminate crashes on exit with an overflow error. (On my more complex example, the error is "with without end.") I've planted debug so I know the Birthday property is never called in line 5. Can someone explain to me what in my code is causing the system to want to destroy the object reference when it is still in use, and how I can work around it? Thanks.
Sub TestClass()
Dim cl As CTest
Set cl = CTest.Make(DateValue("12/6/1946"))
Debug.Print "TestClass", IIf(Not cl Is Nothing, cl.Birthday, "Nothing")
End Sub
Private m_birthday As Date
Private m_otherdata As Variant
Private Sub Class_Initialize()
Debug.Print "Enter Initialize"
If Me Is CTest Then
m_birthday = DateValue("1/1/1800")
Else
m_birthday = Now()
End If
Debug.Print "Exit Initialize", m_birthday
End Sub
Private Sub Class_Terminate()
End Sub
Public Function Make(varparam As Variant) As CTest
If Me Is CTest Then
With New CTest
Select Case VarType(varparam)
Case vbDate:
.Birthday = varparam
Case vbObject:
.Birthday = varparam.Birthday
End Select
Set Make = .Self
End With
ElseIf varparam Is Nothing Then
With New CTest
.Birthday = Me.Birthday
If (VarType(Me.OtherData)) = vbObject Then
Set .OtherData = Me.OtherData
Else
.OtherData = Me.OtherData
End If
Set Make = .Self
End With
Else
Set Make = Nothing
End If
End Function
Public Property Get Self() As CTest
Set Self = Me
End Property
Public Property Get Birthday() As Date
Birthday = m_birthday
End Property
Public Property Let Birthday(val As Date)
m_birthday = val
End Property
Public Property Get OtherData() As Variant
OtherData = m_otherdata
End Property
Public Property Let OtherData(val As Variant)
m_otherdata = val
End Property
Public Property Set OtherData(val As Variant)
Set m_otherdata = val
End Property
I created this simple example to show the problem that showed up in a more complex class
What's missing is code that consumes the class, and the code that actually reproduces the problem, but I wrote a lot of articles on this subject, so let's dig anyway.
Private Sub Class_Initialize()
Debug.Print "Enter Initialize"
If Me Is CTest Then
m_birthday = DateValue("1/1/1800")
Else
m_birthday = Now()
End If
Debug.Print "Exit Initialize", m_birthday
End Sub
A useful piece of information that you're not outputting, is whether the initializing instance is the default instance. Consider:
Debug.Print "Initializing " & TypeName(Me) & IIf(Me Is CTest, " (default instance)", vbNullString)
One problem is this:
If Me Is CTest Then
m_birthday = DateValue("1/1/1800") '<~
Else
m_birthday = Now()
End If
If the current instance is the class' default instance, the internal state is useless. Keeping the default instance stateless is key, in fact: m_birthday is an implementation detail as far as the class' default interface (CTest) is concerned. This would be a better guard clause:
If Me Is CTest Then Exit Sub
m_birthday = Now()
No more nesting, m_birthday is only assigned on a non-default instance, and the intent of keeping the default instance stateless is much more explicitly expressed.
Now, if you type this in the immediate pane:
Set a = New CTest
You'll get this output:
Initializing CTest (default instance)
Initializing CTest
You're missing this trace:
Private Sub Class_Terminate()
Debug.Print "Terminating " & TypeName(Me) & IIf(Me Is CTest, " (default instance)", vbNullString)
End Sub
In the Make factory method, you actually want an even stronger bail-out:
Public Function Make(varparam As Variant) As CTest
If Me Is CTest Then
'...
Consider:
Public Function Make(varparam As Variant) As CTest
If Not Me Is CTest Then Err.Raise 5, TypeName(Me), "Member call is only valid from default/predeclared instance."
And that removes a branch in the conditional path. It also makes me wonder about this:
ElseIf varparam Is Nothing Then
That condition gets evaluated when Me Is CTest is False, i.e. when the factory method is invoked from a user instance... and that should not be allowed to happen.
This is another problem:
Select Case VarType(varparam)
Case vbDate:
.Birthday = varparam
Case vbObject:
.Birthday = varparam.Birthday
vbObject means varparam is an Object reference - not that it's a CTest object: because we're working with a Variant, the member call is late-bound, so if the object doesn't have a Birthday member, we have run-time error 438 raised here. We can keep the member call late-bound but still validate the type:
Case vbObject:
If TypeOf varparam Is CTest Then .Birthday = varparam.Birthday
Or you can get compile-time validation by introducing a variable:
Case vbObject:
Dim typedParam As CTest
If TypeOf varparam Is CTest Then
Set typedParam = varparam
.Birthday = typedParam.Birthday '<~ early-bound member call now
End If
This not only helps the compiler pick up typos (even Option Explicit can't save you from a typo in a late-bound call), it also helps static code analysis tooling like Rubberduck, that now "see" the member call: if the member is renamed, refactoring tools can now update this call site - that's not easily possible with late-bound code.
Public Property Get Self() As CTest
Set Self = Me
End Property
That's syntax sugar that works nicely when there's an explicit interface involved, to cleanly separate the stateless CTest default instance from the ICTest explicit client interface (which could include a Property Get for the birthday, but no Let accessor).
Better syntax sugar that doesn't affect your classes' public interfaces and dramatically cleans up the locals toolwindow in class modules, is shoving the instance state into a Private Type:
Private Type TState
Birthday As Date
OtherData As Variant '<~ note: this breaks strong-typing and gets you back into late-bound land.
End Type
Private this As TState
This Private this instance (module-level) variable replaces all m_-prefixed variables, and now the Birthday property reads like this:
Public Property Get Birthday() As Date
Birthday = this.Birthday
End Property
Public Property Let Birthday(ByVal val As Date)
this.Birthday = val
End Property
...
So, the only convoluted piece of code that looks suspect, is the Make function, which is responsible for too many things.
Write a separate private function that works off a Date, another that works off a CTest object, and conditionally invoke the appropriate one from Make.
With functions that do fewer things, fewer things can go wrong.
Guard your methods - if a method involves instance state, prohibit invoking it from the default/predeclared instance. If a method is supposed to be invoked from the default instance, prohibit calling it from other instances.
See this article for a refresher on the pattern, and this one to see it in action with real code.
I have to doff my cap to #MathieuGuindon and the other chaps and chapesses at Rubberduck as my understanding of VBA has progressed immensely through reading the Rubberduck blogs.
I too have been through some interesting times using the PredeclaredId and therfore offer some of my thoughts on how the OP code should be constructed. As I am still developing my understanding of OOP in VBA folks should feel free to shoot me down in flames if I am wrong or misunderstanding things.
There are two things that I have developed from ideas presented in the rubberduck blogs.
This
I differentiate 'this' into p,s,b and u representing Type definitions of Properties, State,BaseInstance and Using.
Self
I take the construction of a Class instance a step further and pass the Make parameters to the Self method call. In this way the parameters can be used to set up private members of the new instance without the need for public properties.
Option Explicit
Sub TestCTest()
Dim myCTest As CTest
' no errors
Set myCTest = CTest.Make(DateValue("4/6/2020"))
Debug.Print myCTest.Birthday
On Error Resume Next
' Gives "CTest: Expecting Variant/Date or Variant/CTest: Found String"
Set myCTest = CTest.Make("4/6/2020")
Debug.Print Err.Description
On Error GoTo 0
On Error Resume Next
Dim myCtest2 As CTest
'Gives "CTest: Make should only be used with the PredeclaredId"
Set myCtest2 = myCTest.Make(DateValue("4/6/2020"))
Debug.Print Err.Description
On Error GoTo 0
On Error Resume Next
' Gives "New is not permitted outside of the Make Method" error
Dim myCtest3 As CTest
Set myCtest3 = New CTest
Debug.Print Err.Description
On Error GoTo 0
End Sub
Class CTest
Option Explicit
'#PredeclaredId
' Variables used as the private repositories for public properties are located here
Private Type Properties
Birthday As Date
OtherData As Variant ' OP may have a specific type in mind
' NewIsAllowed appears in every instance but we will only ever use
' the value in the predeclared Id to toggle if new is or is not allowed
' via the AllowNew property
NewIsAllowed As Boolean
End Type
Private p As Properties
' If any were present the State type would be used for variables representing
' the state of the instance but which are not intended to be made public through Properties
' Private Type State
' StateVar1 as Typename
' End Type
'
' Private s As State
'
' Used only for PredeclaredId to allow boilerplate code to be written
Private Type BaseInstance
PredeclaredId As CTest
End Type
Private b As BaseInstance
Private Sub Class_Initialize()
' This method runs the **first** time the **PredecalredID** is used in an expression
' and for every subsequent use of New. Therefore managing what happens for the PredeclaredId
' vs instances can become a bit Eulerish.
' Declaring b.predeclaredId allows us to boilerplate code elsewhere
' as it means that the only places that the actual class name is used
' is here ,the Type declaration above and other method declarations.
Set b.PredeclaredId = CTest
' The code to exit on the first use of the PredeclaredID in an expression
If Me Is b.PredeclaredId Then Exit Sub
' Trap the use of New when not used by the Make Function
' the code below means that bad code will be detected at testing time
If Not AllowNew Then
Err.Raise 445 + vbObjectError, TypeName(Me), TypeName(Me) & ": New is not permitted outside of the Make method"
End If
End Sub
Public Function Make(ByVal varparam As Variant) As CTest
' From the OP code we are expecting varparam to be either
' a Date , a CTest object or nothing
If InStr("Date,CTest,Empty,Null,Nothing", TypeName(varparam)) = 0 Then
Err.Raise 13 + vbObjectError, TypeName(Me), TypeName(Me) & ": Expecting Variant/Date or Variant/CTest: Found " & TypeName(varparam)
End If
' In the OP code it is not clear if the OP has
' restricted the use of the Make function to CTest.Make or
' allows the use of <instance>.Make.
' Both uses are legal as Make is a public method but
' in the spirit of declaring a PredecalredId it is
' preferable to restrict the use of Make to CTest.Make
' Thus the code below detects the use of Make by an instance.
If Not Me Is b.PredeclaredId Then
Err.Raise 445 + vbObjectError, TypeName(Me), TypeName(Me) & ": Make should only be used with the PredeclaredId"
End If
' Instruct the PredeclaredId that New is allowed
AllowNew = True
With New CTest
Set Make = .Self(varparam)
End With
' Instruct the PredeclaredId to disallow the use of new
AllowNew = False
End Function
Public Function Self(ByVal varparam As Variant) As CTest
' This code is inside the new instance that is being constructed.
' Therefore there is free access to the private variables of the
' instance 'under construction'
' Its a little difficult to untangle the OP logic for what constitutes
' the birthday so the Case statement below may well be incorrect
Select Case TypeName(varparam)
Case "Empty", "Null", "Nothing"
' The Me in the OP code occurs in the Make function and
' consequently refers to the instance of which Make was called.
' IF make was used as discussed above this implies that Me is b.PredecalredId
' only if the OP has adhered to CTest.Make
' If this is the case????
p.Birthday = DateValue("1/1/1800")
' The OP assigns otherdata in the case of nothing
' using Me.Otherdata. The Me will now refer to the
' the instance under construction so it is likely that a second
' parameter will be required for the Make function
'
Case "Date"
p.Birthday = CDate(varparam)
Case "CTest"
Dim myCTest As CTest
Set myCTest = varparam
p.Birthday = myCTest.Birthday
Case Else
Err.Raise 13 + vbObjectError, TypeName(Me), TypeName(Me) & ": Expecting Variant/Date or Variant/CTest: Found " & TypeName(varparam)
End Select
Set Self = Me
End Function
' The alternative to the AllowNew property is to have a public AllowNew field.
' but as the code below is bolerplate and can be copied to new classes without issue
' I'm happy to use the code below.
' Due to the differentiation of p,s,b
' we have an easily identifiable warning to check if we
' see anything but the p. prefix in Property declarations.
Public Property Get AllowNew() As Boolean
If Me Is b.PredeclaredId Then
AllowNew = p.NewIsAllowed
Else
AllowNew = b.PredeclaredId.AllowNew
End If
End Property
Public Property Let AllowNew(ByVal Value As Boolean)
If Me Is b.PredeclaredId Then
p.NewIsAllowed = Value
Else
b.PredeclaredId.AllowNew = Value
End If
End Property
Public Property Get Birthday() As Date
Birthday = p.Birthday
End Property
Public Property Let Birthday(ByVal val As Date)
p.Birthday = val
End Property
Public Property Get OtherData() As Variant
OtherData = p.OtherData
End Property
Public Property Let OtherData(ByVal val As Variant)
p.OtherData = val
End Property
Public Property Set OtherData(ByVal val As Variant)
Set p.OtherData = val
End Property
Related
Although an experienced VBA programmer it is the first time that I make my own classes (objects). I am surprised to see that all properties are 'duplicated' in the Locals Window. A small example (break at 'End Sub'):
' Class module:
Private pName As String
Public Property Let Name(inValue As String)
pName = inValue
End Property
Public Property Get Name() As String
Name = pName
End Property
' Normal module:
Sub Test()
Dim objTest As cTest
Set objTest = New cTest
objTest.Name = "John Doe"
End Sub
Why are both Name and pName shown in the Locals Window? Can I in some way get rid of pName?
As comments & answers already said, that's just the VBE being helpful.
However if you find it noisy to have the private fields and public members listed in the locals toolwindow, there's a way to nicely clean it up - here I put the Test procedure inside ThisWorkbook, and left the class named Class1:
So what's going on here? What's this?
Here's Class1:
Option Explicit
Private Type TClass1
Name As String
'...other members...
End Type
Private this As TClass1
Public Property Get Name() As String
Name = this.Name
End Property
Public Property Let Name(ByVal value As String)
this.Name = value
End Property
The class only has 1 private field, a user-defined type value named this, which holds all the encapsulated data members.
As a result, the properties' underlying fields are effectively hidden, or rather, they're all regrouped under this, so you won't see the underlying field values unless you want to see them:
And as an additional benefit, you don't need any pseudo-Hungarian prefixes anymore, the properties' implementations are crystal-clear, and best of all the properties have the exact same identifier name as their backing field.
All the Inspection windows not only show the public interface of the objects to you, but also their private members. AFAIK there is nothing you can do about it.
Consider it a nice feature to get even more insights while debugging.
In my experience this is less of an issue in real world objects as they tend to have more fields and properties. Assuming a consistent naming (as your example shows), fields and properties are nicely grouped together.
If you really dont want to see even Mathieu's This you could wrap it into a function. This is a bit more involved, and can be achieved using
a second class that stores the data in public variables. This will be marginally slower then Mattieu's implementation
a collection object that accesses the data using keys. This does not require additional clutter in the project exporer's 'class module' list but will be a little slower if you call the This repeatedly in fast sucession
An example for each is given below. If you break in the Class's Initialisation function, you can add me to the watch window and only the Name property will be listed
Using 2 Objects example
insert a classmodule and name it: InvisibleObjData
Option Explicit
Public Name As String
Public plop
Private Sub Class_Initialize()
Name = "new"
plop = 0
End Sub
insert a classmodule and name it: InvisibleObj
Option Explicit
Private Function This() As InvisibleObjData
Static p As New InvisibleObjData 'static ensures the data object persists at successive calls
Set This = p
End Function
Private Sub Class_Initialize()
This.Name = "invisible man": Debug.Print Name
Me.Name = "test": Debug.Print Name
This.plop = 111: Debug.Print This.plop
End Sub
Property Let Name(aname As String): This.Name = aname: End Property
Property Get Name() As String: Name = This.Name: End Property
'_______________________________________________________________________________________
' in the immediate window type
'
' set x=new invisibleObj
If you dont like splitting the class over two objects, a similar behaviour can be generated using a 'wrapped' collection object:
insert a classmodule and name it: InvisibleCol
Option Explicit
Private Function This() As Collection
Static p As New Collection
'static ensures the collection object persists at successive calls
'different instances will have different collections
'note a better dictionary object may help
Set This = p
End Function
Private Function add2this(s, v)
'a better dictionary object instead of the collection would help...
On Error Resume Next
This.Remove s
This.Add v, s
End Function
Private Sub Class_Initialize()
add2this "name", "invisible man": Debug.Print Name
Me.Name = "test": Debug.Print Name
add2this "plop", 111
Debug.Print This("plop") ' use the key to access your data
Debug.Print This!plop * 2 ' use of the BANG operator to reduce the number of dbl quotes
' Note: This!plop is the same as This("plop")
End Sub
Property Let Name(aname As String): add2this "name", aname: End Property
Property Get Name() As String: Name = This!Name: End Property
'_______________________________________________________________________________________
' in the immediate window type
'
' set x=new invisibleCol
We all know there can only ever be one error object in VBA.
While helping a co-worker with error handling and why he shouldn't use On Error Resume Next I had an idea:
Store the error object somewhere to later reference back to it.
Consider this piece of test code:
Sub Test()
Dim t As ErrObject
On Error Resume Next
Err.Raise 1
Set t = Err
On Error GoTo 0
Debug.Print t.Number
On Error Resume Next
Err.Raise 1
Debug.Print t.Number
End Sub
It will print 0 to the immediate window because On Error GoTo 0 resets the error object and then prints 1 since it still holds a reference to the only error object (?).
If we create a new class and give it some properties pertaining to the ErrObject like so:
(TestClass)
Option Explicit
Public oError As ErrObject
Private Sub Class_Initialize(): End Sub
Private Sub Class_Terminate()
If Not oError Is Nothing Then Set oError = Nothing
End Sub
Public Property Get Error()
Error = oError
End Property
Public Property Set Error(ByVal ErrorObject As ErrObject)
Set oError = ErrorObject
End Property
And create our instance like this:
Sub Test2()
Dim t As TestClass
On Error Resume Next
Set t = New TestClass
Err.Raise 1
Set t.Error = Err
On Error GoTo 0
Debug.Print t.oError.Number
On Error Resume Next
Err.Raise 1
Debug.Print t.oError.Number
End Sub
We still get 0 and 1 as output respectively.
This bringst me to my question: What is the use of declaring a variable as ErrObject when we cannot create a new object itself but it simply becomes another pointer to the only error object in VBA?
None whatsoever.
Err is often treated as some kind of global ErrObject instance, but the truth is, it's a function that returns one - as revealed in the object browser:
And that function is implemented in such a way, that you always get the same object.
Objects need to expose an interface to be usable, and so the object returned by the Err function exposes that of the ErrObject class - it doesn't mean the ErrObject class exists so that it can be instantiated or encapsulated by user code: it merely provides an interface to access the properties of the current run-time error state.
When you encapsulate an ErrObject like you did, you're essentially just giving yourself another way (besides the Err function) to access the ErrObject instance - but it's still the exact same object holding the properties of the current run-time error state.
And when an object's properties change, your encapsulated copy that points to that object is going to start reporting the new values, and the old ones you meant to "remember" are overwritten.
Note that this is true for any object, not just ErrObject.
Say I have a class that does what you're doing with the ErrObject reference, but with a Collection:
Private coll As Collection
Public Property Set InternalCollection(ByVal c As Collection)
Set coll = c
End Property
Public Property Get InternalCollection() As Collection
Set InternalCollection = coll
End Property
If I make an instance of that class (let's call it Class1) and assign c to its InternalCollection, and then add items to c...
Dim c As Collection
Set c = New Collection
With New Class1
Set .InternalCollection = c
c.Add 42
.InternalCollection.Add 42
Debug.Print .InternalCollection.Count
End With
The output is 2, because c and InternalCollection (/the encapsuated coll reference) are the very same object, and that's what's happening with your encapsulated ErrObject.
The solution is to not encapsulate the ErrObject itself, but rather pull its values into backing fields for get-only properties that encapsulate the state of the ErrObject:
Private errNumber As Long
Private errDescription As String
'...
Public Sub SetErrorInfo() 'note: an ErrObject argument would be redundant!
With Err
errNumber = .Number
errDescription = .Description
'...
End With
End Sub
Public Property Get Number() As Long
Number = errNumber
End Property
Public Property Get Description() As String
Description = errDescription
End Property
'...
Now, whether that's useful is up for debate - IMO if the state is consumed at a moment where the global error state already contains the same information, there's no need to do this.
The class could pretty easily be [ab]used as a return type for a Function that returns Nothing to indicate success, and the encapsulated error state in case of failure - the problem is that the language is designed around raising errors rather than returning them; it's too easy to "fire-and-forget" such a function without verifying its return value, and since at the call site the actual runtime error state isn't going to trip an On Error statement, carrying error state as program data isn't idiomatic, it makes a "surprising" API that can easily result in code that ends up ignoring all errors.
Idiomatic error handling deals with the global runtime error state as soon as possible, and either recovers in the same scope, or lets the error state bubble up the call stack to where it can be handled. And until the error is handled, the ErrObject state is accessible through the global Err function.
I have a feeling the answer to this is going to be "not possible", but I'll give it a shot...
I am in the unenviable position of modifying a legacy VB6 app with some enhancements. Converting to a smarter language isn't an option.
The app relies on a large collection of user defined types to move data around. I would like to define a common function that can take a reference to any of these types and extract the data contained.
In pseudo code, here's what I'm looking for:
Public Sub PrintUDT ( vData As Variant )
for each vDataMember in vData
print vDataMember.Name & ": " & vDataMember.value
next vDataMember
End Sub
It seems like this info needs to be available to COM somewhere... Any VB6 gurus out there care to take a shot?
Thanks,
Dan
Contrary to what others have said, it IS possible to get run-time type information for UDT's in VB6 (although it is not a built-in language feature). Microsoft's TypeLib Information Object Library (tlbinf32.dll) allows you to programmatically inspect COM type information at run-time. You should already have this component if you have Visual Studio installed: to add it to an existing VB6 project, go to Project->References and check the entry labeled "TypeLib Information." Note that you will have to distribute and register tlbinf32.dll in your application's setup program.
You can inspect UDT instances using the TypeLib Information component at run-time, as long as your UDT's are declared Public and are defined within a Public class. This is necessary in order to make VB6 generate COM-compatible type information for your UDT's (which can then be enumerated with various classes in the TypeLib Information component). The easiest way to meet this requirement would be to put all your UDT's into a public UserTypes class that will be compiled into an ActiveX DLL or ActiveX EXE.
Summary of a working example
This example contains three parts:
Part 1: Creating an ActiveX DLL project that will contain all the public UDT declarations
Part 2: Creating an example PrintUDT method to demonstrate how you can enumerate the fields of a UDT instance
Part 3: Creating a custom iterator class that allows you easily iterate through the fields of any public UDT and get field names and values.
The working example
Part 1: The ActiveX DLL
As I already mentioned, you need to make your UDT's public-accessible in order to enumerate them using the TypeLib Information component. The only way to accomplish this is to put your UDT's into a public class inside an ActiveX DLL or ActiveX EXE project. Other projects in your application that need to access your UDT's will then reference this new component.
To follow along with this example, start by creating a new ActiveX DLL project and name it UDTLibrary.
Next, rename the Class1 class module (this is added by default by the IDE) to UserTypes and add two user-defined types to the class, Person and Animal:
' UserTypes.cls '
Option Explicit
Public Type Person
FirstName As String
LastName As String
BirthDate As Date
End Type
Public Type Animal
Genus As String
Species As String
NumberOfLegs As Long
End Type
Listing 1: UserTypes.cls acts as a container for our UDT's
Next, change the Instancing property for the UserTypes class to "2-PublicNotCreatable". There is no reason for anyone to instantiate the UserTypes class directly, because it's simply acting as a public container for our UDT's.
Finally, make sure the Project Startup Object (under Project->Properties) is set to to "(None)" and compile the project. You should now have a new file called UDTLibrary.dll.
Part 2: Enumerating UDT Type Information
Now it's time to demonstrate how we can use TypeLib Object Library to implement a PrintUDT method.
First, start by creating a new Standard EXE project and call it whatever you like. Add a reference to the file UDTLibrary.dll that was created in Part 1. Since I just want to demonstrate how this works, we will use the Immediate window to test the code we will write.
Create a new Module, name it UDTUtils and add the following code to it:
'UDTUtils.bas'
Option Explicit
Public Sub PrintUDT(ByVal someUDT As Variant)
' Make sure we have a UDT and not something else... '
If VarType(someUDT) <> vbUserDefinedType Then
Err.Raise 5, , "Parameter passed to PrintUDT is not an instance of a user-defined type."
End If
' Get the type information for the UDT '
' (in COM parlance, a VB6 UDT is also known as VT_RECORD, Record, or struct...) '
Dim ri As RecordInfo
Set ri = TLI.TypeInfoFromRecordVariant(someUDT)
'If something went wrong, ri will be Nothing'
If ri Is Nothing Then
Err.Raise 5, , "Error retrieving RecordInfo for type '" & TypeName(someUDT) & "'"
Else
' Iterate through each field (member) of the UDT '
' and print the out the field name and value '
Dim member As MemberInfo
For Each member In ri.Members
'TLI.RecordField allows us to get/set UDT fields: '
' '
' * to get a fied: myVar = TLI.RecordField(someUDT, fieldName) '
' * to set a field TLI.RecordField(someUDT, fieldName) = newValue '
' '
Dim memberVal As Variant
memberVal = TLI.RecordField(someUDT, member.Name)
Debug.Print member.Name & " : " & memberVal
Next
End If
End Sub
Public Sub TestPrintUDT()
'Create a person instance and print it out...'
Dim p As Person
p.FirstName = "John"
p.LastName = "Doe"
p.BirthDate = #1/1/1950#
PrintUDT p
'Create an animal instance and print it out...'
Dim a As Animal
a.Genus = "Canus"
a.Species = "Familiaris"
a.NumberOfLegs = 4
PrintUDT a
End Sub
Listing 2: An example PrintUDT method and a simple test method
Part 3: Making it Object-Oriented
The above examples provide a "quick and dirty" demonstration of how to use the TypeLib Information Object Library to enumerate the fields of a UDT. In a real-world scenario, I would probably create a UDTMemberIterator class that would allow you to more easily iterate through the fields of UDT, along with a utility function in a module that creates a UDTMemberIterator for a given UDT instance. This would allow you to do something like the following in your code, which is much closer to the pseudo-code you posted in your question:
Dim member As UDTMember 'UDTMember wraps a TLI.MemberInfo instance'
For Each member In UDTMemberIteratorFor(someUDT)
Debug.Print member.Name & " : " & member.Value
Next
It's actually not too hard to do this, and we can re-use most of the code from the PrintUDT routine created in Part 2.
First, create a new ActiveX project and name it UDTTypeInformation or something similar.
Next, make sure that the Startup Object for the new project is set to "(None)".
The first thing to do is to create a simple wrapper class that will hide the details of the TLI.MemberInfo class from calling code and make it easy to get a UDT's field's name and value. I called this class UDTMember. The Instancing property for this class should be PublicNotCreatable.
'UDTMember.cls'
Option Explicit
Private m_value As Variant
Private m_name As String
Public Property Get Value() As Variant
Value = m_value
End Property
'Declared Friend because calling code should not be able to modify the value'
Friend Property Let Value(rhs As Variant)
m_value = rhs
End Property
Public Property Get Name() As String
Name = m_name
End Property
'Declared Friend because calling code should not be able to modify the value'
Friend Property Let Name(ByVal rhs As String)
m_name = rhs
End Property
Listing 3: The UDTMember wrapper class
Now we need to create an iterator class, UDTMemberIterator, that will allow us to use VB's For Each...In syntax to iterate the fields of a UDT instance. The Instancing property for this class should be set to PublicNotCreatable (we will define a utility method later that will create instances on behalf of calling code).
EDIT: (2/15/09) I've cleaned the code up a bit more.
'UDTMemberIterator.cls'
Option Explicit
Private m_members As Collection ' Collection of UDTMember objects '
' Meant to be called only by Utils.UDTMemberIteratorFor '
' '
' Sets up the iterator by reading the type info for '
' the passed-in UDT instance and wrapping the fields in '
' UDTMember objects '
Friend Sub Initialize(ByVal someUDT As Variant)
Set m_members = GetWrappedMembersForUDT(someUDT)
End Sub
Public Function Count() As Long
Count = m_members.Count
End Function
' This is the default method for this class [See Tools->Procedure Attributes] '
' '
Public Function Item(Index As Variant) As UDTMember
Set Item = GetWrappedUDTMember(m_members.Item(Index))
End Function
' This function returns the enumerator for this '
' collection in order to support For...Each syntax. '
' Its procedure ID is (-4) and marked "Hidden" [See Tools->Procedure Attributes] '
' '
Public Function NewEnum() As stdole.IUnknown
Set NewEnum = m_members.[_NewEnum]
End Function
' Returns a collection of UDTMember objects, where each element '
' holds the name and current value of one field from the passed-in UDT '
' '
Private Function GetWrappedMembersForUDT(ByVal someUDT As Variant) As Collection
Dim collWrappedMembers As New Collection
Dim ri As RecordInfo
Dim member As MemberInfo
Dim memberVal As Variant
Dim wrappedMember As UDTMember
' Try to get type information for the UDT... '
If VarType(someUDT) <> vbUserDefinedType Then
Fail "Parameter passed to GetWrappedMembersForUDT is not an instance of a user-defined type."
End If
Set ri = tli.TypeInfoFromRecordVariant(someUDT)
If ri Is Nothing Then
Fail "Error retrieving RecordInfo for type '" & TypeName(someUDT) & "'"
End If
' Wrap each UDT member in a UDTMember object... '
For Each member In ri.Members
Set wrappedMember = CreateWrappedUDTMember(someUDT, member)
collWrappedMembers.Add wrappedMember, member.Name
Next
Set GetWrappedMembersForUDT = collWrappedMembers
End Function
' Creates a UDTMember instance from a UDT instance and a MemberInfo object '
' '
Private Function CreateWrappedUDTMember(ByVal someUDT As Variant, ByVal member As MemberInfo) As UDTMember
Dim wrappedMember As UDTMember
Set wrappedMember = New UDTMember
With wrappedMember
.Name = member.Name
.Value = tli.RecordField(someUDT, member.Name)
End With
Set CreateWrappedUDTMember = wrappedMember
End Function
' Just a convenience method
'
Private Function Fail(ByVal message As String)
Err.Raise 5, TypeName(Me), message
End Function
Listing 4: The UDTMemberIterator class.
Note that in order to make this class iterable so that For Each can be used with it, you will have to set certain Procedure Attributes on the Item and _NewEnum methods (as noted in the code comments). You can change the Procedure Attributes from the Tools Menu (Tools->Procedure Attributes).
Finally, we need a utility function (UDTMemberIteratorFor in the very first code example in this section) that will create a UDTMemberIterator for a UDT instance, which we can then iterate with For Each. Create a new module called Utils and add the following code:
'Utils.bas'
Option Explicit
' Returns a UDTMemberIterator for the given UDT '
' '
' Example Usage: '
' '
' Dim member As UDTMember '
' '
' For Each member In UDTMemberIteratorFor(someUDT) '
' Debug.Print member.Name & ":" & member.Value '
' Next '
Public Function UDTMemberIteratorFor(ByVal udt As Variant) As UDTMemberIterator
Dim iterator As New UDTMemberIterator
iterator.Initialize udt
Set UDTMemberIteratorFor = iterator
End Function
Listing 5: The UDTMemberIteratorFor utility function.
Finally, compile the project and create a new project to test it out.
In your test projet, add a reference to the newly-created UDTTypeInformation.dll and the UDTLibrary.dll created in Part 1 and try out the following code in a new module:
'Module1.bas'
Option Explicit
Public Sub TestUDTMemberIterator()
Dim member As UDTMember
Dim p As Person
p.FirstName = "John"
p.LastName = "Doe"
p.BirthDate = #1/1/1950#
For Each member In UDTMemberIteratorFor(p)
Debug.Print member.Name & " : " & member.Value
Next
Dim a As Animal
a.Genus = "Canus"
a.Species = "Canine"
a.NumberOfLegs = 4
For Each member In UDTMemberIteratorFor(a)
Debug.Print member.Name & " : " & member.Value
Next
End Sub
Listing 6: Testing out the UDTMemberIterator class.
If you change all your Types to Classes. You have options. The big pitfall of changing from a type to a class is that you have to use the new keyworld. Every time there a declaration of a type variable add new.
Then you can use the variant keyword or CallByName. VB6 doesn't have anytype of reflection but you can make lists of valid fields and test to see if they are present for example
The Class Test has the following
Public Key As String
Public Data As String
You can then do the following
Private Sub Command1_Click()
Dim T As New Test 'This is NOT A MISTAKE read on as to why I did this.
T.Key = "Key"
T.Data = "One"
DoTest T
End Sub
Private Sub DoTest(V As Variant)
On Error Resume Next
Print V.Key
Print V.Data
Print V.DoesNotExist
If Err.Number = 438 Then Print "Does Not Exist"
Print CallByName(V, "Key", VbGet)
Print CallByName(V, "Data", VbGet)
Print CallByName(V, "DoesNotExist", VbGet)
If Err.Number = 438 Then Print "Does Not Exist"
End Sub
If you attempt to use a field that doesn't exist then error 438 will be raised. CallByName allows you to use strings to call the field and methods of a class.
What VB6 does when you declare Dim as New is quite interesting and will greatly minimize bugs in this conversion. You see this
Dim T as New Test
is not treated exactly the same as
Dim T as Test
Set T = new Test
For example this will work
Dim T as New Test
T.Key = "A Key"
Set T = Nothing
T.Key = "A New Key"
This will give a error
Dim T as Test
Set T = New Test
T.Key = "A Key"
Set T = Nothing
T.Key = "A New Key"
The reason for this is that in the first example VB6 flags T so that anytime a member is accessed it check whether the T is nothing. If it is it will automatically create a new instance of the Test Class and then assign the variable.
In the second example VB doesn't add this behavior.
In most project we rigorously make sure we go Dim T as Test, Set T = New Test. But in your case since you want to convert Types into Classes with the least amount of side effects using Dim T as New Test is the way to go. This is because the Dim as New cause the variable to mimic the way types works more closely.
#Dan,
It looks like your trying to use RTTI of a UDT. I don't think you can really get that information without knowing about the UDT before run-time.
To get you started try:
Understanding UDTs
Because of not having this reflection capability. I would create my own RTTI to my UDTs.
To give you a baseline. Try this:
Type test
RTTI as String
a as Long
b as Long
c as Long
d as Integer
end type
You can write a utility that will open every source file and add The RTTI with the name of the type to the UDT. Probably would be better to put all the UDTs in a common file.
The RTTI would be something like this:
"String:Long:Long:Long:Integer"
Using the memory of the UDT you can extract the values.
I found a great post on SO that seems to be exactly what I want: Is it possible to access a parent property from a child that is in a collection? However my adaptation of it is giving me Object doesn't support this property or method.
My code which now works thanks to Mat's Mug and Tomalak:
Parent Class - clsComputer
Option Explicit
Private pCD As clsCD
''''''''''''''''''''''''''''''
' CD property
''''''''''''''''''''''''''''''
Public Property Get CD() As clsCD
If pCD Is Nothing Then
Set pCD = New clsCD
'Per Mat's Mug post, drop the parenthesis
pCD.Initialze Me
End If
Set CD = pCD
End Property
Public Property Set CD(value As clsCD)
pCD = value
End Property
Child class - clsCD
Option Explicit
Private pParent As clsComputer
'''''''''''''''''''''''''''''
' Status property - READ ONLY
'''''''''''''''''''''''''''''
Public Property Get Status(Optional strHost As String) As String
Dim strResult As String
If strHost = "" Then strHost = Me.Parent.HostName
strResult = RunCMD("cmd /c ""winrs -r:" & strHost & _
" reg query hklm\system\currentcontrolset\services\cdrom /v start""")
If InStr(1, strResult, "0x4", vbTextCompare) Then
Status = "Disabled"
Else
Status = "Enabled"
End If
End Property
'''''''''''''''''''''''''
' Parent property
'''''''''''''''''''''''''
Public Property Get Parent() As clsComputer
Set Parent = pParent
End Property
'Because as Tomalak points out, you use Set with Objects.
Public Property Set Parent(Obj As clsComputer)
Set pParent = Obj
End Property
'''''''''''''''''''''''''
' Initialize Method
'''''''''''''''''''''''''
Public Sub Initialize(Obj As clsComputer)
Set Me.Parent = Obj
End Sub
Code Module - Module1
Sub test()
Dim oPC As clsComputer
Set oPC = New clsComputer
Debug.Print "CD Status: " & oPC.CD.Status
End Sub
If I test Me, it is an object (eg, If IsObject(Me) Then Stop evaluates true), and Intellisense shows all the properties and methods in clsComputer when I type Me. The Locals windows shows Me as a clsComputer object. Everything I know to check says Me is a clsComputer object, so what am I doing wrong?
Classic.
pCD.Initialize (Me) 'Error occurs on this line when using F8
Drop the parentheses.
pCD.Initialize Me
Done.
Parentheses around a parameter force it to be evaluated and passed ByVal (regardless of what the procedure's signature says) - and since you probably haven't defined a default property for clsComputer then the evaluation blows up and the runtime doesn't even get to the Initialize method.
That said, there's nothing wrong with passing object reference by value. In fact, that's what C# and VB.NET do by default - consider passing any parameter ByVal.
Public Property Set Parent(ByRef Obj As clsComputer)
Set pParent = Obj
End Property
I'm not by my PC so just coding blind
Try this for your clsComputer class
Option Explicit
Private pCD As clsCD
''''''''''''''''''''''''''''''
' CD property
''''''''''''''''''''''''''''''
Public Property Get CD() As clsCD
Set CD = pCD
End Property
Public Property Set CD(value As clsCD)
pCD = value
End Property
Sub Class_Initialize()
Set pCD = New clsCD
pCD.Initialize(Me)
End Property
I just discovered that the Me keyword cannot access private procedures even when they are inside its own class model.
Take the following code in Class1:
Private Sub Message()
Debug.Print "Some private procedure."
End Sub
Public Sub DoSomething()
Me.Message
End Sub
This code instantiates an instance of the class:
Sub TestClass()
Dim objClass As New Class1
objClass.DoSomething
End Sub
Me.Message throws compile error "Method or data member not found."
If I change Private Sub Message() to Public the procedure works fine. I can also remove the Me keyword from the DoSomething procedure, but I was under the impression that the idea behind the Me keyword is to ensure that multiple instances of Class1 are properly encapsulated.
Why can't the VBA Me keyword access procedures in its own module when they are private? Is it safe to omit the Me keyword and do something like this in a class?
Private Sub Message()
Debug.Print "Some private procedure."
End Sub
Public Sub DoSomething()
Message
End Sub
Thanks!
Update: Thanks for the tips on proper syntax, my code is working. I am still looking for an explanation of why Me can reference private procedures in an instance of it's own module. I couldn't find any good documentation.
Any guess as to why it was designed that way would be pure supposition without talking to the designers. But my own guess is this, the Me keyword returns a reference to the object the code is currently executing in. I would guess rather than create a special case for Me, they found it easier to continue to obey rules of scope for an object. Which is to say object.method can only work on public or friend methods. So Me, is exactly what it says, an instance of the currently executing object. And since VBA/VB6 doesn't have shared methods, it doesn't really matter if you prefix with Me or not.
But if it makes you feel any better, I find it incredibly obnoxious too.
You do not need the Me keyword to call inside own class.
Me is this class object instance. So no one can directly call private subs or functions or access private variables except this class public functions or subs.
Public Function Fight() As String
'performs a round of attacks i.e. each character from both sides performs an attack
'returns a scripted version of the outcomes of the round
'check if buccaneers are all dead
If mBuccaneers.aliveCount > 0 Then
'check if any hostiles are alive
If mHostiles.aliveCount > 0 Then
'check we have some buccaneers
If mBuccaneers.count = 0 Then
Fight = "There are no buccaneers. Load or create some buccaneers"
Else
If mHostiles.count = 0 Then
'can't fight
Fight = "There are no hostiles to fight. Generate some hostiles"
Else
mScript = ""
Call GroupAttack(mBuccaneers, mHostiles)
Call GroupAttack(mHostiles, mBuccaneers)
Fight = mScript
End If
End If
Else 'hostiles are all dead
Fight = "Hostiles are all dead. Generate a new set of hostiles"
End If
Else
Fight = "Buccaneers are all dead :(. Suggest building or loading a new buccaneer group"
End If
End Function
Uses the private class method GroupAttack by using the Call statement
Private Sub GroupAttack(attackersGroup As clsGroup, defendersGroup As clsGroup)
'implements the attack of one group on another
Dim victimNo As Integer
Dim randomNumber As Integer
Dim attacker As clsCharacter
Dim damage As Integer
Dim defender As clsCharacter
Randomize
For Each attacker In attackersGroup.members
'check if attacker is still alive
If attacker.health > 0 Then
'check if any defenders are still alive because there's no point attacking dead defenders
If defendersGroup.aliveCount > 0 Then
'do some damage on a defender
If defendersGroup.count > 0 Then
'choose a random hostile
victimNo = Int(((Rnd() * defendersGroup.aliveCount) + 1))
'find an alive victim
memberid = 0
j = 0
Do While j < victimNo
memberid = memberid + 1
If defendersGroup.members(memberid).health > 0 Then
j = j + 1
End If
Loop
'reset our victimno to the live victim
victimNo = memberid
damage = defendersGroup.character(victimNo).attack(attacker.strength)
If damage <> 0 Then 'attacker hit
mScript = mScript & attacker.name & " hits " & _
defendersGroup.character(victimNo).name & " for " & damage & " damage"
If defendersGroup.character(victimNo).health = 0 Then
mScript = mScript & " and kills " & defendersGroup.character(victimNo).name
End If
mScript = mScript & vbCrLf
Else 'attacker missed
mScript = mScript & attacker.name & " missed " & defendersGroup.character(victimNo).name & vbCrLf
End If
End If
End If
End If
Next attacker
End Sub
Thats all you need to do ,works like a charm
In COM, there's a difference between the types of object instances and the types of object variables. In particular, the types of object variables behave as interface types. Every type implements at least one interface (itself), but types may implement other interfaces as well. Such ability is used to fake inheritance.
In some frameworks, if class Foo has a private member Bar, then any non-null variable of type Foo will hold a reference to some class object which contains that member. The member may not be accessible to any outside code, but it will exist, and can thus be accessed from anywhere within the code for Foo.
Because COM class-variable types behave like interfaces rather than inheritable class types, however, there's no guarantee that a variable of type Foo will refer to an object which has any of Foo's non-public members. While a compiler could know that Me will always refer to the present object, which will be of actual type Foo, the fact that the only object upon which a private member of Foo could be accessed is Me means that there's no real reason for the compiler to support dot-based dereferencing of private members.