Event handling class doesn't fire unless I break userform initialization - vba

This is a follow-up to the following question:
Can't set Userform.KeyPreview to true
To recap: the goal is to build a form with some command buttons and a frame containing check boxes. The check boxes are dynamically populated at userform_initialize in the frame so the user can scroll through them. My problem was with keyboard shortcuts. It wasn't possible to brute force write KeyDown handlers for each of the checkboxes because I don't know which ones will exist. Unfortunately, Excel doesn't support KeyPreview so I had to mock up my own version. Thank you to #UGP for giving me promising avenues that seem to work, but not quite...
First, this is my class module called clsReasonPickKP. I create a new instance for each checkbox to listen for KeyDown events:
Option Explicit
Dim WithEvents vChkBx As MSForms.CheckBox
Friend Sub initializeListener(cControl As control)
Set vChkBx = cControl
End Sub
Private Sub vChkBx_KeyDown(ByVal keyCode As MSForms.ReturnInteger, ByVal shift As Integer)
frm2.keyChooser keyCode
End Sub
The line frm2.keyChooser keyCode launches a quick sub located in the userform code module. Code below:
Public Sub keyChooser(ByVal keyCode As MSForms.ReturnInteger)
Select Case keyCode
Case vbKeyEscape: cancelBtn_Click
Case vbKeyReturn: completeDecision_Click
Case vbKeyN: customizeNote_Click
Case vbKeyS: resetDecisionNote_Click
Case vbKeyR: chkRefGrnds_Click
End Select
End Sub
I've copied the relevant part of the UserForm_Initialize sub below. The loop creates the checkboxes and an event listener for each.
Sub UserForm_Initialize()
Dim x As Long, maxWidth as Long
Dim cControl As control
Dim keyPreviewCollection As New Collection
Dim keyPreviewer As clsReasonPickKP
For x = 1 To dTbl.Rows.Count - 1
Set cControl = chkBoxFrame.Controls.Add("Forms.CheckBox.1", "chkBox" & x, True)
With cControl
.AutoSize = True
.WordWrap = False
.Left = 10
.Top = 16 * x - 12
.Caption = dTbl(x, 1).Value
If .Width > maxWidth Then maxWidth = .Width
End With
Set keyPreviewer = New clsReasonPickKP
keyPreviewer.initializeListener cControl
keyPreviewCollection.Add keyPreviewer
Next x
'Additional initialization code here
End Sub
The odd thing is that unless I break code some time after keyPreviewCollection.Add keyPreviewer, the listener doesn't seem to handle the event. For example, if I set a break point at Next x or for x > 1 and then complete initialization, then when the form is finished initializing and appears the listener calls keyChooser and all is well; if I don't break code like that, it doesn't trap the event or call the sub, etc.
To trouble-shoot, I've tried not adding keyPreviewer to the collection, and then the listener also doesn't work, no matter if or when I break. It seems adding the object to the collection, and being in code break mode after adding it to the collection, somehow makes the listener trap the event.
Also interesting, if I put a breakpoint in the vChkBx_KeyDown module, it breaks when the event is raised (assuming an appropriate break as described above). After I then run the code, however, it stops handling the KeyDown event when its raised.
In case it helps, I'm currently working in Excel 2010.
Does anybody have any idea what's going on? Any idea how to solve this, even with a different code approach?
Thank you as always for everybody's help.

It turns out that the problem was so simple and right in front of my eyes. I just had to make the keyPreviewer and keyPreviewCollection variables public in my userform code module.
That still doesn't answer why breaking code execution after adding the object to the collection made VBA treat it as public, but just happy that it all works.

DoEvents might be the ticket. See the article below:
https://www.automateexcel.com/vba/doevents/

Related

One color change code for all ToggleButtons in a Userforms' Multipage

