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?
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.
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!
-
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 aProperty Let
(lubSet
, 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.
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:]}
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:
...co prawdopodobnie podważa cel polimorfizmu, ponieważ klasa ta jest teraz powiązana z konkretną implementacją - ale to właśnie robią zdarzenia VBA: są szczegółami implementacji, z natury powiązanymi z konkretną implementacją... as far as I wiedzieć.
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.
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
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