I am trying to instantiate an existing form (frmVisibleForm) in my project from within a custom class module (clsMBox) and manipulate its properties from there too. I want to be able to use events from the form.
What I expect to happen:
The Form frmVisibleForm is instantiated but invisible
The Form gets set to Modal
The Form gets set to Visible
The Form gets Focus
What happens:
Nothing. No form shows up, no error messages, no prompts, nothing happens at all when running the test module´s function. Its my first time trying out custom classes in access so maybe I made some fundamental error but I can't figure out why it doesn´t work the way I thought it would. Appreciate any help.
Here is the code I have so far:
The Form (frmVisibleForm):
Option Compare Database
Option Explicit
Public Event DataInput(InputValue As String)
(No actual events thus far)
The Custom Class Module (clsMBox):
Option Compare Database
Option Explicit
Dim WithEvents cls_frmVisibleForm As Form_frmVisibleForm
Private Sub InstantiateForm()
Set cls_frmVisibleForm = New Form_frmVisibleForm
With cls_frmVisibleForm
.Modal = True
.Visible = True
.SetFocus
End With
End Sub
The Module I try to test it from (mdlTestMBox):
Option Compare Database
Option Explicit
Public Function ClassTest()
Dim mbox As clsMBox
Set mbox = New clsMBox
End Function
I guess you need to either make InstantiateForm a public method, and then call that, or rename it to initialise:
Private Sub Class_Initialize()
Static cls_frmVisibleForm As Access.Form
Set cls_frmVisibleForm = New Form_frmVisibleForm
With cls_frmVisibleForm
.Modal = True
.Visible = True
.Move 0, 0
End With
End Sub
To open and close an instance of the form:
Public Function ClassTest()
Static mbox As clsMBox
Set mbox = New clsMBox
Stop
DoCmd.Close acForm, Forms(0).Name
End Function
Related
tl;dr Is there a way to enable events for built-in objects without coupling the event to the original object's parent, assuming the event interacts with the parent?
Disclaimer 1: I don't have access to MS Office on my home machine and therefore type all code from memory. I'm sorry if something's incorrect.
Disclaimer 2: This post is incredibly lengthy because I've been trying to figure out how to do this process for several years but never quite hit the correct Google terms to figure it out. I do a lot of explaining in the hopes that it might help someone with the same issues.
The Original Problem
I've had this longstanding issue of having Userforms with near-identical event handling but no way to compact the code into a generic solution. For example, let's say I have a Userform with a bunch of Command Buttons that all do the same thing when clicked. Traditionally, you would have to include something like the following in Userform1
Private Sub CommandButton1_Click()
Me.DoSomething CommandButton1.Name
End Sub
Private Sub CommandButton2_Click()
Me.DoSomething CommandButton2.Name
End Sub
'...a bunch more of these...'
Private Sub CommandButtonN_Click()
Me.DoSomething CommandButtonN.Name
End Sub
This is annoying to setup and hurts readability for a large number of buttons.
The Naive Solution
I recently discovered that wrapper classes can be utilized to make a generic WithEvents handler for built-in objects. Applying this to our previous example, we create an EventCommandButton.cls Class with the following code
Private WithEvents mCommandButton as MSForms.CommandButton
Private Sub mCommandButton_Click()
mCommandButton.Parent.DoSomething(mCommandButton.Name)
End Sub
Property Get CommandButton() as MSForms.CommandButton
Set CommandButton = mCommandButton
End Property
Property Set CommandButton(cmdBtn as MSForms.CommandButton)
Set mCommandButton = cmdBtn
End Property
And Userform1 turns into
Private EventCommandButtons() as New EventCommandButton
Private Sub Userform1_Initialize()
For Each ctl in Me.Controls
If TypeName(ctl) = "CommandButton" Then
i = i + 1
ReDim Preserve EventCommandButtons(1 to i)
Set EventCommandButtons(i).CommandButton = ctl
End If
Next
End Sub
This approach saves space and looks comparatively nice, but it presents (at least) 2 major issues:
All of Userform1's control events are no longer housed in its own code
Our EventCommandButton requires a specific procedure (DoSomething(str)) to exist in its parent or else we'll get an error.
A Slight Refinement
The solution I'm currently implementing is to take a more intuitive approach that returns control of the event handling back to where you'd expect it to be. In EventCommandButton.cls we add a new property to specify where we expect to find the return code:
Private mCommandButton as MSForms.CommandButton
Private mCallback as Object
Private Sub mCommandButton_Click()
'Some error handling should be here to check that mCallback is set
mCallback.EventCommandButton_Click(mCommandButton)
End Sub
Property Get Callback() as Object
Set Callback = mCallback
End Property
Property Set Callback(ParentObject as Object)
'Let's not assume it's always the .Parent
Set mCallback = ParentObject
End Property
Property Get CommandButton() as MSForms.CommandButton
Set CommandButton = mCommandButton
End Property
Property Set CommandButton(cmdBtn as MSForms.CommandButton)
Set mCommandButton = cmdBtn
End Property
And in Userform1
Private EventCommandButtons() as New EventCommandButton
Public Sub EventCommandButton_Click(cmdBtn as MSForms.CommandButton)
Me.DoSomething cmdBtn.name
End Sub
Private Sub Userform1_Initialize()
For Each ctl in Me.Controls
If TypeName(ctl) = "CommandButton" Then
i = i + 1
ReDim Preserve EventCommandButtons(1 to i)
Set EventCommandButtons(i).CommandButton = ctl
Set EventCommandButtons(i).Callback = Me 'Set new property
End If
Next
End Sub
This approach feels close to the intuitive solution of the original problem (with some extra steps involved) and resolves issue #1 from the previous, but we still have issues:
There's still coupling between the Class and Userform, now requiring that each parent object must have corresponding pseudo-event procedures of the form Public Sub [ClassName]_[EventName]([OriginalObject], Optional [EventParams]), which isn't intuitive and looks weird amongst the sea of Private Event Subs.
The coupling now depends on the class name, which may not always be ideal. Renaming the class will require editing the events to reflect that.
For the wrapper to be "complete", it must include all events and error handling to ignore the ones that aren't setup on the Parent side. At some point I'd think having all these On Error GoTo EoF statements in each class instance will have a performance impact.
The Question
Is there a way that this process can be further improved to reduce the coupling between (in this case) the Class and Form code? With VBIDE we could detect the classname and generate the pseudo-events, but without VBIDE access it seems like it requires some upkeeping and instruction to properly utilize the class.
In Python (and I'm sure other languages), you could just pass a reference to a function to direct the event returns; however, VBA doesn't seem to support this.
If you can pass the method name from the parent as a string you could use something like CallByName mCallback, vbMethod, mProcName, mCommandButton from within the class instance, to call the method mProcName on the parent, passing the clicked-on button.
For example:
Event class (properties changed to public fields for brevity)
Option Explicit
Public WithEvents mCommandButton As MSForms.CommandButton
Public mCallback As Object '<< object on which the callback method is to be called
Public mProcName As String '<< name of the callback method
Private Sub mCommandButton_Click()
CallByName mCallback, mProcName, VbMethod, mCommandButton
End Sub
Form code:
Private EventCommandButtons As Collection
Public Sub ButtonClick(cmdBtn As MSForms.CommandButton)
MsgBox "clicked on button " & cmdBtn.Caption
End Sub
Private Sub Userform_Initialize()
Dim ctl As Object
Set EventCommandButtons = New Collection
For Each ctl In Me.Controls
If TypeName(ctl) = "CommandButton" Then
EventCommandButtons.Add NewClickHandler(ctl)
End If
Next
End Sub
Function NewClickHandler(btn As Object) As EventCommandButton
Set NewClickHandler = New EventCommandButton
Set NewClickHandler.mCommandButton = btn
Set NewClickHandler.mCallback = Me
NewClickHandler.mProcName = "ButtonClick"
End Function
I want to create a vbModeless dynamic user form during run-time. The user form has just one button, that's it. The form works fine using vbModal but unfortunately with vbModeless I can't get the click event for the button to work. Clicking the button does not call the event. I'm using the following steps/code:
Created an empty user form named UserForm1
Created a module named Modul1 with the following code:
Sub CreateFormControls()
'Create Command Button
Dim Button01 As MSForms.CommandButton
Set Button01 = UserForm1.Controls.Add("Forms.CommandButton.1", "dynButton01", False)
Button01.Visible = True
'Reference click event
Dim ClickEvents As New Class1
Set ClickEvents.ButtonEvent = Button01
'Show User Form
UserForm1.Show vbModeless '-> THIS DOES NOT WORK, only vbmodal works
End Sub
Created a class module named Class1 with the following code:
Public WithEvents ButtonEvent As MSForms.CommandButton
Private Sub ButtonEvent_Click()
MsgBox "Test"
Unload UserForm1
End Sub
Is there a way to get this working with vbModeless or is there a different work around?
Note: I haven't used dynamic forms very much yet, and I copied/modified the shown implementation using an existing code snippet without completely understanding how the button object references the click event e.g. why a separate class is needed and I can't do it within the procedure in Modul1. I assume within lies the reason why it doesn't work opening the form non-modal. A little light on this issue would be appreciated as well.
ClickEvents should be declared at the module level...
Option Explicit
Dim ClickEvents As New Class1 'declared at the module level
Sub CreateFormControls()
'etc
'
'
End Sub
I've been building testable MVC logic for my Access database using RubberDuck's answer to Best way to test a MS Access Application? but I'm stuck with the custom event handling. I can't figure out why the OnCreate event isn't firing.
Form_CreateStudents:
Option Compare Database
Private ctrl As ctrCreateStudent
Public Event OnCreate()
Private Sub btnCreate_Click()
Set ctrl = New ctrCreateStudent
ctrl.Run
RaiseEvent OnCreate
End Sub
Class module ctrCreateStudent:
Private WithEvents frm As [Form_Create Students]
Public Sub Run()
MsgBox "run called"
Set frm = New [Form_Create Students]
End Sub
Public Sub frm_OnCreate()
MsgBox "frm_oncreate event called"
End Sub
Run is being called, but frm_OnCreate is just ignored. I'm relatively new to VBA, what am I missing here?
Quite simple:
frm is a New [Form_Create Students], not the one calling it.
This new form doesn't raise the OnCreate event. In fact, this new form is not even visible, because you haven't set frm.Visible = True
If you want to set it to the form that just called Run, pass it:
On the form:
Private ctrl As ctrCreateStudent
Public Event OnCreate()
Private Sub btnCreate_Click()
Set ctrl = New ctrCreateStudent
ctrl.Run Me
RaiseEvent OnCreate
End Sub
On the class:
Private WithEvents frm As [Form_Create Students]
Public Sub Run(parentForm As [Form_Create Students])
MsgBox "run called"
Set frm = parentForm
End Sub
Public Sub frm_OnCreate()
MsgBox "frm_oncreate event called"
End Sub
A strong warning, though: this code contains a reference loop, and thus a memory leak.
The form has a reference to the class, and the class has a reference to the form, so neither will ever get destroyed. Every time you close and open the form, a new form and class object will get created, and none of them will ever get destroyed.
When closing the form, it turns invisible and looks gone, but it's still there and using memory.
There are many ways to work around this, but an easy one is:
In the class:
Public Sub frm_Close()
Set frm = Nothing 'Release form object, break reference loop
End Sub
And make sure the Form's On Close property is set to "[Event Procedure]" so the close event gets raised.
I am trying to make class that would create a button. The button should have a macro assigned to it. The macro is a function of the class.
The code of the class module is the following:
'Class Module: btnClass
Option Explicit
Dim btn As Button
Function addButton()
'Adding a button
Set btn = ActiveSheet.Buttons.Add( _
Range("A1").Left, _
Range("A1").Top, _
Range("A1").Width, _
Range("A1").Height)
With btn
'Assigning a function
.OnAction = Me.onClickAction
.Caption = "Button"
End With
End Function
Function onClickAction()
MsgBox ("Click")
End Function
The code of the main macro is the following:
'Module
Option Explicit
Sub main()
Dim btnInstance As btnClass
Set btnInstance = New btnClass
'Calling a function of the instance that creates a button
Call btnInstance.addButton
End Sub
The code above creates a button successfully. However, the function assigned to the button is run immediately (right after the button is created, not when I click on it), and only once (when you click on the button later, nothing happens).
Is there a way in VBA to implement the required functionality using class modules (want to create a class that does not rely on the outside functions)?
To expand on Rory's comment, your class needs something like this:
Public WithEvents Button As CommandButton
Private Sub Class_Initialize()
Set Me.Button = Sheet1.OLEObjects("Thebutton").Object
End Sub
Private Sub Button_Click()
MsgBox "Foo"
End Sub
Then in a normal module create a public instance of the class so it stays in memmory:
Public myButt As ButtonClass
Public Sub AddEvent()
Set myButt = New ButtonClass
End Sub
Note that the click event will only be handled as long as the instance of the class remains in memory. If you close the workbook and open it again the event will no longer be handled.
Edit: I forgot to mention, you need to set a reference to Microsoft Forms in order to declare a variable of type CommandButton.
In a VBA project of mine I am/will be using a series of reasonably complex userforms, many of which are visually identical but have different subroutines attached to the buttons. As a result I'm not overly keen on the idea of duplicating them multiple times in order to get different functionality out of the same layout. Is it possible to have a userform detect which subroutine called it and use this in flow control? I would like to be able to do something like this:
Private Sub UserForm_Initialize()
If [the sub that called the userform is called "foo"] then
Call fooSub
else
Call barSub
End If
End Sub
My backup plan is to have the calling subroutine set a global variable flag and have the userform check that, but that seems like a rather crude and clumsy solution.
Thanks everyone,
Louis
You can use the tag property of the form. Load the form, set the property, then show the form:
Sub PassCallerToForm()
Load UserForm1
UserForm1.Tag = "foo"
UserForm1.Show
End Sub
Now that the property is set, you can determine what to do in the form:
Private Sub UserForm_Activate()
If Me.Tag = "foo" Then
Call fooSub
Else
Call barSub
End If
End Sub
You can also use public variables:
' in userform
Public Caller As String
Private Sub UserForm_Click()
MsgBox Caller
Caller = Now()
Me.Hide
End Sub
' in caller
Sub callUF()
Dim frm As New UserForm1
frm.Caller = "Test Caller"
frm.Show
MsgBox frm.Caller ' valid after Me.Hide
Set frm = Nothing
End Sub
Personally, I would not have one userform doing two disparate activities. The code would get hard to read pretty quickly, I think. Copying the layout of a userform is pretty trivial.
To copy a userform: Open a blank workbook. In the Project Explorer, drag the userform to the new workbook. Rename the userform in the new workbook. Now drag it back to the original workbook. Change the code in the userform copy.
If you absolutely don't want separate userforms, I recommend setting up a property of the userform. Userforms are just classes except they have a user interface component. In the userform module
Private mbIsFoo As Boolean
Public Property Let IsFoo(ByVal bIsFoo As Boolean): mbIsFoo = bIsFoo: End Property
Public Property Get IsFoo() As Boolean: IsFoo = mbIsFoo: End Property
Public Sub Initialize()
If Me.IsFoo Then
FooSub
Else
BarSub
End If
End Sub
I always write my own Initialize procedure. In a standard module:
Sub OpenForm()
Dim ufFooBar As UFooBar
Set ufFooBar = New UFooBar
ufFooBar.IsFoo = True
ufFooBar.Initialize
ufFooBar.Show
End Sub