Подтвердить что ты не робот

Можем ли мы одновременно использовать интерфейсы и события?

Я все еще пытаюсь обернуть голову тем, как интерфейсы и события работают вместе (если вообще?) в VBA. Я собираюсь создать большое приложение в Microsoft Access, и я хочу сделать его максимально гибким и расширяемым. Для этого я хочу использовать MVC, Интерфейсы (2) (3), Классы пользовательских коллекций, Поднятие событий с использованием пользовательских классов коллекций, поиск лучших способов централизовать и управлять событиями, вызванными элементами управления в форме, и некоторыми дополнительными шаблонами проектирования VBA,

Я ожидаю, что этот проект станет довольно волосатым, поэтому я хочу попытаться оценить ограничения и преимущества использования интерфейсов и событий вместе в VBA, поскольку они являются двумя основными способами (я думаю), чтобы действительно реализовать рыхлую связь в VBA.

Для начала существует этот вопрос об ошибке, возникающей при попытке использовать интерфейсы и события вместе в VBA. В ответе говорится: "По-видимому, событиям не разрешается проходить через класс интерфейса в конкретный класс, как вы хотите использовать" Реализаторы ".

Затем я нашел этот оператор в ответе на другом форуме: "В VBA6 мы можем только поднимать события, объявленные в интерфейсе по умолчанию класса, - мы не может поднять события, объявленные в реализованном интерфейсе".

С тех пор, как я все еще сталкиваюсь с интерфейсами и событиями (VBA - это первый язык, на котором у меня действительно была возможность опробовать ООП в реальном мире, я знаю дрожь), я не могу полностью работать в своем подумайте, что все это означает для совместного использования событий и интерфейсов в VBA. Это похоже на то, что вы можете использовать их одновременно, и это похоже на то, что вы не можете. (Например, я не уверен, что обозначается выше "интерфейсом класса по умолчанию" и "реализованным интерфейсом".)

Может кто-нибудь дать мне некоторые основные примеры реальных преимуществ и ограничений использования интерфейсов и событий вместе в VBA?

4b9b3361

Ответ 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