Colecciones de objetos con VBA Access. Imagen de código VBA.

Colecciones de objetos con VBA Access

En este artículo te voy a ofrecer un ejemplo de sencillo de como manejar colecciones de objetos en Visual Basic para Access (VBA). Incluyo todas las líneas de código y una interesante conversación con ChatGPT para solucionar un error que se producía en el bucle For Each.

Usar la programación orientada a objetos (OOP) te puede ahorrar cientos de horas de trabajo, es una de las mejores técnicas que existen y la más lógica. Espero que estos ejemplos de VBA sean de ayuda y si te interesan más ejemplos te recomiendo este artículo: Trucos y ejemplos de VBA para Microsoft Access.

Ejemplo: Lista de ciudades de una Comunidad Autónoma

El ejemplo que vamos a ver usa 3 objetos y dos colecciones. El objetivo es listar todas las ciudades de una Comunidad Autónoma agrupadas por provincias.

Bucle principal

Se inicia un objeto Region y se recorre la colección States (Provincias). Para cada provincia se recorre su colección de ciudades. En el código original se genera una tabla HTML en el fichero de salida. En este ejemplo he simplificado la salida de datos.

Public Sub ExportarMunicipiosHTML()
    Dim objRegion As New clsRegion
    Dim objState As New clsState
    Dim objCity As clsCity
    Dim txtFile As String
    Dim ind As Long
' Fichero de salida
    txtFile = CurrentProject.Path & "\cities.html"
    If Dir(txtFile) <> "" Then Kill txtFile
    Open txtFile For Output As #1 ' Para exportar municipios en HTML
    Print #1, "Municipios"
' Bucle
    objRegion.Inicializar 1 ' (region_id=1)
    For Each objState In objRegion.StatesInRegions
        Print #1,  objState.state_txt 
        For Each objCity In objState.CitiesInState
            Print #1, objCity.city_txt
        Next
    Next
    Close #1
    Set objCity = Nothing
    Set objState = Nothing
    MsgBox "Proceso Finalizado"
End Sub

Objetos

En esta sección se detalla el código de los 3 objetos utilizados: Region, State y City

Region

El objeto de la Comunidad Autónoma. Contiene la colección States (provincias) como propiedad.

Option Compare Database
Option Explicit
    Dim rst As Recordset
    Dim strSQL   As String
    Dim colStates As clsStates
Public region_id As Long
Public region_txt As String
Public Existe     As Boolean
Public Property Get StatesInRegions() As clsStates
    On Error GoTo ErrorSub
    If colStates Is Nothing Then
        Set colStates = New clsStates
        GoSub InicializarStates
    End If
    Set StatesInRegions = colStates
    Exit Property
InicializarStates:
    strSQL = "SELECT state_id From wp_datastat_states Where region_id=" & region_id
    Set rst = CurrentDb.OpenRecordset(strSQL)
    Do While rst.EOF = False
        colStates.Add rst!state_id
        rst.MoveNext
    Loop
    rst.Close
    Return
ErrorSub:
    Debug.Print Err.Description
End Property
Public Sub Inicializar(Codigo As Long)
    On Error GoTo ErrorSub
    region_id = Codigo
    strSQL = "SELECT * FROM wp_datastat_regions WHERE region_id=" & region_id
    Set rst = CurrentDb.OpenRecordset(strSQL)
    Existe = Not rst.EOF
    If Not Existe Then
        Existe = False
        GoTo InicializarFin
    End If
    region_txt = ValidarString(rst!region_txt)
InicializarFin:
    rst.Close
    Exit Sub
ErrorSub:
    MsgBox Err.Description, vbCritical, Application.Name
End Sub

State

El objeto de la Provincia. Contiene la colección Cities (ciudades o municipios) como propiedad.

Option Compare Database
Option Explicit
    Dim rst As Recordset
    Dim strSQL   As String
    Dim colCities As clsCities
Public state_id As Long
Public state_txt As String
Public Existe     As Boolean
Public Property Get CitiesInState() As clsCities
    On Error GoTo ErrorSub
    If colCities Is Nothing Then
        Set colCities = New clsCities
        GoSub InicializarCities
    End If
    Set CitiesInState = colCities
    Exit Property
InicializarCities:
    strSQL = "SELECT city_id From wp_datastat_cities Where state_id=" & state_id
    Set rst = CurrentDb.OpenRecordset(strSQL)
    Do While rst.EOF = False
        colCities.Add rst!city_id
        rst.MoveNext
    Loop
    rst.Close
    Return
