Можем ли мы одновременно использовать интерфейсы и события?
Я все еще пытаюсь обернуть голову тем, как интерфейсы и события работают вместе (если вообще?) в VBA. Я собираюсь создать большое приложение в Microsoft Access, и я хочу сделать его максимально гибким и расширяемым. Для этого я хочу использовать MVC, Интерфейсы (2) (3), Классы пользовательских коллекций, Поднятие событий с использованием пользовательских классов коллекций, поиск лучших способов централизовать и управлять событиями, вызванными элементами управления в форме, и некоторыми дополнительными шаблонами проектирования VBA,
Я ожидаю, что этот проект станет довольно волосатым, поэтому я хочу попытаться оценить ограничения и преимущества использования интерфейсов и событий вместе в VBA, поскольку они являются двумя основными способами (я думаю), чтобы действительно реализовать рыхлую связь в VBA.
Для начала существует этот вопрос об ошибке, возникающей при попытке использовать интерфейсы и события вместе в VBA. В ответе говорится: "По-видимому, событиям не разрешается проходить через класс интерфейса в конкретный класс, как вы хотите использовать" Реализаторы ".
Затем я нашел этот оператор в ответе на другом форуме: "В VBA6 мы можем только поднимать события, объявленные в интерфейсе по умолчанию класса, - мы не может поднять события, объявленные в реализованном интерфейсе".
С тех пор, как я все еще сталкиваюсь с интерфейсами и событиями (VBA - это первый язык, на котором у меня действительно была возможность опробовать ООП в реальном мире, я знаю дрожь), я не могу полностью работать в своем подумайте, что все это означает для совместного использования событий и интерфейсов в VBA. Это похоже на то, что вы можете использовать их одновременно, и это похоже на то, что вы не можете. (Например, я не уверен, что обозначается выше "интерфейсом класса по умолчанию" и "реализованным интерфейсом".)
Может кто-нибудь дать мне некоторые основные примеры реальных преимуществ и ограничений использования интерфейсов и событий вместе в VBA?
Ответы
Ответ 1
Это идеальный прецедент для адаптера: внутренняя адаптация семантики для набора контрактов (интерфейсов) и раскрытие их как собственного внешнего API; возможно, согласно другому контракту.
Определить модули классов 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
и контроллер:
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
плюс стандартные модули Конструкторы:
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
и 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
Обратите внимание, что использование Шаблона адаптера в сочетании с программированием на интерфейсы приводит к очень гибкой структуре, где во время выполнения могут быть заменены различные реализации контроллера или представления. Каждое определение контроллера (в случае требуемых различных реализаций) использует разные экземпляры одной и той же реализации ViewAdapter, поскольку Dependency Injection используется для делегирования источника событий и командной строки для каждого экземпляра во время выполнения.
Тот же шаблон можно повторить, чтобы определить взаимосвязь между контроллером/презентатором/ViewModel и моделью, хотя реализация MVVM в COM может стать довольно утомительной. Я нашел MVP или MVC, как правило, лучше подходит для приложений на базе COM.
Производственная реализация также добавит правильную обработку ошибок (как минимум) в объеме, поддерживаемом VBA, на который я только намекнул с определением константы mModuleName в каждом модуле.
Ответ 2
Интерфейс, строго говоря, и только в терминах ООП, то, что объект предоставляет внешнему миру (т.е. его вызывающим/ "клиентам" ).
Итак, вы можете определить интерфейс в модуле класса, скажем ISomething
:
Option Explicit
Public Sub DoSomething()
End Sub
В другом модуле класса, скажем Class1
, вы можете реализовать интерфейс ISomething
:
Option Explicit
Implements ISomething
Private Sub ISomething_DoSomething()
'the actual implementation
End Sub
Когда вы это сделаете, обратите внимание, что Class1
ничего не раскрывает; единственный способ получить доступ к его методу DoSomething
- через интерфейс ISomething
, поэтому вызывающий код будет выглядеть так:
Dim something As ISomething
Set something = New Class1
something.DoSomething
Итак, ISomething
- это интерфейс здесь, и фактически выполняемый код реализуется в теле Class1
. Это один из основополагающих принципов ООП: полиморфизм - потому что у вас вполне может быть Class2
, который реализует ISomething
совершенно другим способом, но вызывающему вообще не понадобится заботиться: реализация абстрагируется за интерфейсом - и что красивая и освежающая вещь, чтобы увидеть в коде VBA!
Есть несколько вещей, которые нужно иметь в виду:
- Поля обычно рассматриваются как детали реализации: если интерфейс предоставляет публичные поля, классы реализации должны реализовать для него
Property Get
и Property Let
(или Set
, в зависимости от типа).
- События также рассматриваются как детали реализации. Поэтому они должны быть реализованы в классе, что
Implements
интерфейс, а не сам интерфейс.
Эта последняя точка довольно раздражает. Учитывая Class1
, это выглядит так:
'@Folder StackOverflowDemo
Public Foo As String
Public Event BeforeDoSomething()
Public Event AfterDoSomething()
Public Sub DoSomething()
End Sub
Класс реализации будет выглядеть следующим образом:
'@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
Если это проще визуализировать, проект выглядит следующим образом:
![Rubberduck Code Explorer]()
Итак, Class1
может определять события, но класс реализации не имеет возможности их реализовать - это одна печальная вещь о событиях и интерфейсах в VBA, и это связано с способом событий работа в COM - сами события определены в их собственном интерфейсе "поставщик событий"; поэтому "интерфейс класса" не может выставлять события в COM (насколько я понимаю), и поэтому в VBA.
Таким образом, события должны быть определены на классе реализации, чтобы иметь смысл:
'@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
Если вы хотите обрабатывать события Class2
при запуске кода, реализующего интерфейс Class1
, вам понадобится поле WithEvents
на уровне модуля типа Class2
(реализация) и уровень процедуры объектная переменная типа Class1
(интерфейс):
'@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
И поэтому мы имеем Class1
как интерфейс, Class2
в качестве реализации и Class3
как некоторый код клиента:
![Rubberduck Code Explorer]()
... который, возможно, побеждает цель полиморфизма, поскольку этот класс теперь связан с конкретной реализацией, - но затем, что делают события VBA: они являются деталями реализации, неотъемлемо связанными с конкретной реализацией... как далеко как я знаю.
Ответ 3
Потому что щедрость уже направляется на ответ Питера, я не буду пытаться ответить на вопрос MVC вопроса, но вместо этого заголовок вопроса. Ответ: "События имеют ограничения".
Было бы грубо называть их "синтаксическим сахаром", потому что они сохраняют много кода, но в какой-то момент, если ваш дизайн становится слишком сложным, вам придется выкинуть и вручную реализовать функциональность.
Но сначала механизм обратного вызова (для этого есть события)
modMain, точка входа/начала
Option Explicit
Sub Main()
Dim oClient As Client
Set oClient = New Client
oClient.Run
End Sub
Client
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, контракт интерфейса, который описывает события
Option Explicit
Public Sub SomethingHappened(ByVal vSomeParam As Variant)
End Sub
EventEmitter, класс сервера
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
Итак, как работает WithEvents? Один из ответов - посмотреть в библиотеке типов, вот несколько IDL из Access (Microsoft Access 15.0 Object Library
), определяющих события, которые будут подняты.
[
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 */
};
Также из Access IDL представлен класс, описывающий его основной интерфейс и интерфейс событий, ищите ключевое слово source
, а VBA нуждается в dispinterface
, поэтому игнорируйте один из них.
[
uuid(7398AAFD-6527-48C7-95B7-BEABACD1CA3F),
helpcontext(0x00003576)
]
coclass Form {
[default] interface _Form3;
[source] interface _FormEvents;
[default, source] dispinterface _FormEvents2;
};
Итак, что это говорит клиенту, это управлять мной через интерфейс _Form3, но если вы хотите получать события, то вы, клиент, должны реализовать _FormEvents2. И верьте этому или нет. VBA будет, когда WithEvents будет встречен, разворачивает объект, который реализует исходный интерфейс для вас, а затем направляет входящие вызовы на ваш код обработчика VBA. Довольно удивительно.
Итак, VBA генерирует класс/объект, реализующий исходный интерфейс для вас, но опросник встретил ограничения с механизмом и событиями полиморфизма интерфейса. Поэтому мой совет - отказаться от WithEvents и реализовать собственный интерфейс обратного вызова, и это то, что делает данный код выше.
Для получения дополнительной информации, я рекомендую прочитать книгу на С++, которая реализует события с использованием интерфейсов точки подключения, ваши поисковые термины go точки подключения withevents
Вот хорошая цитата из 1994 года, в которой подчеркивается работа VBA, о которой я упоминал выше
После пробоя через предыдущий код CSink вы обнаружите, что перехватывание событий в Visual Basic почти удручающе легко. Вы просто используете ключевое слово WithEvents при объявлении объектной переменной, а Visual Basic динамически создает объект-приемник, который реализует интерфейс источника, поддерживаемый подключаемым объектом. Затем вы создаете экземпляр объекта с помощью ключевого слова Visual Basic New. Теперь, когда подключаемый объект вызывает методы исходного интерфейса, объект раковины Visual Basic проверяет, написали ли вы какой-либо код для обработки вызова.
EDIT: На самом деле, обдумывая мой примерный код, вы можете упростить и отменить промежуточный интерфейс, если вы не хотите реплицировать то, как COM делает что-то, и вы не обеспокоены связью. Это, в конце концов, просто прославленный механизм обратного вызова. Я думаю, что это пример того, почему COM получил репутацию слишком сложного.
Ответ 4
Реализованный класс
' clsHUMAN
Public Property Let FirstName(strFirstName As String)
End Property
Производный класс
' clsEmployee
Implements clsHUMAN
Event evtNameChange()
Private Property Let clsHUMAN_FirstName(RHS As String)
UpdateHRDatabase
RaiseEvent evtNameChange
End Property
Использование в форме
Private WithEvents Employee As clsEmployee
Private Sub Employee_evtNameChange()
Me.cmdSave.Enabled = True
End Sub