Esta es la primera parte de maximizado el reusó de código. La segunda parte está aquí.

Una técnica de programación muy usada frecuentemente en aplicaciones de Access es la asignación de múltiples manejadores de eventos a un mismo evento de un objeto o compartir el mismo manejador de eventos en diferentes eventos de diferentes objetos. Para mostrarle un ejemplo, vamos a ver cómo mejorar un formulario continuo. Una característica muy buena de los formularios continuos  en el modo vista de datos es que tenemos un mejor control del diseño y de cómo podemos mostrar los datos. Una característica que tenemos en la vista de datos que no se encuentra en los formularios continuos es que podemos navegar fácilmente utilizando nuestro teclado.

Ahora tenemos un sin número de preguntas:

1)      ¿Cómo agregar el código necesario para que los formularios soporten  el uso de la tecla de navegación?

2)      ¿Cómo combinarlo con cualquier manejador de eventos existente?

3)      ¿Cómo hacerlo en la mínima cantidad de pasos?

La repuesta a las tres preguntas es mejor expresarla en un módulo de clases. Con la clase KeyNavigator, podemos agregar solo 4 líneas de código para cada formulario continuo (Sin contar las líneas en blanco ni la declaración de procedimiento) donde queramos habilitar la tecla de navegación.

Private kn As KeyNavigator

Private Form_Load()
  Set kn = New KeyNavigator
  kn.Init Me
End Sub

Private Form_Close()
  Set kn = Nothing
End Sub

¿Cómo estas cuatro líneas proveen todas las funcionalidades? ¿No Necesitamos adjuntar otros manejadores de evento? ¿Cómo esto sabe cuándo se presiona una tecla y se mueve a otro registro? Al final del artículo voy a publicar el código completo pero por ahora miremos el procedimiento Init así como también la variable para la clase KeyNavigator para ver como conectamos los eventos en el módulo de clase.

Private WithEvents frm As Access.Form
...
Private Const Evented As String = "[Event Procedure]"

Public Sub Init(SourceForm As Access.Form)
...
    Set frm = SourceForm
    frm.KeyPreview = True
    frm.OnKeyDown = Evented
...
End Sub

Observe como tenemos una variable declarada a nivel de clase, con la palabra clave “WithEvents” la cual le permite saber a VBA que nosotros queremos suscribir los eventos de este objeto. Claro que luego tendremos que asignar el Init al WithEvents interno de la variable frm. Pero esto no es suficiente para suscribirse a los eventos del formulario.

Recuerde como usted diseña los manejadores de eventos usualmente, usted tiene que ir al tab de eventos de un objeto, agregar el “[Event Procedure]” o el procedimiento del evento para el evento donde usted quiere que se dispare e inmediatamente se genera un código en el módulo del formulario. El proceso sigue siendo el mismo pero los pasos son diferentes para el módulo de clase con un tab WithEvents. Asignamos el  “Event Procedure” a una de las propiedades del formulario como OnKeyDown la cual actualmente responde a la  “On Key Down” propiedad que se muestra en el tab de eventos del formulario. (De hecho, hay muchas propiedades que comienza con “On”  que son en realidad propiedades de evento, pero tenga en cuenta que algunas no tienen prefijo «On»; «AfterUpdate» y «BeforeUpdate» es un ejemplo notable de esto). Haciendo esto le estamos diciendo que queremos que el evento sea manejado. En otras palabras si no se le agrega esto el formulario pensará que no tiene nada que hacer y no hará nada y no se molestará en decirle a alguien “Hey, tengo un evento aquí, ¿Quiere hacer algo con él?”.

La asignación manual en el código es necesaria cuando se le pasa una clase a un formulario que no tiene ningún manejador de eventos. Esto funciona siempre y cuando el formulario seleccionado tenga o no un manejador de eventos para el mismo evento. El único problema es si usamos una función en lugar de un manejador de evento, la función se puede bloquear. Por ejemplo,  si le ponemos “=MyFunction” en la propiedad del evento, el código de arriba cambiaría su comportamiento y la función no se volvería a llamar.