I have around 100 ToggleButtons.
I would like:
If .value = true then
togglebuttons.BackColor = vbRed
Else
= vbGreen
I can write the code for every one, but is there a way to create a group or class so that color change code would be applied to all of them?
-Excel365
Here's an example that creates a new class in order to handle multiple toggle buttons using one event handler. Note that it assumes that the first page of your multipage control contains your toggle buttons. Change the page reference accordingly.
First insert a new class module (Insert >> Class Module), and name it clsToggleButton.
Then copy and paste the following code into the code module for your new class . . .
Option Explicit
Public WithEvents toggleButton As MSForms.toggleButton
Private Sub toggleButton_Click()
With toggleButton
If .Value = True Then
.BackColor = vbRed
Else
.BackColor = vbGreen
End If
End With
End Sub
Then copy and paste the following code into your userform code module . . .
Option Explicit
Dim toggleButtonCollection As Collection
Private Sub UserForm_Initialize()
Set toggleButtonCollection = New Collection
Dim ctrl As MSForms.Control
Dim cToggleButton As clsToggleButton
For Each ctrl In Me.MultiPage1.Pages(0).Controls
If TypeName(ctrl) = "ToggleButton" Then
'ctrl.BackColor = vbGreen 'uncomment to initially set the backcolor to green
Set cToggleButton = New clsToggleButton
Set cToggleButton.toggleButton = ctrl
toggleButtonCollection.Add cToggleButton
End If
Next ctrl
End Sub
I have not worked with VB for many years and it was .net, so, if this solution is incorrect, let me know.
Solution 1: Arrays or Lists
You can create an array or a list containing all your toggle buttons, loop them and perform the operation you need for each of them. This will make sure that the logic above would be implemented exactly once rather than duplicated, yet, you still need to build your collections with the buttons.
Solution 2: A class
You can create a subclass for your toggle buttons and make sure that every toggle button in question will be of that class. And then you can create a static List for the class. In the constructor of each toggle button you append that button to the shared list in the class. And then you can create a shared method that loops the list and performs the logic you need.
P.S. Sorry for not writing code, I no longer remember the syntax of the language.

Access cascading combobox autofill When left with one option

I have ~50 cascading comboboxes on a form that I want to autofill if only one option is left. I found some code that worked for cboTwo (second combobox), but the other comboboxes aren't filling in automatically. I still have to use the drop down menu to make a selection. Is there any way that I can make all of my comboboxes autofill if there is only one option left in the drop down? I'd prefer some sort of macro help because that's what I've been using until now, but I'll use VBA if necessary. Thank you for all of your help!
Private Sub cboOne_AfterUpdate()
Me.cboTwo.Requery
If Me.cboTwo.ListCount = 1 Then
With Me.cboTwo
cboTwo.SetFocus
cboTwo.Value = cboTwo.ItemData(0)
End With
End If
End Sub
Private Sub cboTwo_AfterUpdate()
Me.cboThree.Requery
If Me.cboThree.ListCount = 1 Then
With Me.cboThree
cboThree.SetFocus
cboThree.Value = cboThree.ItemData(0)
End With
End If
End Sub
The problem may be with misunderstanding Access control events. Unlike in many other languages, control events are rarely triggered by changes made in VBA code. In other words, the event handler cboTwo_AfterUpdate() is not automatically called when cboTwo.Value = cboTwo.ItemData(0) is executed in code, so there will not be any automatic event cascade. Try the following pattern:
Private Sub cboOne_AfterUpdate()
Me.cboTwo.Requery
If Me.cboTwo.ListCount = 1 Then
With Me.cboTwo
cboTwo.SetFocus
cboTwo.Value = cboTwo.ItemData(0)
cboTwo_AfterUpdate
End With
End If
End Sub
Private Sub cboTwo_AfterUpdate()
Me.cboThree.Requery
If Me.cboThree.ListCount = 1 Then
With Me.cboThree
cboThree.SetFocus
cboThree.Value = cboThree.ItemData(0)
cboThree_AfterUpdate
End With
End If
End Sub

Stopping "TextBox_Change" event from executing if called by code

