visual net interfaz vba oop ms-access interface access-vba

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 un Property Let (o Set , 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é .