En nuestro proyecto no nos gusta usar funciones preferimos usar manejadores de eventos en todos lados, que eso no es algo por lo cual preocuparse pero cuando hay diferentes proyectos es algo como para tomar en cuenta. Esto también se puede manejar de otra manera, detectando cuando una función existe y llamándola con Eval pero ese es un tema que se merece un artículo para poder explicarlo.

Ahora, con el objeto asignado  a una variable WithEvents y con el manejador de eventos establecido  a un [Event Procedure] podremos agregar un manejador de eventos. Como nombramos la variable “frm” podremos usarla como prefijo en vez de “Form_KeyDown” usaremos esto “frm_KeyDown”, pero ahí se está manejando el mismo evento para el mismo objeto.

Private Sub frm_KeyDown(KeyCode As Integer, Shift As Integer)

    ...
    Select Case KeyCode
        Case vbKeyUp
            ...
        Case vbKeyDown
            ...
        Case vbKeyLeft
            ...
        Case vbKeyRight
            ...
    End Select

...
End Sub

SI coloca su cursor en el procedimiento frm_KeyDown observe que el editor VBA muestra el “frm” en  la lista desplegable de la derecha y “KeyDown” en la de la izquierda. Si usted abre la lista desplegable de la izquierda, verá la misma lista de eventos de un formulario y seleccionando  uno de ellos le generará el código de un evento vacío,  igual como cuando usted agrega un nuevo evento de un procedimiento y cliquea  el botón “…”  en la vista de diseño.

Espero que ahora esté viendo como 4 líneas en el módulo del formulario original se pueden usar para agregar más detalles y mantener el código limpio. Si usted encuentra un error en el módulo de KeyNavigator arréglelo y todos los formularios que estén usando esta clase se actualizarán. ¿Con las nuevas funcionalidades también? Del mismo modo; vaya al módulo de KeyNavigator, agréguela, y todos los formularios disfrutaran de la nueva funcionalidad.

Lo más importante del código para manejar la tecla de navegación se debe mantener separado del código que especifica el formulario; no tenemos que preocuparnos de mezclar dos funcionalidades diferentes en el mismo manejador de eventos. Así que el formulario también puede usar el evento de KeyDown para otro propósito, usted todavía puede ejecutar el código específico del formulario en el procedimiento del Form_KeyDown y en ambos Form_KeyDown en el módulo del formulario y en frm_keyDown en el módulo de KeyNavigator van a responder al mismo evento.

Ya sabiendo esto hay algunos cabos sueltos que hay que ajustar.

1)      No se sabe el orden en que se va a disparar cada evento.

Hasta donde tengo entendido, no hay un documento explícito con el tema e información, con pruebas del orden en el que cada manejador de eventos se dispara, usualmente es el mismo orden en que se agregan a un objeto. Porque el módulo del formulario va a cargar primero, esto significa que el manejador de eventos del módulo de un formulario va a ser manejado antes que cualquier otro evento de cualquier otra clase. De todos modos, recomiendo antes de caer en cualquier suposición que un manejador de eventos se va a disparar antes que otro. Sería mejor si  hubiera una posibilidad de escribir el manejador  de eventos en una forma que no importe el orden. Piense en el manejador de eventos como en una isla con un solo puente de ida y vuelta hacia la isla principal (El objeto dispara lo eventos al mismo tiempo) pero no hay puentes para mandar dos objetos al mismo tiempo así que pasa uno primero y el otro después.

2)      No trate de modificar los parámetros de un evento dentro de un manejador de eventos.

Este no es el motivo #1 pero sí es muy importante. Usaremos el evento BeforeUpdate como ejemplo porque este tiene el parámetro Cancel:

Private Sub Form_BeforeUpdate(Cancel As Integer)

Cuando establecemos  el Cancel = true, el evento BeforeUpdate se cancelará. Pero esto no significa que el manejador de eventos cancelará el evento y  le dirá a los otros manejares de eventos que se detengan. De todos modos para asegurase que los otros manejadores de eventos también sean cancelados utilice  un if :

Private Sub frm_BeforeUpdate(Cancel As Integer)

  If Cancel = False Then
    'perform usual actions
  End If
End Sub

