Czy możemy używać interfejsów i zdarzeń jednocześnie?

Wciąż staram się ogarnąć, jak interfejsy i zdarzenia współpracują ze sobą (jeśli w ogóle?) w VBA. Mam zamiar zbudować dużą aplikację w Microsoft Access i chcę, aby była jak najbardziej elastyczna i rozszerzalna. Aby to zrobić, chcę skorzystać z MVC, interfejsy (2) (3) , niestandardowe klasy kolekcji, tworzenie zdarzeń za pomocą klas Custom Collection , znajdowanie lepszych sposobów na centralizację i Zarządzanie zdarzenia wywołane przez kontrolki na formularzu, a niektóre dodatkowe wzorce projektowe VBA .

Spodziewam się, że ten projekt będzie dość owłosiony, więc chcę spróbować wyciąć granice i korzyści wynikające z używania interfejsów i zdarzeń razem w VBA, ponieważ są to dwa główne sposoby (myślę), aby naprawdę zaimplementować luźne sprzężenie w VBA.

Na początek, jest to pytanie o błędzie powstałym podczas próby użycia interfejsów i zdarzeń razem w VBA. Na odpowiedź mówi: "najwyraźniej zdarzenia nie mogą być przekazywane przez klasę interfejsu do konkretnej klasy, tak jak chcesz używać 'Implements'."

Potem znalazłem to stwierdzenie w odpowiedzi na innym forum: "w VBA6 możemy wywoływać tylko zdarzenia zadeklarowane w domyślnym interfejsie klasy - nie możemy wywoływać zdarzeń zadeklarowanych w zaimplementowanym interfejsie."

Ponieważ wciąż groking interfejsów i zdarzeń (VBA jest pierwszym językiem, który naprawdę miałem okazję wypróbować OOP w realne ustawienie, wiem shudder ), nie mogę do końca rozgryźć w moim umyśle, co to wszystko oznacza dla korzystania zdarzeń i interfejsów razem w VBA. To trochę brzmi, jak można używać ich obu w tym samym czasie, i to trochę brzmi, jak nie można. (na przykład, nie jestem pewien, co jest rozumiane powyżej przez "domyślny interfejs klasy" vs " zaimplementowany interfejs.")

Czy ktoś może mi podać kilka podstawowych przykładów rzeczywistych korzyści i ograniczeń korzystania z interfejsów i zdarzeń razem w VBA?

Author: Community, 2016-12-07

4 answers

Jest to idealny przypadek użycia adaptera : wewnętrznie dostosowuje semantykę dla zestawu umów (interfejsów) i ujawnia je jako własne zewnętrzne API; być może zgodnie z innym kontraktem.

Define class modules Iviewents:

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 = View
    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

I Kontroler:

Option Compare Database
Option Explicit

Private Const mModuleName       As String = "Controller"

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 = Data Is Nothing
End Sub

Plus Konstruktory Modułów Standardowych:

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

I Aplikacji:

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

Uwaga Jak używać ze wzoru adaptera w połączeniu z programowaniem interfejsów daje bardzo elastyczną strukturę, w której można zastąpić różne implementacje kontrolera lub widoku w czasie wykonywania. Każda definicja kontrolera (w przypadku, gdy wymagane są różne implementacje) używa różnych instancji tej samej implementacji ViewAdapter, ponieważ Wtrysk zależności jest używany do delegowania źródła zdarzenia i polecenia sink dla każdej instancji w czasie wykonywania.

Ten sam wzór może być powtarzane w celu zdefiniowania relacji między kontrolerem / prezenterem/Viewmodelem a modelem, choć implementacja MVVM w COM może być dość żmudna. Znalazłem MVP lub MVC jest zwykle lepiej nadaje się do aplikacji opartych na COM.

Implementacja produkcyjna dodałaby również odpowiednią obsługę błędów (co najmniej) w zakresie obsługiwanym przez VBA, o czym wspomniałem tylko przy definicji stałej mModuleName w każdym module.

 19
Author: Pieter Geerkens,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-08-30 20:49:43

