tags:

views:

1061

answers:

4

I have a feel 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

A: 

Even COM objects are not runtime-enumerable in VB 6.0. And you are not even doing COM, you are using internal types, whose properties only have names until you feed your code to the compiler. So no, this won't be possible in VB.

Tomalak
+1  A: 

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.

RS Conley
Be careful with "Dim As New". For example, if you do "Dim acct As New BankAccount" and then later want to do "If acct Is Nothing" it won't work as expected. VB6 will auto-instantiate acct if it is Nothing, so the check for Nothing will *always* return False. Can get you into trouble sometimes...
Mike Spross
...However, I do agree with your point here. For a type that has been converted to a class, there won't be any existing checks for Nothing against variables of that type, so it's only an issue if such a check is added later.
Mike Spross
I fixed the staff that didn't get formatted as code it should be clearer
RS Conley
Wow ... this is like a book.
Nissan Fan
A: 

@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.

Gutzofter
Twisted, but ingenious (I mean that as a compliment!). I think it'll be non-trivial to access the RTTI memory? Also, it only solves part of the problem though - he also wants to log the member names. I guess you could store them too in your RTTI member. I guess you did say it was a baseline
MarkJ
+16  A: 

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.

Mike Spross
Of course this makes the assumption that you are working with UDTs that have type information, i.e. those declared in "public modules" (separately compiled DLL, OCX, etc.).
Bob
The code needs some cleanup as well. GetWrappedmembersForUDT is particularly rough.
Bob
+1 Excellent answer, I would +2 if I could.
MarkJ
@Bob - Can you be more specific? I agree it's not super elegant, but if you have any specific clean-up suggestions/ideas to make it look nicer, I'd be interested in hearing them.
Mike Spross
Whoa. Totally forgot about answers automatically switching to wiki mode after a certain number of revisions...
Mike Spross
Thanks for the great answer! I've been trying to come up with a metyhod for this for a long time. I'll see how an implementation goes today.
Dan
Just got finished with the "quick and dirty" version of implementation. Everything works like a charm! Like MarkJ said, would give +2 if possible. Although, this this solution could also have the unintended consequence of prolonging this component's existence in VB6...
Dan
Also, Bob's comment about separately compiled had me nervous, but this worked fine as a new class in an existing ActiveX dll.
Dan
Hi again. Can you modify your code to make it handle nested UDTs? I have changes to do this, but would like to document them here for posterity. Thanks, Dan
Dan
@Dan, I should have been clearer in my answer regarding making the UDT's public. As you discovered, the UDT's don't have to be in separate DLL. It's making them public that is important. I just suggested putting them in a separate DLL more to show that it works even when they are in another file
Mike Spross