ErrorSub:
    Debug.Print Err.Description
End Property
Public Sub Inicializar(Codigo As Long)
    On Error GoTo ErrorSub
    state_id = Codigo
    strSQL = "SELECT * FROM wp_datastat_states WHERE state_id=" & state_id
    Set rst = CurrentDb.OpenRecordset(strSQL)
    Existe = Not rst.EOF
    If Not Existe Then
        Existe = False
        GoTo InicializarFin
    End If
    state_txt = ValidarString(rst!state_txt)
InicializarFin:
    rst.Close
    Exit Sub
ErrorSub:
    MsgBox Err.Description, vbCritical, Application.Name
End Sub

City

El objeto de la ciudad o municipio. Es el objeto final a procesar. Es clase usa el objeto State como objeto heredado.

Option Compare Database
Option Explicit
    Dim rst As Recordset
    Dim strSQL   As String
Public city_id As Long
Public city_txt As String
Public code_id As String
Public state_id As Long
Public Existe     As Boolean
Public Property Get State() As clsState
    Set State = New clsState
    State.Inicializar state_id
End Property
Public Sub Inicializar(Codigo As Long)
    On Error GoTo ErrorSub
    city_id = Codigo
    strSQL = "SELECT * FROM wp_datastat_cities WHERE city_id=" & city_id
    Set rst = CurrentDb.OpenRecordset(strSQL)
    Existe = Not rst.EOF
    If Not Existe Then
        Existe = False
        GoTo InicializarFin
    End If
    city_txt = ValidarString(rst!city_txt)
    code_id = Format(rst!city_id, "00000")
InicializarFin:
    rst.Close
    Exit Sub
ErrorSub:
    MsgBox Err.Description, vbCritical, Application.Name
End Sub

Colecciones

Las colecciones son lo que permite recorrer los elementos asociados a un objeto, provincias de la región y ciudades de la provincia.

States

La colección de provincias que permite definirla en el objeto Region.

Option Compare Database
Option Explicit
Private mcolStates As New Collection
Public Function Add(ByVal state_id As Long, Optional ControlErrores As Boolean = True) As String
    On Error GoTo ErrorSub
    Dim clsState As New clsState
    clsState.Inicializar state_id
    mcolStates.Add clsState, CStr(state_id)
    Add = state_id ' Devuelve la referencia al objeto creado
    Set clsState = Nothing
    Exit Function
ErrorSub:
    Debug.Print Err.Description
End Function
Public Function AddState(ByVal objState As clsState) As clsState
    On Error GoTo ErrorSub
    mcolStates.Add objState, objState.state_id
    Set AddState = objState ' Devuelve la referencia al objeto creado
    Exit Function
ErrorSub:
    Debug.Print Err.Description
End Function
Public Function Count() As Long
   Count = mcolStates.Count
End Function
Public Function Item(ByVal Index As Variant) As clsState
    On Error Resume Next
    Set Item = mcolStates.Item(Index)
End Function
Public Function Remove(ByVal Index As Variant)
    mcolStates.Remove Index
End Function
' NewEnum tiene que devolver la interfaz IUnknown del enumerador de una colección.
Public Function NewEnum() As IUnknown
   Set NewEnum = mcolStates.[_NewEnum]
End Function

Cities

La colección de ciudades (municipios) que permite definirla en el objeto State.

Option Compare Database
Option Explicit
Private mcolCities As New Collection
Public Function Add(ByVal city_id As Long, Optional ControlErrores As Boolean = True) As String
    On Error GoTo ErrorSub
    Dim objCity As New clsCity
    objCity.Inicializar city_id
    mcolCities.Add objCity, CStr(city_id)
    Add = CStr(city_id) ' Devuelve la referencia al objeto creado
    Set objCity = Nothing
    Exit Function
ErrorSub:
    Debug.Print Err.Description
End Function
Public Function AddCity(ByVal objCity As clsCity) As clsCity
    On Error GoTo ErrorSub
    mcolCities.Add objCity, objCity.city_id
    Set AddCity = objCity ' Devuelve la referencia al objeto creado
    Exit Function
ErrorSub:
    Debug.Print Err.Description
End Function
Public Function Count() As Long
   Count = mcolCities.Count