Interfejs jest, ściśle mówiąc i tylko w kategoriach OOP, tym, co obiekt wystawia na świat zewnętrzny (tj. jego wywołujących/"klientów").

Więc możesz zdefiniować interfejs w module klasowym, powiedzmy ISomething:

Option Explicit
Public Sub DoSomething()
End Sub

W innym module klas, powiedzmy Class1, możesz zaimplementować interfejs ISomething:

Option Explicit
Implements ISomething

Private Sub ISomething_DoSomething()
    'the actual implementation
End Sub

Kiedy zrobisz dokładnie to, zauważ, że Class1 niczego nie ujawnia; jedynym sposobem na dostęp do jej metody DoSomething jest ISomething interfejs, więc kod wywołujący wyglądałby tak:

Dim something As ISomething
Set something = New Class1
something.DoSomething

Więc ISomething jest interfejsem tutaj, a kod, który faktycznie działa, jest zaimplementowany w ciele Class1. Jest to jeden z podstawowych filarów OOP: polimorfizm - ponieważ bardzo dobrze można mieć Class2, który implementuje ISomething w zupełnie inny sposób, jednak rozmówca wcale nie musi się tym przejmować: implementacja jest abstrakcyjna za interfejsem - i to piękna i odświeżająca rzecz do zobaczenia w kodzie VBA!

Jest jednak kilka rzeczy, o których warto pamiętać:]}
  • pola są zwykle uważane za szczegóły implementacji: jeśli interfejs wyświetla publiczne pola, klasy implementujące muszą dla niego zaimplementować Property Get i a Property Let (lub Set, w zależności od typu).
  • zdarzenia są również uważane za szczegóły implementacji. Dlatego muszą być zaimplementowane w klasie, która Implements interfejs, nie sam interfejs.
Ten ostatni punkt jest dość irytujący. Biorąc pod uwagę Class1 to wygląda tak:
'@Folder StackOverflowDemo
Public Foo As String
Public Event BeforeDoSomething()
Public Event AfterDoSomething()

Public Sub DoSomething()
End Sub

Klasa implementująca wyglądałaby tak:

'@Folder StackOverflowDemo
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
Jeśli jest to łatwiejsze do wizualizacji, projekt wygląda tak:]}

Rubberduck Code Explorer

Więc Class1 może definiować zdarzenia, ale Klasa implementująca nie ma możliwości ich implementacji - to jedna smutna rzecz w zdarzeniach i interfejsach w VBA, a wynika to z sposobu, w jaki zdarzenia praca w COM - zdarzenia same w sobie są zdefiniowane w ich własnym interfejsie "event provider"; więc "class interface" nie może ujawniać zdarzeń w COM (o ile to Rozumiem), a więc w VBA.


Więc zdarzenia muszą być zdefiniowane na klasie implementującej, aby miały jakikolwiek sens:

'@Folder StackOverflowDemo
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

Jeśli chcesz obsłużyć zdarzenia Class2 podczas uruchamiania kodu implementującego interfejs Class1, potrzebujesz pola na poziomie modułu WithEvents typu Class2 (implementacja), i zmienna obiektu poziomu procedury typu Class1 (interfejs):

'@Folder StackOverflowDemo
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

I tak mamy Class1 jako interfejs, Class2 jako implementację i Class3 jako jakiś kod klienta:

Rubberduck Code Explorer

...co prawdopodobnie podważa cel polimorfizmu, ponieważ klasa ta jest teraz powiązana z konkretną implementacją - ale to właśnie robią zdarzenia VBA: szczegółami implementacji, z natury powiązanymi z konkretną implementacją... as far as I wiedzieć.

 15
Author: Mathieu Guindon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:17:28

Ponieważ bounty już zmierza do odpowiedzi Pietera, Nie będę próbował odpowiedzieć na aspekt MVC pytania, ale zamiast pytania nagłówka. Odpowiedź jest taka, że Wydarzenia mają granice.

Trudno byłoby nazwać je "cukrem składniowym", ponieważ oszczędzają dużo kodu, ale w pewnym momencie, jeśli twój projekt stanie się zbyt skomplikowany, musisz ręcznie zaimplementować funkcjonalność.