Es bastante razonable leer el parámetro para ver si un manejador de eventos ha cancelado el evento, pero no es una buena idea hacer algo parecido a esto:

Private Sub frm_BeforeUpdate(Cancel As Integer)
  If Cancel = True Then
    Cancel = False
  End If
End Sub

Esto puede ser confuso y como expliqué no estamos realmente seguros de en qué orden se disparan lo manejadores de eventos. En otras palabras, esto puede dar un resultado inesperado y terminar   cancelando algo que no debería cancelarse. Sé que el ejemplo es un poco absurdo pero es solo para mostrarle porque no debemos diseñar nuestros manejadores de eventos con otros parámetros, simplemente acepte lo que tiene por defecto y cámbielo solo si es necesario.

1)      Evite el comando Docmd si es posible, cuando  escribe código non-interactivo

Cuando escribe código que funciona con otras variables de un objeto, no siempre  tenemos todo el conocimiento acerca del contexto de un formulario. Si usted mira el código de ejemplo completo se dará cuenta que no hay ninguna referencia  a DoCmd en ningún lado y por una buena razón. Los métodos de DoCmd son heredados interactivamente, ellos imitan las acciones que pasan cuando un usuario hace click y esto funciona con el objeto activo pero no tenemos una garantía de que el objeto es el mismo que queremos. Hagamos un DoCmd.RunCommand acCmdRecordGoToNew por ejemplo. No hay parámetro que nos especifique que acción debe realizar el formulario. DoCmd.GoToRecord nos das un parámetro para entrar el nombre del formulario pero esto no funcionará si el formulario es un sub-formulario; tiene que ser un formulario principal. Cuando se escribe código robusto usualmente es necesario identificar y usar métodos que no sean interactivos. Algunas veces cuando esto no se puede evitar. Por ejemplo, DoCmd.OpenForm / DoCmd.Close Esta es la única manera de  agregar / eliminar un formulario de la colección de formularios.

Esté pendiente para la segunda parte donde mostraremos como agregar esas 4  líneas de código para 100 formularios continuos.

Aquí le dejo el código completo para la clase keyNavigator:

Option Compare Database

Option Explicit

Private col As VBA.Collection
Private WithEvents frm As Access.Form
Private ctl As Access.Control

Private lngMaxTabs As Long
Private Const Evented As String = "[Event Procedure]"

Public Sub Init(SourceForm As Access.Form)
On Error GoTo ErrHandler
    Dim varTabIndex As Variant

    Set frm = SourceForm
    frm.KeyPreview = True
    frm.OnKeyDown = Evented
    With frm
        For Each ctl In .Section(acDetail).Controls
            varTabIndex = Null
            On Error GoTo NoPropertyErrHandler
            varTabIndex = ctl.TabIndex
            On Error GoTo ErrHandler
            If Not IsNull(varTabIndex) Then
                col.Add ctl, CStr(varTabIndex)
                If lngMaxTabs < CLng(varTabIndex) Then
                     lngMaxTabs = CLng(varTabIndex)
                End If
            End If
        Next
    End With

ExitProc:
    On Error Resume Next
    Exit Sub
NoPropertyErrHandler:
    Select Case Err.Number
        Case 438
            varTabIndex = Null
            Resume Next
    End Select
ErrHandler:
    Select Case Err.Number
        Case Else
            VBA.MsgBox "Error " & Err.Number & ": " & Err.Description, vbCritical, "Unexpected error"
    End Select
    Resume ExitProc
    Resume
End Sub

Private Sub Class_Initialize()
On Error GoTo ErrHandler
    Set col = New VBA.Collection
ExitProc:
    On Error Resume Next
    Exit Sub
ErrHandler:
    Select Case Err.Number
        Case Else
            VBA.MsgBox "Error " & Err.Number & ": " & Err.Description, vbCritical, "Unexpected error"
    End Select
    Resume ExitProc
    Resume
End Sub

Private Sub Class_Terminate()
On Error GoTo ErrHandler
    Do Until col.Count = 0
        col.Remove 1
    Loop
    Set ctl = Nothing
    Set col = Nothing
    Set frm = Nothing
