vba - interfaz - interface vb net
¿Podemos usar interfaces y eventos juntos al mismo tiempo? (4)
Todavía estoy tratando de entender cómo las interfaces y los eventos funcionan en conjunto (¿si acaso?) En VBA. Estoy a punto de construir una gran aplicación en Microsoft Access, y quiero que sea lo más flexible y extensible posible. Para hacer esto, quiero usar MVC , Interfaces ( 2 ) ( 3 ), Clases de colección personalizadas , Criar eventos usando clases de colección personalizadas , encontrar mejores formas de centralize y manage los eventos activados por los controles en un formulario, y algunos patrones de diseño de VBA adicionales.
Anticipo que este proyecto se pondrá bastante complicado, así que quiero tratar de asimilar los límites y los beneficios de usar interfaces y eventos en VBA, ya que son las dos formas principales (creo) de implementar realmente el acoplamiento flexible en VBA.
Para empezar, hay una pregunta sobre un error al tratar de usar interfaces y eventos juntos en VBA. La respuesta dice "aparentemente no se permite que los eventos pasen a través de una clase de interfaz a la clase concreta como se desea usando ''Implementos''".
Luego encontré esta declaración en una respuesta en otro foro : "En VBA6 solo podemos generar eventos declarados en la interfaz predeterminada de una clase: no podemos generar eventos declarados en una interfaz Implementada".
Como sigo pensando en interfaces y eventos (VBA es el primer idioma en el que realmente he tenido la oportunidad de probar OOP en un entorno real, lo siento estremecer ), no puedo recordar en mi mente qué es todo esto significa para usar eventos e interfaces juntos en VBA. Suena como si pudieras usar ambos al mismo tiempo, y suena como que no puedes. (Por ejemplo, no estoy seguro de lo que significa "interfaz predeterminada de una clase" frente a "una interfaz implementada").
¿Puede alguien darme algunos ejemplos básicos de los beneficios y limitaciones reales del uso de interfaces y eventos juntos en VBA?
Clase implementada
'' clsHUMAN
Public Property Let FirstName(strFirstName As String)
End Property
Clase derivada
'' clsEmployee
Implements clsHUMAN
Event evtNameChange()
Private Property Let clsHUMAN_FirstName(RHS As String)
UpdateHRDatabase
RaiseEvent evtNameChange
End Property
Usando en forma
Private WithEvents Employee As clsEmployee
Private Sub Employee_evtNameChange()
Me.cmdSave.Enabled = True
End Sub
Debido a que la recompensa ya está destinada a la respuesta de Pieter, no intentaré responder el aspecto MVC de la pregunta, sino la pregunta principal. La respuesta es que los eventos tienen límites.
Sería duro llamarlos "azúcar sintáctico" porque ahorran una gran cantidad de código, pero en algún momento si su diseño se vuelve demasiado complejo, entonces debe descartar e implementar manualmente la funcionalidad.
Pero primero, un mecanismo de devolución de llamada (porque eso es lo que son los eventos)
modMain, el punto de entrada / salida
Option Explicit
Sub Main()
Dim oClient As Client
Set oClient = New Client
oClient.Run
End Sub
Cliente
Option Explicit
Implements IEventListener
Private Sub IEventListener_SomethingHappened(ByVal vSomeParam As Variant)
Debug.Print "IEventListener_SomethingHappened " & vSomeParam
End Sub
Public Sub Run()
Dim oEventEmitter As EventEmitter
Set oEventEmitter = New EventEmitter
oEventEmitter.ServerDoWork Me
End Sub
IEventListener, el contrato de interfaz que describe los eventos
Option Explicit
Public Sub SomethingHappened(ByVal vSomeParam As Variant)
End Sub
EventEmitter, la clase de servidor
Option Explicit
Public Sub ServerDoWork(ByVal itfCallback As IEventListener)
Dim lLoop As Long
For lLoop = 1 To 3
Application.Wait Now() + CDate("00:00:01")
itfCallback.SomethingHappened lLoop
Next
End Sub
Entonces, ¿cómo funciona WithEvents? Una respuesta es buscar en la biblioteca de tipos, aquí hay algunos IDL de Access ( Microsoft Access 15.0 Object Library
) que definen los eventos que se deben generar.
[
uuid(0EA530DD-5B30-4278-BD28-47C4D11619BD),
hidden,
custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, "Microsoft.Office.Interop.Access._FormEvents")
]
dispinterface _FormEvents2 {
properties:
methods:
[id(0x00000813), helpcontext(0x00003541)]
void Load();
[id(0x0000080a), helpcontext(0x00003542)]
void Current();
''/* omitted lots of other events for brevity */
};
También desde Access IDL aquí está la clase que detalla cuál es su interfaz principal y qué es la interfaz de eventos, busque la palabra clave de source
, y VBA necesita una dispinterface
para ignorar uno de ellos.
[
uuid(7398AAFD-6527-48C7-95B7-BEABACD1CA3F),
helpcontext(0x00003576)
]
coclass Form {
[default] interface _Form3;
[source] interface _FormEvents;
[default, source] dispinterface _FormEvents2;
};
Entonces, lo que le está diciendo a un cliente es que me opere a través de la interfaz _Form3, pero si desea recibir eventos, entonces usted, el cliente, debe implementar _FormEvents2. Y créanlo o no, VBA lo hará cuando se logre WithEvents, desarrolle un objeto que implemente la interfaz de origen y luego enrute las llamadas entrantes a su código de controlador de VBA. Bastante sorprendente en realidad.
Entonces, VBA genera una clase / objeto que implementa la interfaz de origen para usted, pero el interrogador ha cumplido con los límites del mecanismo y los eventos del polimorfismo de la interfaz. Así que mi consejo es abandonar WithEvents e implementar su propia interfaz de devolución de llamada, y esto es lo que hace el código anterior.
Para obtener más información, le recomiendo leer un libro en C ++ que implemente eventos usando las interfaces de punto de conexión, sus términos de búsqueda de Google son puntos de conexión con eventos.
Aquí hay una buena cita de 1994 que destaca el trabajo que VBA mencioné anteriormente
Después de recorrer el código CSink anterior, descubrirá que interceptar eventos en Visual Basic es casi desalentadoramente fácil. Simplemente usa la palabra clave WithEvents cuando declara una variable de objeto, y Visual Basic crea dinámicamente un objeto receptor que implementa la interfaz de origen admitida por el objeto conectable. Luego crea una instancia del objeto usando la nueva palabra clave Visual Basic. Ahora, cada vez que el objeto conectable llama a los métodos de la interfaz de origen, el objeto receptor de Visual Basic verifica si ha escrito algún código para manejar la llamada.
EDITAR: En realidad, reflexionando sobre mi código de ejemplo podría simplificar y eliminar la clase de interfaz intermedia si no quiere replicar la forma en que COM hace las cosas y no le molesta el acoplamiento. Después de todo, es solo un mecanismo de devolución de llamada glorificado. Creo que este es un ejemplo de por qué COM tiene una reputación de ser demasiado complicado.
Este es un caso de uso perfecto para un adaptador : adapta internamente la semántica para un conjunto de contratos (interfaces) y los expone como su propia API externa; posiblemente de acuerdo con algún otro contrato.
Definir módulos de clase IViewEvents:
Option Compare Database
Option Explicit
Private Const mModuleName As String = "IViewEvents"
Public Sub OnBeforeDoSomething(ByVal Data As Object, ByRef Cancel As Boolean): End Sub
Public Sub OnAfterDoSomething(ByVal Data As Object): End Sub
Private Sub Class_Initialize()
Err.Raise 5, mModuleName, AccessError(5) & "-Interface class must not be instantiated."
End Sub
IViewCommands:
Option Compare Database
Option Explicit
Private Const mModuleName As String = "IViewCommands"
Public Sub DoSomething(ByVal arg1 As String, ByVal arg2 As Long): End Sub
Private Sub Class_Initialize()
Err.Raise 5, mModuleName, AccessError(5) & "-Interface class must not be instantiated."
End Sub
ViewAdapter:
Option Compare Database
Option Explicit
Private Const mModuleName As String = "ViewAdapter"
Public Event BeforeDoSomething(ByVal Data As Object, ByRef Cancel As Boolean)
Public Event AfterDoSomething(ByVal Data As Object)
Private mView As IViewCommands
Implements IViewCommands
Implements IViewEvents
Public Function Initialize(View As IViewCommands) As ViewAdapter
Set mView = mView
Set Initialize = Me
End Function
Private Sub IViewCommands_DoSomething(ByVal arg1 As String, ByVal arg2 As Long)
mView.DoSomething arg1, arg2
End Sub
Private Sub IViewEvents_OnBeforeDoSomething(ByVal Data As Object, ByRef Cancel As Boolean)
RaiseEvent BeforeDoSomething(Data, Cancel)
End Sub
Private Sub IViewEvents_OnAfterDoSomething(ByVal Data As Object)
RaiseEvent AfterDoSomething(Data)
End Sub
y controlador:
Option Compare Database
Option Explicit
Private Const mModuleName As String = "ViewAdapter"
Private WithEvents mViewAdapter As ViewAdapter
Private mData As Object
Public Function Initialize(ViewAdapter As ViewAdapter) As Controller
Set mViewAdapter = ViewAdapter
Set Initialize = Me
End Function
Private Sub mViewAdapter_AfterDoSomething(ByVal Data As Object)
'' Do stuff
End Sub
Private Sub mViewAdapter_BeforeDoSomething(ByVal Data As Object, ByRef Cancel As Boolean)
Cancel = Not Data Is Nothing
End Sub
más constructores de módulos estándar:
Option Compare Database
Option Explicit
Option Private Module
Private Const mModuleName As String = "Constructors"
Public Function NewViewAdapter(View As IViewCommands) As ViewAdapter
With New ViewAdapter: Set NewViewAdapter = .Initialize(View): End With
End Function
Public Function NewController(ByVal ViewAdapter As ViewAdapter) As Controller
With New Controller: Set NewController = .Initialize(ViewAdapter): End With
End Function
y MyApplication:
Option Compare Database
Option Explicit
Private Const mModuleName As String = "MyApplication"
Private mController As Controller
Public Function LaunchApp() As Long
Dim frm As IViewCommands
'' Open and assign frm here as instance of a Form implementing
'' IViewCommands and raising events through the callback interface
'' IViewEvents. It requires an initialization method (or property
'' setter) that accepts an IViewEvents argument.
Set mController = NewController(NewViewAdapter(frm))
End Function
Tenga en cuenta cómo el uso del patrón de adaptador combinado con la programación de las interfaces da como resultado una estructura muy flexible, donde diferentes implementaciones de controlador o vista pueden sustituirse en el tiempo de ejecución. Cada definición de Controlador (en el caso de que se requieran diferentes implementaciones) utiliza instancias diferentes de la misma implementación de ViewAdapter, ya que Dependency Injection se usa para delegar el origen de eventos y el sumidero de comandos para cada instancia en tiempo de ejecución.
El mismo patrón se puede repetir para definir la relación entre el Controlador / Presentador / ViewModel y el Modelo, aunque implementar MVVM en COM puede ser bastante tedioso. Descubrí que MVP o MVC suelen ser más adecuados para aplicaciones basadas en COM.
Una implementación de producción también agregaría el manejo adecuado de errores (como mínimo) en la medida admitida por VBA, que solo he insinuado con la definición de la constante mModuleName en cada módulo.
Una interfaz es, estrictamente hablando y solo en términos de OOP, lo que un objeto expone al mundo exterior (es decir, sus llamadores / "clientes").
Entonces puede definir una interfaz en un módulo de clase, digamos ISomething
:
Option Explicit
Public Sub DoSomething()
End Sub
En otro módulo de clase, digamos Class1
, puede implementar la interfaz ISomething
:
Option Explicit
Implements ISomething
Private Sub ISomething_DoSomething()
''the actual implementation
End Sub
Cuando hagas exactamente eso, observa cómo Class1
no expone nada; la única forma de acceder a su método DoSomething
es a través de la interfaz ISomething
, por lo que el código de llamada se vería así:
Dim something As ISomething
Set something = New Class1
something.DoSomething
Entonces, ISomething
es la interfaz aquí, y el código que realmente se ejecuta se implementa en el cuerpo de Class1
. Este es uno de los pilares fundamentales de OOP: polimorfismo , porque muy bien podría tener una Class2
que implemente ISomething
de una manera tremendamente diferente, aunque la persona que llama no tenga que preocuparse en absoluto: la implementación se abstrae detrás de una interfaz, ¡y eso es algo hermoso y refrescante para ver en el código VBA!
Sin embargo, hay varias cosas que se deben tener en cuenta:
- Normalmente, los campos se consideran detalles de implementación: si una interfaz expone campos públicos, las clases de implementación deben implementar un
Property Get
y unProperty Let
(oSet
, según el tipo) para él. - Los eventos también se consideran detalles de implementación. Por lo tanto, deben implementarse en la clase que
Implements
la interfaz, no la interfaz en sí.
Ese último punto es bastante molesto. Dada la Class1
que se ve así:
''@Folder Demo
Public Foo As String
Public Event BeforeDoSomething()
Public Event AfterDoSomething()
Public Sub DoSomething()
End Sub
La clase de implementación se vería así:
''@Folder Demo
Implements Class1
Private Sub Class1_DoSomething()
''method implementation
End Sub
Private Property Let Class1_Foo(ByVal RHS As String)
''field setter implementation
End Property
Private Property Get Class1_Foo() As String
''field getter implementation
End Property
Si es más fácil de visualizar, el proyecto se ve así:
Así que Class1
podría definir eventos, pero la clase implementadora no tiene forma de implementarlos, eso es una cosa triste acerca de los eventos e interfaces en VBA, y se debe a la forma en que los eventos funcionan en COM : los eventos mismos se definen en su propio "proveedor de eventos" interfaz; así que una "interfaz de clase" no puede exponer eventos en COM (por lo que yo lo entiendo), y por lo tanto en VBA.
Entonces los eventos deben definirse en la clase de implementación para que tengan sentido:
''@Folder Demo
Implements Class1
Public Event BeforeDoSomething()
Public Event AfterDoSomething()
Private foo As String
Private Sub Class1_DoSomething()
RaiseEvent BeforeDoSomething
''do something
RaiseEvent AfterDoSomething
End Sub
Private Property Let Class1_Foo(ByVal RHS As String)
foo = RHS
End Property
Private Property Get Class1_Foo() As String
Class1_Foo = foo
End Property
Si desea manejar los eventos que Class2
plantea al ejecutar código que implementa la interfaz Class1
, necesita un campo WithEvents
nivel de módulo Class2
(la implementación) y una variable de objeto de nivel de procedimiento de tipo Class1
(la interfaz):
''@Folder Demo
Option Explicit
Private WithEvents SomeClass2 As Class2 '' Class2 is a "concrete" implementation
Public Sub Test(ByVal implementation As Class1) ''Class1 is the interface
Set SomeClass2 = implementation '' will not work if the "real type" isn''t Class2
foo.DoSomething '' runs whichever implementation of the Class1 interface was supplied
End Sub
Private Sub SomeClass2_AfterDoSomething()
''handle AfterDoSomething event of Class2 implementation
End Sub
Private Sub SomeClass2_BeforeDoSomething()
''handle BeforeDoSomething event of Class2 implementation
End Sub
Y entonces tenemos Class1
como interfaz, Class2
como implementación y Class3
como código de cliente:
... lo que podría decirse que frustra el propósito del polimorfismo, ya que esa clase ahora está acoplada con una implementación específica, pero eso es lo que hacen los eventos de VBA: son detalles de implementación, inherentemente combinados con una implementación específica ... por lo que sé .