I have a userform which contains a TextBox object named myTextBox.
The text inside the object can be changed either by the user or by the code. There's a "onChange" procedures attached to the textbox:
Private Sub myTextBox_Change()
'do some stuffs
End Sub
I would like the event to be processed only when is the user changing the text, but not the code. I had thought about adding an Optional parameter like the following:
Private Sub myTextBox_Change(Optional isCode As Boolean)
If isCode = False Then
'do some stuffs
End If
End Sub
but this is not really helpful because I cannot pass the isCode = True when changing the name programmatically, like this:
myForm.myTextBox = "new text"
Does anyone have an idea on how I can fix this?
One way (perhaps not the right way?) would be to declare a global variable to use in the same way as your IsCode Boolean. Set it to true during your code block so the textbox_change event knows code is processing in the background, and then set back to false when that code has finished.
Put this at the top of a normal module (won't work in a form module)
Option Compare Database
Option Explicit
Global IsCode As Boolean
and then in the code you're running just set IsCode=true as required.

Problems when calling a public sub

I'm facing a deadend When trying to call this sub :
Public Sub backblue(ByVal frm As Form, ByVal boxname As String)
For i = 1 To 3
CType(frm.Controls(boxname & i.ToString()), TextBox).BackColor = Color.LightBlue
Next
End Sub
with button click event :
Private Sub Button1_click and bla bla....
backblue(Me, "txb1_")
End Sub
Can anybody show me a suggestion to fix the code.
It throws "Object Referrence not set to an instance bla bla" error
For information the textbox names are :
txb1_1 , txb1_2 , txb1_3
(these are some of the many textboxes in the form that i want its bakcolor changed)
and these three textboxes are already created through designer, not from execution.
i did check the textboxes names and there's nothing wrong.
the form class is also public.
if they are the only textboxs on said form you can just loop through
For Each box as Textbox In frm.Controls
box.BackColor = Color.LightBlue
Next
This error will occur if you do not declare the Form class to be public.
Also, make sure the textbox names are really correct, although this will probably cause a different error.
If you create the textboxes during execution, make sure they are initialized with New and added to the form's Controls collection.
Try this....
Public Sub backblue(ByVal frm As Form, ByVal prefix As String)
For i = 1 To 3
Dim bxName as String = prefix & i.ToString()
Dim bx as TextBox = CType(frm.Controls(bxName), TextBox)
If bx Is Nothing Then
MsgBox("Unable to find text box " +bxName)
Dim mtch() As Control = frm.Controls.Find(bxName, true)
If mtch.Length> 0 then
bx = mtch(0)
Else
Continue For
End if
End If
Bx.BackColor = Color.LightBlue
Next
End Sub
Although, a better solution would be to either create the textboxes inside a control and pass that control to BackBlue or to create an collection that has the controls and pass that in. Which brings up what is most likely yor problem your control is contained in a sub component and thus is not in the main form control collection
Alternative, you could use either the tag of the control or create a component control that implements IExtenderProvider and add it to the form --all of the above would effectively allow you to define the controls and/how they should be handled at designtime.
It may really seem that the names generated by this loop may not be the names of the original textboxes. My suggestion is before setting this Color property verify that the names generated by this loop are indeed the actual names. Maybe output this in a messagebox:
MessageBox.Show(boxname & i.ToString()) for each loop before you set the property

SetFocus inside a GotFocus procedure initiated by another SetFocus

Objective: Redirect focus from one command button to another using the first's GotFocus procedure.
Context: I have a form-independent procedure in a generic module that, on most forms, sets focus to the NewRecord button after saving the previous record. But on one form, I would like to redirect (based on certain conditions) focus back to the SignRecord button so the user can "sign" a second part of the same record (I may need this for other uses in the future). The target control is enabled and visible and can otherwise be focused and the original control can be focused when the redirect doesn't occur. Reference [2] below implies that this should be possible, though I'm not changing visibility of my controls.
Issue: When the conditions are met to redirect focus in the GotFocus procedure, it redirects as desired but the original (test) SetFocus call throws a "Run-time error '2110', Can't move focus to the control CommandNew".
What I've tried:
Exit Sub after my downstream SetFocus calls.
Call CommandSign.SetFocus in the hopes that it would make it happen outside the previous SetFocus process.
In a module,
Public Sub test()
Forms("TargetForm").CommandNew.SetFocus 'This gets the error '2110'
End Sub
In the 'TargetForm',
Private Sub CommandNew_GotFocus()
If IsNull(textDateTime) Then Exit Sub 'Works as expected
'I can see these two parts work. The framSign value changes
'and CommandSign gets focus
If checPPC And IsNull(textSigID_PPC) And framSign = 2 Then
framSign = 1
CommandSign.SetFocus
ElseIf checDAS And IsNull(textSigID_DAS) And framSign = 1 Then
framSign = 2
CommandSign.SetFocus
End If
End Sub
References:
[1]: SelectNextControl() a bad idea in a GotFocus event?
[2]: http://www.access-programmers.co.uk/forums/showthread.php?t=100071
I think your problem is that the call to Forms("TargetForm").CommandNew.SetFocus doesn't quite seem to, in fact, finish setting the focus to CommandNew until after Private Sub CommandNew_GotFocus() has finished executing. Because you've called another SetFocus before the first SetFocus could finish, there is a conflict that Access seems to be unable to cope with.
Whether or not that is the case, one thing is clear: the way you have your execution plan set up right now is unfortunately not going to work. You might try adding either a global variable or a public variable to each form that determines whether or not you should set your focus to CommandSign after you set the focus to CommandNew.
Ex. TargetForm:
Public boolSetCommandSignFocusInstead As Boolean
Private Sub CommandNew_GotFocus()
If IsNull(textDateTime) Then Exit Sub 'Works as expected
'I can see these two parts work. The framSign value changes
'and CommandSign gets focus
If checPPC And IsNull(textSigID_PPC) And framSign = 2 Then
framSign = 1
boolSetCommandSignFocusInstead = True
ElseIf checDAS And IsNull(textSigID_DAS) And framSign = 1 Then
framSign = 2
boolSetCommandSignFocusInstead = True
Else
boolSetCommandSignFocusInstead = False
End If
End Sub
Module:
Public Sub test()
Forms("TargetForm").CommandNew.SetFocus
If Forms("TargetForm").boolSetCommandSignFocusInstead Then
Forms("TargetForm").CommandSign.SetFocus
End If
End Sub