Handling custom events using modeless form & user defined classes - vba

I'm trying to display the progress of various routines on a modeless form, by having those routines raise custom events detailing their progress. The form should handle those events to display appropriate information.
The problem is that although RaiseEvent is called, the event handlers don't then do anything.
The intended result of the following code is the two debug.prints would be called whenever an event is raised by triggerTest.
The only success I've had is with raising the error within the userform, by CommandButton1_Click in the following code. The form's event handler then kicks in (rather redundantly, but perhaps that means I'm on the right path).
Thanks
Event class clsChangeProgressTrigger
Option Explicit
Public Enum geProgressStatus
geProgressStatusComplete = -1
geProgressStatusRestart = -2
End Enum
Public Event ChangeProgress(dProgress As Double, sProcedure As String)
'
Public Sub Update(dProgress As Double, sProcedure As String)
RaiseEvent ChangeProgress(dProgress, sProcedure)
End Sub
Public Sub Complete(sProcedure As String)
RaiseEvent ChangeProgress(geProgressStatusComplete, sProcedure)
End Sub
Public Sub Restart(sProcedure As String)
RaiseEvent ChangeProgress(geProgressStatusRestart, sProcedure)
End Sub
User form frmOutput
Option Explicit
Private WithEvents mProgressTrigger As clsChangeProgressTrigger
'
Private Sub CommandButton1_Click()
Call mProgressTrigger.Update(12.34, "SomeValue")
End Sub
Private Sub CommandButton2_Click()
Call modZTest.triggerTest
End Sub
Private Sub UserForm_Initialize()
Set mProgressTrigger = New clsChangeProgressTrigger
End Sub
Private Sub mProgressTrigger_ChangeProgress(dProgress As Double, sProcedure As String)
Debug.Print "Form Event Handled"
End Sub
Event test class clsEventTest
Option Explicit
Private WithEvents mProgressTrigger As clsChangeProgressTrigger
'
Private Sub mProgressTrigger_ChangeProgress(dProgress As Double, sProcedure As String)
Debug.Print "Class Event Handled"
End Sub
Private Sub Class_Initialize()
Set mProgressTrigger = New clsChangeProgressTrigger
End Sub
Test wrapper in public module modZTest
Public Sub triggerTest()
Application.EnableEvents = True
' Instantiate Trigger class for this routine
' Dim cChangeProgressTrigger As clsChangeProgressTrigger
Set gChangeProgressTrigger = New clsChangeProgressTrigger
' Instantiate Event Test class, which should handle raised event
Dim cEventTest As clsEventTest
Set cEventTest = New clsEventTest
' Instantiate user form, which should handle raised event
Set gfrmOutput = New frmOutput ' Modeless form, gfrmOutput has global scope
gfrmOutput.Show
Stop
' Raise an event
Call gChangeProgressTrigger.Complete("SomeValue")
' Tidy Up
Set gfrmOutput = Nothing
Set gChangeProgressTrigger = Nothing
Set cEventTest = Nothing
End Sub

Thanks Dee, that helped me along to the solution.
With this declared as global scope:
Public gChangeProgressTrigger As clsChangeProgressTrigger
I had to change the class / form level initialisations as follows:
Private Sub UserForm_Initialize()
' Set mProgressTrigger = New clsChangeProgressTrigger ' Old
Set mProgressTrigger = gChangeProgressTrigger ' New
End Sub
and
Private Sub Class_Initialize()
' Set mProgressTrigger = New clsChangeProgressTrigger ' Old
Set mProgressTrigger = gChangeProgressTrigger ' New
End Sub
Then the event handlers fired as desired.

Related

How to pass a form as a parameter with events

My function currently accepts a form and saves it to a variable so that the form's events are caught in this module. Thing is, the form is strongly typed - every form needs its own function. It's a lot of duplicate code, so I tried making it generic by typing the variable as Access.Form, but the events stopped firing.
I think it might be because now the form is only defined at runtime. Whatever the reason, does anyone know if it's possible to pass a form anonymously without losing its events?
Current code
Private WithEvents frm As [Form_Create]
Public Sub Run(parentForm As [Form_Create])
Set model = New mdlKita
Set frm = parentForm
End Sub
// then I can do things like
Public Sub frm_OnCreate()
Current code updated to be generic - events don't fire
Private WithEvents frm As Access.Form
Public Sub Run(parentForm As Access.Form)
Set model = New mdlKita
Set frm = parentForm
End Sub
// this never fires
Public Sub frm_OnCreate()
Inside the form in case someone's interested
Private ctrl As ctrCreate
Private Sub btnContinue_Click()
Set ctrl = New ctrCreate
ctrl.Run Me
RaiseEvent OnCreate
End Sub
I guess you miss to Initialize and Terminate a helper class - here for textboxes:
Option Explicit
' Helper class for form Palette for event handling of textboxes.
' 2017-04-19. Gustav Brock, Cactus Data ApS, CPH.
' Version 1.0.0
' License: MIT.
' *
Private Const EventProcedure As String = "[Event Procedure]"
Private WithEvents ClassTextBox As Access.TextBox
Public Sub Initialize(ByRef TextBox As Access.TextBox)
Set ClassTextBox = TextBox
ClassTextBox.OnClick = EventProcedure
End Sub
Public Sub Terminate()
Set ClassTextBox = Nothing
End Sub
Private Sub ClassTextBox_Click()
' Select full content.
ClassTextBox.SelStart = 0
ClassTextBox.SelLength = Len(ClassTextBox.Value)
' Display the clicked value.
ClassTextBox.Parent!CopyClicked.Value = ClassTextBox.Value
' Copy the clicked value to the clipboard.
DoCmd.RunCommand acCmdCopy
End Sub
taken from my tiny project VBA.ModernTheme

Raise Event Vb.net from worker Thread

I'm looking at a console app for vb.net. I'm trying to get a worker thread to raise an event to the main thread to display data on the screen (the word "HIT" everytime the worker thread completes a cycle). My code is below.
I'm not sure why but the main thread's Private Sub CounterClass_GivingUpdate() Handles _counter.AboutToDistributeNewupdate isn't executing.
Imports System.Threading
Module Module1
Private WithEvents _counter As CounterClass
Private trd As Thread
Sub Main()
While True
Dim s As String = Console.ReadLine()
Dim started As Boolean
Select Case s
Case "status"
WriteStatusToConsole("You typed status")
Case "startcounter"
If started = False Then
starttheThread()
started = True
WriteStatusToConsole("You Have Started The Timer")
Else
WriteStatusToConsole("YOU HAVE ALREADY STARTED THE TIMER!!!")
End If
End Select
End While
End Sub
Private Sub CounterClass_GivingUpdate() Handles _counter.AboutToDistributeNewupdate
WriteStatusToConsole("Hit")
End Sub
Private Sub starttheThread()
Dim c As New CounterClass
trd = New Thread(AddressOf c.startProcess)
trd.Start()
End Sub
Sub WriteStatusToConsole(ByVal stringToDisplay As String)
Console.WriteLine(stringToDisplay)
End Sub
End Module
Public Class CounterClass
Public Event AboutToDistributeNewupdate()
Public Sub sendStatusUpdateEvent(ByVal updatestatus As String)
RaiseEvent AboutToDistributeNewupdate()
End Sub
Public Sub startProcess()
Dim i As Int64
Do
Thread.Sleep(1000)
i = i + 1
sendStatusUpdateEvent(i.ToString)
Loop
End Sub
End Class
Your CounterClass_GivingUpdate() only handles the _counter variable's event (the variable that you do not use!). Every time you declare a new CounterClass it has its own instance of the event that it raises.
You know have two options:
Option 1
Subscribe to the event for each new CounterClass instance you create. Meaning you must use the AddHandler statement every time you create a new instance of your class:
Private Sub starttheThread()
Dim c As New CounterClass
AddHandler c.AboutToDistributeNewupdate, AddressOf CounterClass_GivingUpdate
trd = New Thread(AddressOf c.startProcess)
trd.Start()
End Sub
Option 2
Mark the event as Shared to make it available without needing to create an instance of the class. For this you must also change how you subscribe to the event, by subscribing to it in your method Main():
Sub Main()
AddHandler CounterClass.AboutToDistributeNewupdate, AddressOf CounterClass_GivingUpdate
...the rest of your code...
End Sub
Private Sub CounterClass_GivingUpdate() 'No "Handles"-statement here.
WriteStatusToConsole("Hit")
End Sub
Public Class CounterClass
Public Shared Event AboutToDistributeNewupdate() 'Added the "Shared" keyword.
...the rest of your code...
End Class

VB.NET call inside thread function

Public Class ThreadWithState
' State information used in the task.
Private locationx As Integer
Private locationy As Integer
' The constructor obtains the state information.
Public Sub New(x As Integer, y As Integer)
locationx = x
locationy = y
End Sub
' The thread procedure performs the task, such as formatting
' and printing a document.
Dim win As New VBSoftKeyboard.KeyBoardForm
Public Sub ThreadProc()
'MessageBox.Show(locationy)
Console.WriteLine(locationx, locationy)
win.FormBorderStyle = Windows.Forms.FormBorderStyle.None
win.Location = New Point(locationx, locationy)
win.StartPosition = FormStartPosition.Manual
Application.Run(win)
End Sub
Public Sub HideWin()
win.Hide()
End Sub
Public Sub ShowWin()
win.Show()
End Sub
End Class
Dim tws As New ThreadWithState(1000, 0) 'show the keyboard form location
Private Sub SimpleButton1_Click_1(sender As Object, e As EventArgs) Handles SimpleButton1.Click
Dim t As New Thread(New ThreadStart(AddressOf tws.ThreadProc))
t.Start()
tws.HideWin() //PROBLEM HERE ! how to hide the form ? and when i want to use i can show the form
End Sub
I use a thread to create a form, and then i m trying to hide the form first, when want to use the form i only call the ShowWin() function. t as thread to start the thread and create the form, how to call the t thread HideWin() function?
How to use created thread to call the others function ?

Creating A Custom Event With VBA

I'm trying struggling to understand how to create a custom event using class modules in VBA.
I've put together the simple following example. You put a value in A1 and B1 and then re activate the sheet to calculate the sum of the two and then I hoped an event would fire to warn of calculation, but nothing happens.
I'd be very grateful for any help solving this example.
Class module cCalc:
Dim m_wks As Worksheet
Public Event BeforeCalc()
Property Set Worksheet(wks As Worksheet)
Set m_wks = wks
End Property
Public Sub Calc()
Dim dVal1 As Double
Dim dVal2 As Double
With m_wks
dVal1 = .Range("A1").Value
dVal2 = .Range("B1").Value
RaiseEvent BeforeCalc
.Range("C1").Value = dVal1 + dVal2
End With
End Sub
In a module mGlobal:
Public gCalc As cCalc
In the code behind Sheet1:
Private WithEvents calcEvent As cCalc
Private Sub calcEvent_BeforeCalc()
MsgBox "About to Calc!", vbInformation
End Sub
Private Sub Worksheet_Activate()
Set gCalc = New cCalc
Set gCalc.Worksheet = ActiveSheet
gCalc.Calc
End Sub
You can't declare event-driven classes in modules. You'll need to set the cCalc reference in gModule equal to the object you declared WithEvents in Sheet1. Change your code in Sheet1 to what i wrote below and it will work:
Private WithEvents calcEvent As cCalc
Private Sub calcEvent_BeforeCalc()
MsgBox "About to Calc!", vbInformation
End Sub
Private Sub Worksheet_Activate()
Set calcEvent = New cCalc 'Instantiate the WithEvents object above
Set mGlobal.gCalc = calcEvent 'Set the object declared in gModule
Set mGlobal.gCalc.Worksheet = ActiveSheet
mGlobal.gCalc.Calc
End Sub
Note that this is using the variable you put in gModule... The event that is actually called is still calcEvent_BeforeCalc(), which is good as this way you can have n number of objects defined in gModule that would all fire off the same event code when the event is triggered.
To simplify the code, you could always just write:
Private Sub Worksheet_Activate()
Set calcEvent = New cCalc
Set calcEvent.Worksheet = ActiveSheet
calcEvent.Calc
End Sub

Programmatically adding a commandbutton to a userform

In excel vba I have added a commandbutton to userform... like below
Set ctrl = Me.Controls.Add( _
bstrProgID:="Forms.CommandButton.1", _
Name:="CommandButton1", Visible:=True)
Now I wanted to know how would I tell it what to do when it is clicked?
This is one of those techniques that vba will let you do, but you probably shouldn't. For all the same reasons you shouldn't use code that alters your code.
That said, here is how to do what you want. First insert a class module and name it DynBtn, then paste this code into it:
Private WithEvents mobjBtn As MSForms.CommandButton
Private msOnAction As String
''// This has to be generic or call by name won't be able to find the methods
''// in your form.
Private mobjParent As Object
Public Property Get Object() As MSForms.CommandButton
Set Object = mobjBtn
End Property
Public Function Load(ByVal parentFormName As Object, ByVal btn As MSForms.CommandButton, ByVal procedure As String) As DynBtn
Set mobjParent = parentFormName
Set mobjBtn = btn
msOnAction = procedure
Set Load = Me
End Function
Private Sub Class_Terminate()
Set mobjParent = Nothing
Set mobjBtn = Nothing
End Sub
Private Sub mobjBtn_Click()
CallByName mobjParent, msOnAction, VbMethod
End Sub
Now to use this in your form, create a blank user form and paste this code into it:
Private Const mcsCmdBtn As String = "Forms.CommandButton.1"
Private mBtn() As DynBtn
Private Sub UserForm_Initialize()
Dim i As Long
ReDim mBtn(1) As DynBtn
For i = 0 To UBound(mBtn)
Set mBtn(i) = New DynBtn
Next
''// One Liner
mBtn(0).Load(Me, Me.Controls.Add(mcsCmdBtn, "Btn1", True), "DoSomething").Object.Caption = "Test 1"
''// Or using with block.
With mBtn(1).Load(Me, Me.Controls.Add(mcsCmdBtn, "Btn2", True), "DoSomethingElse").Object
.Caption = "Test 2"
.Top = .Height + 10
End With
End Sub
Public Sub DoSomething()
MsgBox "It Worked!"
End Sub
Public Sub DoSomethingElse()
MsgBox "Yay!"
End Sub
Private Sub UserForm_Terminate()
Erase mBtn
End Sub