Is it possible to create and handle a custom Event

2020-05-16 06:55发布

I have a UserForm to which I added a property with the adequate Let and Get statements. I would like to have an event fire when the property changes. Event, RaiseEvent and Routine have been stated, all in the UserForm module. However, I can't find a way to assign the routine to the event.

For what I know, this is different from the usual Custom Events in Class Modules situation, because it is not declared in a Class Module whose Class I can instantiate in a regular module. All my searches provided examples to Custom Events in Class Modules or Built-In Events in UserForms, but no material on Custom Events in UserForms.

This actually made me wonder if it is at all possible to create Custom Events in UserForms and Private Subs to handle those Events. So is it possible to do it? And if so, what am I missing? How can I make Private Sub UFStatus_StatusChange() handle the event?

Any help would be apreciated!

So far, the code goes, all in the UserForm module:

Public Event StatusChange (old_status As Long, current_status As Long)

Dim old_status As Long

Private current_status As Long

Public Property Let UFStatus (RHS As Long)
    old_status = current_status
    current_status = RHS
    RaiseEvent StatusChange(old_status, current_status)

End Property

Private Sub UFStatus_StatusChange()
    MsgBox("Status changed from " & old_status & "to " & current_status)
End Sub

1条回答
男人必须洒脱
2楼-- · 2020-05-16 07:43

Yes, but you need to understand how VBA/COM events work first.

A bit of background...

Notice the dropdowns/comboboxes at the top of the VBE's code panes? The leftmost one is listing all available interfaces and event providers - the rightmost one is listing the available members and events exposed by whatever is selected in the leftmost dropdown.

So when you handle the Click event of some OkButton in the code-behind of UserForm1, the handler stub might look like this:

Private Sub OkButton_Click()

End Sub

The signature is constructed in a very particular way, always in the same manner, regardless of whether you're implementing an interface or handling an event:

Private Sub [Provider]_[MemberName]([Args])

That underscore matters. Whatever you do, DO NOT name an event (or interface member) with an identifier that contains an underscore. In the case of an event, you'll hit a compile error:

Compile error: Invalid event name

In the case of an interface, you'll also get a compile error, with the VBE complaining that interface members aren't implemented.

This is why everything is PascalCase and not Upper_Snake_Case in VB. Stick to the convention, avoid underscores in public member names.

If you're not sure what interfaces are and why I'm mentioning them in a post about events, lookup the Implements keyword, know that interfaces and events are very intimately related and work in a very similar fashion, and keep reading ;-)


The Provider

Any class can define events. A UserForm being a class, it can absolutely define events, yes. You define events exactly how you've done, using the Event keyword:

Public Event SomethingHappened(ByVal SomeArg As Long)

The class that defines the event is an event provider - it is the only class that is allowed to raise the events it defines.

You raise events using the RaiseEvent keyword, and provide the arguments:

Private Sub OnSomethingHappened()
    RaiseEvent SomethingHappened(42)
End Sub

When and why you raise events is entirely up to your imagination.


The Client

Consider the Click event of a CommandButton on a UserForm: the CommandButton class probably has a method that listens for Win32 messages involving the mouse, and when it decides to handle a left-button click, it raises its Click event, and something something and poof the OkButton_Click procedure runs. Right?

The part MSForms is automagically doing for you, is that when you add a CommandButton on the form, and name it OkButton, well this OkButton identifier essentially becomes a public field on the form, as if you had added a public, module-level variable:

Public OkButton As MSForms.CommandButton

Except, it actually looks like this:

Public WithEvents OkButton As MSForms.CommandButton

That WithEvents keyword makes the OkButton available in the left-side dropdown - OkButton becomes an event provider, and its Click event can be handled... in the form's code-behind.

The CommandButton class doesn't know or care about a handler for its Click event: the event provider is the OkButton object, and the client is the UserForm1 class you're implementing the handlers in.

In other words, the event provider, and the client, are two completely separate classes.


The catch is that WithEvents is only legal in a class module.

You can make your UserForm1 an event provider, but it cannot handle its own events.

Declare the events in your UserForm1 code-behind, and make sure you specify ByVal parameters for parameters that are meant to be read-only at the handler site - use ByRef when the handler can modify the value, e.g. for some Cancel As Boolean parameter that you can read when the handler returns:

Public Event StatusChange(ByVal oldStatus As Long, ByVal newStatus As Long)

Now add a class module, call it MyPresenter:

Option Explicit
Private WithEvents MyView As UserForm1

Private Sub Class_Initialize()
    Set MyView = New UserForm1
End Sub

Private Sub Class_Terminate()
    Set MyView = Nothing
End Sub

Public Sub ShowDialog()
    MyView.Show
End Sub

Select MyView from the leftmost dropdown; the rightmost dropdown should contain the StatusChange event - and selecting it should create a handler stub:

Private Sub MyView_StatusChange(ByVal oldStatus As Long, ByVal newStatus As Long)
    MsgBox "Status changed from " & oldStatus & " to " & newStatus & "!"
End Sub

Now, in the standard/procedural module where you were normally showing your form, instantiate that presenter class instead:

Public Sub Macro1()
    With New MyPresenter
        .ShowDialog
    End With
End Sub
查看更多
登录 后发表回答