Ale najpierw mechanizm wywołania zwrotnego (bo tym są zdarzenia)

ModMain, wejście / punkt wyjścia

Option Explicit

Sub Main()

    Dim oClient As Client
    Set oClient = New Client

    oClient.Run


End Sub

Klient

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, umowa interfejsu opisująca zdarzenia

Option Explicit

Public Sub SomethingHappened(ByVal vSomeParam As Variant)

End Sub

EventEmitter, Klasa serwera

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
Jak działa WithEvents? Jedną z odpowiedzi jest zajrzenie do biblioteki typów, tutaj jest jakiś IDL z Access (Microsoft Access 15.0 Object Library) definiujący zdarzenia, które mają zostać wywołane.
[
  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 */
};

Również z Access IDL jest tutaj Klasa opisująca, czym jest jej główny interfejs i czym jest interfejs zdarzeń, poszukaj source słowo kluczowe, a VBA potrzebuje dispinterface, więc zignoruj jedno z nich.

[
  uuid(7398AAFD-6527-48C7-95B7-BEABACD1CA3F),
  helpcontext(0x00003576)
]
coclass Form {
    [default] interface _Form3;
    [source] interface _FormEvents;
    [default, source] dispinterface _FormEvents2;
};

Więc to, co mówi klientowi, to to, że obsługuje mnie przez interfejs _Form3, ale jeśli chcesz odbierać zdarzenia, to ty, klient, musisz zaimplementować _FormEvents2. i wierzcie lub nie VBA po spełnieniu WithEvents uruchomi obiekt, który implementuje interfejs źródłowy dla Ciebie, a następnie przekieruje połączenia przychodzące do kodu obsługi VBA.Całkiem niesamowite.

Więc VBA generuje Klasa / obiekt implementujący interfejs źródłowy dla ciebie, ale questherer spełnił limity z mechanizmem polimorfizmu interfejsu i zdarzeniami. Więc moja rada jest, aby porzucić WithEvents i wdrożyć własny interfejs oddzwaniania i to jest to, co powyższy kod robi.

Aby uzyskać więcej informacji, polecam przeczytać książkę C++, która implementuje zdarzenia za pomocą interfejsów punktu połączenia, Twoje terminy wyszukiwania google są punkty połączenia withevents

Oto dobry cytat z 1994 roku podkreślający pracę VBA, którą wspomniałem powyżej

Po przejrzeniu poprzedniego kodu CSink przekonasz się, że przechwytywanie zdarzeń w Visual Basicu jest prawie zniechęcające. Wystarczy użyć słowa kluczowego WithEvents podczas deklarowania zmiennej obiektowej, a Visual Basic dynamicznie tworzy obiekt sink, który implementuje interfejs źródłowy obsługiwany przez podłączany obiekt. Następnie tworzysz instancję obiektu za pomocą Visual Basic New słowo kluczowe. Teraz, za każdym razem, gdy podłączony obiekt wywołuje metody interfejsu źródłowego, obiekt sink Visual Basic sprawdza, czy napisałeś jakikolwiek kod do obsługi połączenia.

EDIT: właściwie, rozważając mój przykładowy kod, mógłbyś uprościć i zlikwidować klasy interfejsu pośredniego, jeśli nie chcesz powielać sposobu, w jaki COM robi rzeczy i nie przeszkadza ci łączenie. Jest to przecież tylko gloryfikowany mechanizm oddzwaniania. Myślę, że to jest przykład, dlaczego COM dostał reputacja za zbyt skomplikowane.

 10
Author: S Meaden,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-08-23 22:36:37

Zaimplementowana Klasa

'   clsHUMAN

Public Property Let FirstName(strFirstName As String)
End Property

Klasa Pochodna

'   clsEmployee

Implements clsHUMAN

Event evtNameChange()

Private Property Let clsHUMAN_FirstName(RHS As String)
    UpdateHRDatabase
    RaiseEvent evtNameChange
End Property

Użycie w formie

Private WithEvents Employee As clsEmployee

Private Sub Employee_evtNameChange()
    Me.cmdSave.Enabled = True
End Sub
 2
Author: Nathan_Sav,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-12-07 17:43:08