ExitProc:
    On Error Resume Next
    Exit Sub
ErrHandler:
    Select Case Err.Number
        Case Else
            VBA.MsgBox "Error " & Err.Number & ": " & Err.Description, vbCritical, "Unexpected error"
    End Select
    Resume ExitProc
    Resume
End Sub

Private Sub frm_KeyDown(KeyCode As Integer, Shift As Integer)
On Error GoTo ErrHandler
    Dim i As Long
    Dim bolAdvance As Boolean
    Dim bolInsertable As Boolean

    bolInsertable = frm.AllowAdditions
    If bolInsertable Then
        Select Case True
            Case TypeOf frm.Recordset Is DAO.Recordset
                bolInsertable = frm.Recordset.Updatable
            Case TypeOf frm.Recordset Is ADODB.Recordset
                bolInsertable = Not (frm.Recordset.LockType = adLockReadOnly)
            Case Else
                bolInsertable = False
        End Select
    End If

    Select Case KeyCode
        Case vbKeyUp
            With frm.Recordset
                If frm.NewRecord Then
                    If Not (.BOF And .EOF) Then
                        .MoveLast
                    End If
                Else
                    If Not (.BOF And .EOF) Then
                        .MovePrevious
                        If .BOF And Not .EOF Then
                            .MoveFirst
                        End If
                    End If
                End If
            End With
            KeyCode = &H0
        Case vbKeyDown
            With frm.Recordset
                If Not frm.NewRecord Then
                    If Not (.BOF And .EOF) Then
                        .MoveNext
                        If .EOF And Not .BOF Then
                            If bolInsertable Then
                                frm.SelTop = .RecordCount + 1
                            End If
                        End If
                    Else
                        If bolInsertable Then
                            frm.SelTop = .RecordCount + 1
                        End If
                    End If
                End If
            End With
            KeyCode = &H0
        Case vbKeyLeft
            Set ctl = frm.ActiveControl
            On Error GoTo NoPropertyErrHandler
            bolAdvance = (ctl.SelStart = 0)
            On Error GoTo ErrHandler
            If bolAdvance Then
                Do
                    If ctl.TabIndex = 0 Then
                        With frm.Recordset
                            If frm.NewRecord Then
                                .MoveLast
                            Else
                                .MovePrevious
                            End If
                            If .BOF And Not .EOF Then
                                .MoveFirst
                            End If
                        End With
                        Set ctl = col(CStr(lngMaxTabs))
                    Else
                        Set ctl = col(CStr(ctl.TabIndex - 1))
                    End If
                Loop Until ((ctl.TabStop = True) And (ctl.Enabled = True) And (ctl.Visible = True))
                ctl.SetFocus
                KeyCode = &H0
            End If
        Case vbKeyRight
            Set ctl = frm.ActiveControl
            On Error GoTo NoPropertyErrHandler
            bolAdvance = (ctl.SelStart >= Len(ctl.Value))
            On Error GoTo ErrHandler
            If bolAdvance Then
                Do
                    If ctl.TabIndex = lngMaxTabs Then
                        With frm.Recordset
                            If Not frm.NewRecord Then
                                .MoveNext
                            End If
                            If .EOF And Not .BOF Then
                                If bolInsertable Then
                                    frm.SelTop = .RecordCount + 1
                                End If
                            End If
                        End With
                        Set ctl = col("0")
                    Else
                        Set ctl = col(CStr(ctl.TabIndex + 1))
                    End If
                Loop Until ((ctl.TabStop = True) And (ctl.Enabled = True) And (ctl.Visible = True))
                ctl.SetFocus
                KeyCode = &H0
            End If
    End Select

ExitProc:
    On Error Resume Next
    Exit Sub
NoPropertyErrHandler:
    Select Case Err.Number
        Case 94
            Resume ExitProc
        Case 438
            bolAdvance = True
            Resume Next
    End Select
ErrHandler:
    Select Case Err.Number
        Case 3021, 3426
            Resume Next
        Case Else
            VBA.MsgBox "Error " & Err.Number & ": " & Err.Description, vbCritical, "Unexpected error"
    End Select
    Resume ExitProc
    Resume
End Sub