End Function
Public Function Item(ByVal Index As Variant) As clsCity
    On Error Resume Next
    Set Item = mcolCities.Item(Index)
End Function
Public Function Remove(ByVal Index As Variant)
    mcolCities.Remove Index
End Function
' NewEnum tiene que devolver la interfaz IUnknown del enumerador de una colección.
Public Function NewEnum() As IUnknown
   Set NewEnum = mcolCities.[_NewEnum]
End Function

Error en bucle For Each y ChatGPT

En el momento de usar el bucle For Each se produjo un error, pese a crearse correctamente la colección no recorría sus elementos. La ayuda de ChatGPT fue de mucha utilidad y por eso reproduzco el diálogo.

Pregunta:
Tengo un problema con un código VBA en una base de datos Access formato accdb. Creo una colección de Cities para el objeto State, se carga bien la colección con todos los items, pero después de devolver la colección con «Set CitiesInState = colCities» se produce el error «El objeto no admite la propiedad o método» en la línea del bucle principal «For Each objCity In objState.CitiesInState»

A continuación le pego el código relacionado.

Respuesta:
Tu problema parece estar relacionado con cómo estás manejando la colección y los objetos dentro de VBA para Access. Aquí hay algunos puntos que debemos revisar y posibles soluciones:

  1. Tipo de Colección: En VBA, normalmente las colecciones contienen objetos de un tipo específico. Tu propiedad CitiesInState está devolviendo un objeto de tipo clsCities. Es importante asegurarse de que clsCities sea efectivamente una colección (o se comporte como tal) y que esté correctamente definido para manejar objetos del tipo clsCity.
  2. Iteración sobre la Colección: Cuando haces For Each objCity In objState.CitiesInState, estás asumiendo que objState.CitiesInState devuelve una colección de objetos clsCity. Si ese no es el caso, recibirás un error. Asegúrate de que la colección colCities dentro de la clase clsState esté efectivamente almacenando y devolviendo objetos del tipo clsCity.
  3. Inicialización de la Colección: Tu método InicializarCities llena la colección con IDs de ciudad (city_id). Sin embargo, parece que debería estar llenándola con objetos clsCity. Si colCities.Add rst!city_id simplemente añade un ID en lugar de un objeto, intentar iterar a través de la colección como si contuviera objetos clsCity causará errores.Puedes solucionarlo creando instancias de clsCity y agregándolas a la colección en lugar de solo agregar el ID:
  4. Definición de la Colección y los Objetos: Asegúrate de que la clase clsCities esté definida de manera que permita agregar y almacenar objetos clsCity. Si clsCities es simplemente una colección genérica, esto debería funcionar, pero si es una clase personalizada, necesitas asegurarte de que tenga métodos para agregar y recuperar objetos clsCity.
  5. Error Handling: Tu manejo de errores en CitiesInState está bien, pero asegúrate de que esté capturando y mostrando los errores relevantes correctamente. El error «El objeto no admite la propiedad o método» sugiere que la línea que causa el problema está intentando usar un método o propiedad que uno de los objetos no soporta. Esto es más común cuando se espera un tipo de objeto, pero se recibe otro.

Revisa estos puntos en tu código. Es muy probable que el problema esté relacionado con la forma en que estás añadiendo elementos a la colección colCities o cómo estás intentando iterar a través de esta colección. Asegúrate de que los objetos que añades a colCities son efectivamente del tipo clsCity y que clsCity tiene definida la propiedad city_txt que intentas imprimir en tu bucle.

Pregunta:
Revisa el código de la colección Cities

