Auto Inspección de VB6 UDTs
user-defined-types (3)
Tengo la sensación de que la respuesta a esto va a ser "imposible", pero lo intentaré ... Estoy en una posición poco envidiable de modificar una aplicación VB6 heredada con algunas mejoras. Convertir a un lenguaje más inteligente no es una opción. La aplicación se basa en una gran colección de tipos definidos por el usuario para mover datos. Me gustaría definir una función común que puede tomar una referencia a cualquiera de estos tipos y extraer los datos contenidos.
En pseudo código, esto es lo que estoy buscando:
Public Sub PrintUDT ( vData As Variant )
for each vDataMember in vData
print vDataMember.Name & ": " & vDataMember.value
next vDataMember
End Sub
Parece que esta información debe estar disponible para COM en alguna parte ... ¿Algún gurú de VB6 por ahí quiere tomar una foto?
Gracias,
Dan
@Dan,
Parece que estás tratando de usar RTTI de un UDT. No creo que realmente pueda obtener esa información sin conocer el UDT antes del tiempo de ejecución. Para empezar, intente:
Comprender los UDT
Por no tener esta capacidad de reflexión. Crearía mi propio RTTI para mis UDT.
Para darte una línea de base. Prueba esto:
Type test
RTTI as String
a as Long
b as Long
c as Long
d as Integer
end type
Puede escribir una utilidad que abrirá todos los archivos fuente y agregará el RTTI con el nombre del tipo al UDT. Probablemente sea mejor colocar todos los UDT en un archivo común.
El RTTI sería algo como esto:
"Cadena: largo: largo: largo: entero"
Usando la memoria del UDT puede extraer los valores.
Si cambia todos sus tipos a Clases. Tienes opciones. El gran escollo de cambiar de un tipo a una clase es que tienes que usar el nuevo mundo clave. Cada vez que hay una declaración de una variable de tipo agrega nueva.
Luego puede usar la palabra clave variante o CallByName. VB6 no tiene ningún tipo de reflejo pero puede hacer listas de campos válidos y probar para ver si están presentes, por ejemplo
La prueba de clase tiene lo siguiente
Public Key As String
Public Data As String
A continuación, puede hacer lo siguiente
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
Si intenta utilizar un campo que no existe, se generará el error 438. CallByName le permite usar cadenas para llamar al campo y a los métodos de una clase.
Lo que VB6 hace cuando declara Dim como nuevo es bastante interesante y minimizará enormemente los errores en esta conversión. Ves esto
Dim T as New Test
no se trata exactamente lo mismo que
Dim T as Test
Set T = new Test
Por ejemplo, esto funcionará
Dim T as New Test
T.Key = "A Key"
Set T = Nothing
T.Key = "A New Key"
Esto dará un error
Dim T as Test
Set T = New Test
T.Key = "A Key"
Set T = Nothing
T.Key = "A New Key"
La razón de esto es que en el primer ejemplo, VB6 marca T de manera que cada vez que se accede a un miembro, verifique si la T no es nada. Si lo es, creará automáticamente una nueva instancia de la Clase de prueba y luego asignará la variable.
En el segundo ejemplo, VB no agrega este comportamiento.
En la mayoría de los proyectos, nos aseguramos rigurosamente de que vayamos a Dim T como Test, Set T = New Test. Pero en su caso, ya que desea convertir Tipos en clases con la menor cantidad de efectos secundarios con Dim T ya que New Test es el camino a seguir. Esto se debe a que Dim como nuevo causa que la variable imite el modo en que los tipos funcionan más de cerca.
Al contrario de lo que otros han dicho, ES POSIBLE obtener información de tipo de tiempo de ejecución para UDT en VB6 (aunque no es una función de lenguaje incorporado). La biblioteca de objetos de información TypeLib de Microsoft (tlbinf32.dll) le permite inspeccionar mediante programación la información del tipo COM en tiempo de ejecución. Ya debe tener este componente si tiene instalado Visual Studio: para agregarlo a un proyecto VB6 existente, vaya a Proyecto-> Referencias y verifique la entrada etiquetada como "Información de TypeLib". Tenga en cuenta que deberá distribuir y registrar tlbinf32.dll en el programa de instalación de su aplicación.
Puede inspeccionar instancias de UDT utilizando el componente de información de TypeLib en tiempo de ejecución, siempre que sus UDT se declaren Public
y se definan dentro de una clase Public
. Esto es necesario para hacer que VB6 genere información de tipo compatible con COM para sus UDT (que luego se pueden enumerar con varias clases en el componente de información de TypeLib). La forma más fácil de cumplir este requisito sería colocar todos sus UDT en una clase de UserTypes
pública que se compilará en una DLL de ActiveX o EXE de ActiveX.
Resumen de un ejemplo de trabajo
Este ejemplo contiene tres partes:
- Parte 1 : Crear un proyecto DLL de ActiveX que contendrá todas las declaraciones públicas de UDT
- Parte 2 : Crear un ejemplo del método
PrintUDT
para demostrar cómo puede enumerar los campos de una instancia de UDT - Parte 3 : Crear una clase de iterador personalizada que le permita iterar fácilmente en los campos de cualquier UDT público y obtener nombres y valores de campo.
El ejemplo de trabajo
Parte 1: la DLL de ActiveX
Como ya mencioné, debe hacer que su UDT sea de acceso público para enumerarlos usando el componente de información de TypeLib. La única forma de lograr esto es colocar su UDT en una clase pública dentro de un archivo DLL ActiveX o un proyecto EXE ActiveX. Otros proyectos en su aplicación que necesitan acceder a su UDT harán referencia a este nuevo componente.
Para seguir con este ejemplo, comience por crear un nuevo proyecto DLL ActiveX y UDTLibrary
nombre UDTLibrary
.
A continuación, cambie el nombre del módulo de clase Class1
(esto se agrega de forma predeterminada por el IDE) a UserTypes
y agregue dos tipos definidos por el usuario a la clase, Person
y 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
Listado 1: UserTypes.cls
actúa como un contenedor para nuestro UDT
A continuación, cambie la propiedad de Instanciación para la clase UserTypes
a "2-PublicNotCreatable". No hay ninguna razón para que nadie instanciar la clase UserTypes
directamente, ya que simplemente actúa como un contenedor público para nuestros UDT.
Finalmente, asegúrese de que el Project Startup Object
del Project Startup Object
(en Proyecto-> Propiedades ) esté configurado en "(Ninguno)" y compile el proyecto. Ahora debería tener un nuevo archivo llamado UDTLibrary.dll
.
Parte 2: enumeración de la información del tipo de UDT
Ahora es el momento de demostrar cómo podemos usar TypeLib Object Library para implementar un método PrintUDT
.
Primero, comienza creando un nuevo proyecto EXE estándar y llámalo como quieras. Agregue una referencia al archivo UDTLibrary.dll
que se creó en la Parte 1. Como solo quiero demostrar cómo funciona esto, usaremos la ventana Inmediato para probar el código que vamos a escribir.
Cree un nuevo Módulo, UDTUtils
nombre UDTUtils
y agregue el siguiente código:
''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
Listado 2: Un ejemplo del método PrintUDT
y un método de prueba simple
Parte 3: Hacerlo orientado a objetos
Los ejemplos anteriores proporcionan una demostración "rápida y sucia" de cómo usar la biblioteca de objetos de información de TypeLib para enumerar los campos de un UDT. En un escenario del mundo real, probablemente crearía una clase UDTMemberIterator
que le permitiría iterar más fácilmente a través de los campos de UDT, junto con una función de utilidad en un módulo que crea un UDTMemberIterator
para una instancia de UDT determinada. Esto le permitiría hacer algo como lo siguiente en su código, que está mucho más cerca del pseudo-código que publicó en su pregunta:
Dim member As UDTMember ''UDTMember wraps a TLI.MemberInfo instance''
For Each member In UDTMemberIteratorFor(someUDT)
Debug.Print member.Name & " : " & member.Value
Next
De hecho, no es demasiado difícil hacer esto, y podemos volver a utilizar la mayoría del código de la rutina PrintUDT
creada en la Parte 2.
Primero, cree un nuevo proyecto ActiveX y UDTTypeInformation
nombre UDTTypeInformation
o algo similar.
A continuación, asegúrese de que el objeto de inicio para el nuevo proyecto esté configurado en "(ninguno)".
Lo primero que debe hacer es crear una clase contenedora simple que oculte los detalles de la clase TLI.MemberInfo
del código de llamada y facilite la obtención del nombre y el valor de un campo UDT. Llamé a esta clase UDTMember
. La propiedad Instancing para esta clase debe ser 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
Listado 3: La clase de contenedor UDTMember
Ahora necesitamos crear una clase de iterador, UDTMemberIterator
, que nos permita usar VB''s For Each...In
sintaxis para iterar los campos de una instancia de UDT. La propiedad de Instancing
para esta clase debe establecerse en PublicNotCreatable
(más PublicNotCreatable
definiremos un método de utilidad que creará instancias en nombre del código de llamada).
EDITAR: (15/2/09) He limpiado el código un poco más.
''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
Listado 4: La clase UDTMemberIterator
.
Tenga en cuenta que para hacer esta clase iterable para que For Each
se pueda usar con ella, deberá establecer ciertos Atributos del Procedimiento en los métodos Item
y _NewEnum
(como se indica en los comentarios del código). Puede cambiar los Atributos del procedimiento desde el menú Herramientas (Herramientas-> Atributos del procedimiento).
Finalmente, necesitamos una función de utilidad ( UDTMemberIteratorFor
en el primer ejemplo de código en esta sección) que creará un UDTMemberIterator
para una instancia de UDT, que luego podemos iterar con For Each
. Cree un nuevo módulo llamado Utils
y agregue el siguiente código:
''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
Listado 5: La función de utilidad UDTMemberIteratorFor
.
Finalmente, compile el proyecto y cree un nuevo proyecto para probarlo.
En su proyecto de prueba, agregue una referencia al recién creado UDTTypeInformation.dll
y el UDTLibrary.dll
creado en la Parte 1 y pruebe el siguiente código en un nuevo módulo:
''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
Listado 6: Probando la clase UDTMemberIterator
.