Respuesta:
Tu implementación de la colección personalizada clsCities para manejar objetos de tipo clsCity parece estar en la dirección correcta, pero hay algunos aspectos y prácticas recomendadas que podemos revisar para asegurarnos de que funcione correctamente y de manera eficiente en VBA.

  1. Manejo de Errores: En tus funciones Add y AddCity, el manejo de errores es adecuado, pero en la función Item, utilizas On Error Resume Next. Este enfoque puede ocultar errores en tu código. Es mejor manejar los errores de manera explícita o, al menos, documentar por qué se utiliza On Error Resume Next y restaurar el manejo normal de errores con On Error GoTo 0 después de la línea que podría causar errores.
  2. Método Add: En tu método Add, estás inicializando un nuevo objeto clsCity y luego lo agregas a la colección. Esto está bien, pero asegúrate de que el método Inicializar en clsCity esté bien definido y esté estableciendo correctamente todas las propiedades necesarias del objeto clsCity a partir del city_id.
  3. Método AddCity: Este método permite agregar directamente un objeto clsCity a la colección, lo que es una buena práctica ya que ofrece flexibilidad para agregar objetos que ya han sido creados y posiblemente modificados. Solo asegúrate de que los objetos clsCity siempre tengan un city_id válido antes de intentar agregarlos a la colección.
  4. Método Item: Este método permite acceder a elementos individuales de la colección por índice o clave. Asegúrate de que cuando uses este método, estés preparado para manejar casos donde el índice no existe o la clave no se encuentra en la colección, ya que On Error Resume Next puede ocultar estos problemas.
  5. Método NewEnum: Implementar este método es una buena práctica ya que permite usar la sintaxis For Each en la colección personalizada. Parece que lo has hecho correctamente aquí, permitiendo que la colección se comporte más como una colección estándar de VBA.
  6. Documentación y Convenciones: Aunque tu código parece estar bien en términos de funcionalidad, no olvides documentarlo adecuadamente. Comentar tu código ayudará a otros desarrolladores (y a ti mismo en el futuro) a entender rápidamente lo que cada parte se supone que debe hacer.
  7. Pruebas: Después de realizar cualquier cambio, asegúrate de probar tu colección personalizada exhaustivamente. Asegúrate de probar la adición de nuevos objetos clsCity, el acceso a elementos individuales, la eliminación de elementos y la iteración a través de la colección con For Each.
  8. Optimización del Código: Por último, aunque no menos importante, siempre busca maneras de optimizar tu código. Por ejemplo, si encuentras que estás utilizando una pieza de código repetidamente, considera si puede ser extraída a una función o procedimiento separado.

Si después de revisar y aplicar los puntos anteriores sigues teniendo problemas, sería útil revisar también cómo estás implementando y usando la clase clsCity, especialmente cómo se inicializa y cómo se manejan sus propiedades internas.

Pregunta:
Con el mismo código este bucle funciona bien:

    
For ind = 1 To objState.CitiesInState.Count
    Set objCity = objState.CitiesInState.Item(ind)
    Debug.Print objCity.city_txt
Next

El problema surge cuando uso el bucle For Each
Respuesta:

El hecho de que el bucle con el índice funcione correctamente pero el bucle For Each no, sugiere que hay un problema con cómo tu colección personalizada clsCities está implementando el mecanismo de enumeración necesario para el bucle For Each.

En VBA, para que una colección personalizada sea enumerable con For Each, debes asegurarte de lo siguiente:

  1. Implementar correctamente el método _NewEnum: Este es el método que permite que tu colección personalizada sea utilizada con un bucle For Each. Parece que ya lo tienes en tu código, lo cual es bueno. Sin embargo, asegúrate de que está correctamente configurado. El método _NewEnum debe tener el atributo [VB_UserMemId] configurado en -4. Normalmente, este ajuste no se puede hacer directamente en el editor de VBA, sino que se debe hacer a través de la edición del archivo exportado o utilizando herramientas de terceros que permiten modificar los atributos de los miembros de una clase.Si después de verificar y corregir estos aspectos sigue sin funcionar, podría deberse a otras particularidades del entorno de VBA o a detalles específicos de tu implementación. Pero el ajuste más común y necesario para solucionar problemas con el bucle For Each en colecciones personalizadas es asegurarse de que el método _NewEnum esté correctamente atribuido y configurado, ya que es lo que permite que la colección sea enumerable. Ten en cuenta que esta modificación no se puede realizar directamente en el editor de VBA estándar. Necesitas exportar tu clase, editar el archivo de texto para añadir la línea Attribute NewEnum.VB_UserMemId = -4 justo arriba del método NewEnum, y luego reimportar la clase. O usar una herramienta de terceros que permita modificar estos atributos.
  2. Implementación de la interfaz IUnknown: Asegúrate de que tu colección maneja correctamente la interfaz IUnknown a través del método _NewEnum. Por lo general, esto se maneja automáticamente cuando se devuelve mcolCities.[_NewEnum], pero si hay un problema en cómo se ha configurado la colección o la clase, podría resultar en errores.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Uso de cookies

Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las mencionadas cookies y la aceptación de nuestra política de cookies, pinche el enlace para mayor información.plugin cookies

ACEPTAR
Aviso de cookies