Using A Collection Class to Raise Common Event

MajP

You've got your good things, and you've got mine.
Local time
Today, 05:09
Joined
May 21, 2018
Messages
9,538
This is in response to question posed by @Gasman here

@Gasman,
Let me explain how I do it, you can take it or leave it but it works.

Almost always when I build a custom collection that wraps control events and I am working with more than one control I build a matching custom collection class

This is makes adding creating groups of controls so much easier
So I make a class
clsAlphaCmds to hold groups of clsAlphaCmd
Code:
Option Compare Database
Option Explicit

Private m_AlphaCmds As New Collection
Public Event AlphaCommandClicked(ClickedCommand As CommandButton)

Public Function Add(TheCommandButton As Access.CommandButton, iAlpha As Integer) As ClsAlphaCmd
  'create a new AlphaCmd and add to collection
  Dim NewAlphaCmd As New ClsAlphaCmd
  NewAlphaCmd.fInit TheCommandButton, iAlpha, Me
  m_AlphaCmds.Add NewAlphaCmd, CStr(iAlpha)
  Set Add = NewAlphaCmd
 End Function
Public Sub Add_AlphaCmd(ByVal TheAlphaCmd As ClsAlphaCmd)
  'I also add a second Add to allow you to build the object and then assign it
   m_AlphaCmds.Add TheAlphaCmd, TheAlphaCmd.Name
End Sub
Public Property Get Count() As Long
  Count = m_AlphaCmds.Count
End Property
Public Property Get NewEnum() As IUnknown
    'Attribute NewEnum.VB_UserMemId = -4
    'Attribute NewEnum.VB_MemberFlags = "40"
    'This is allows you to iterate the collection "For Each AlphaCmd in AlphaCmds"
   Set NewEnum = m_AlphaCmds.[_NewEnum]
End Property

Public Property Get Item(Name_Or_Index As Variant) As ClsAlphaCmd
   'Attribute Item.VB_UserMemId = 0
  'Export the class and uncomment the below in a text editer to allow this to be the default property
  'Then reimport
  Set Item = m_AlphaCmds.Item(Name_Or_Index)
End Property

Sub Remove(Name_Or_Index As Variant)
  'remove this person from collection
  'The name is the key of the collection
  m_AlphaCmds.Remove Name_Or_Index
End Sub

Public Property Get ToString() As String
  Dim strOut As String
  Dim i As Integer
  For i = 1 To Me.Count
    strOut = strOut & Me.Item(i).ToString & vbCrLf
  Next i
  ToString = strOut
End Property

'----------------------------------------------- All Classes Have 2 Events Initialize and Terminate --------
Private Sub Class_Initialize()
 'Happens when the class is instantiated not related to the fake Initialize method
 'Do things here that you want to run on opening
  Set m_AlphaCmds = New Collection
End Sub

Private Sub Class_Terminate()
  'Should set the object class properties to nothing
  Set m_AlphaCmds = Nothing
End Sub
'****************************************************************************************************************************************************************
'-----------------------------------------------------------------------------------   The Clicked AlphaCommad calls this Method   -------------------------------------------------------------
'*****************************************************************************************************************************************************************
'
Public Sub RaiseClickEvent(TheCommand As CommandButton)
  RaiseEvent AlphaCommandClicked(TheCommand)
  'MsgBox "Raise in Collection Class"
End Sub

This is a cut and paste exercise done the same way every time.
In the Collection Class I add one method where each control that raises an event calls it.
Now I create, add, and remove clsAlphaCmd only through the class. See the Collection class add method.

In clsAlphaCmd I do one more thing. I tell the clsAlphaCmd who the parent collection is.

Code:
Private WithEvents mCmd As CommandButton
Private ParentCollection As ClsAlphaCmds

Private Const mcstrEvProc As String = "[Event Procedure]"
Private Const cstrAlphabet As String = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ"



Public Function fInit(lCmd As CommandButton, iAlpha As Integer, TheParentCollection As ClsAlphaCmds)
    Set mCmd = lCmd
    mCmd.Caption = Mid(cstrAlphabet, iAlpha, 1)
    mCmd.OnClick = mcstrEvProc
    Set ParentCollection = TheParentCollection
End Function
Private Sub Class_Terminate()
    Set mCmd = Nothing
End Sub

'****************************************************************************************************************************************************************
'-----------------------------------------------------------------------------------   call the parent collection to raise an event -------------------------------------------------------------
'*****************************************************************************************************************************************************************

Private Sub mCmd_Click()
  ParentCollection.RaiseClickEvent mCmd
End Sub


Now you are done.

In the form you add only the Custom Collection with Events.
The individual clsAlphaCommand call a procedure in the collection class that raises an event.

Load the Collection in the Form
Code:
Private Sub Form_Load()
  Call SetAlphaButtons ' Initialise the command buttons via a class
End Sub
Private Sub SetAlphaButtons()

' Use clsCommand for the buttons for alphabet
Dim i As Integer
Set AlphaCommands = New ClsAlphaCmds
For i = 1 To 27
    Debug.Print Me.Controls("AlphaTab" & i).Caption & " - " & Me.Controls("AlphaTab" & i).Name
   
   ' Set Me.Controls("AlphaTab" & i) = ClsAlphaCmd()
   AlphaCommands.Add Me.Controls("AlphaTab" & i), i
Next
End Sub

Then Listen to the Collection Class to Raise an Event
Code:
'****************************************************************************************************************************************************************
'-----------------------------------------------------------------------------------  Locate Record  -------------------------------------------------------------
'*****************************************************************************************************************************************************************
Private Sub AlphaCommands_AlphaCommandClicked(ClickedCommand As CommandButton)
 LocateRecord ClickedCommand

End Sub
Private Sub LocateRecord(ClickedCommand As CommandButton)
    Dim i As Integer
     MsgBox "Trap in Form Command caption " & ClickedCommand.Caption
    DoCmd.RunCommand acCmdSaveRecord
    Me.Recordset.FindFirst "[AAC] = '" & strCaption & "'"
    Me.NavMenu = Me.NavMenu.ItemData(AIN) 'RecordsetClone![AIN]
    'Debug.Print strCaption & " - " & Me.NavMenu.ItemData(AIN) & " - " & AIN

End Sub

To summarize this technique
To Summarize
You custom class is usually some kind of control wrapper where you add a control to your custom class and trap its events. If VBA had inheritance this would all be much easier to do. Instead of inheritance we have to use the principle of Composition. The custom class is Composed of the control as a property of the class. Along with all the other properties we want to add.

1. In your custom class you need to add a property to maintain a reference to the related parent Collection Class. I called it Parent Collection.
2. Create a custom collection class. This wraps all the normal collection methods such as Add, Item, Remove, Count etc. Cut and Paste replace names.
2. In the collection class you add a method/s that the individual items (custom class objects) call when they trap a control event. This method then raises a custom event in the collection class. Now in your form listen to the collection class that is reporting events from the individual items in the collection.
3. Trap the collection class event in your main form.
This is kind of a chain. The custom class traps a control event. The custom class calls a method in the parent collection class which in turn raises a custom event. Your form listens to the the Collection Class custom event.
5. When creating custom class items you do this through the collection class add method. This passes a reference for the Collection Class to each custom class object. You never really work directly with the custom class. You do everything including trapping events through the collection class.

There is some similar discussion here, but I demo a simple listener instead of a custom collection class. But for me a collection class takes 5 minutes and it is just easier to add one even if it can be done without it.

Hopefully this provides sufficient explanation while under 5 pages, because that is how I roll.
 
Mikey likes it! And barely under 5 pages. GOOD JOB!

Pullin your chain @MajP .

I wasn't following the original thread so I have no idea what the big picture was supposed to do. But I do like passing in the parent and then calling the method in the parent to raise the event. My only real objection is hard coding the number of command buttons i = 1 to 27. My preference is to iterate the controls collection looking for command buttons and calling a factory every time one is found. But that is just personal preference.

Again though, I don't know what the big picture is.
 
Mikey likes it! And barely under 5 pages. GOOD JOB!

Pullin your chain @MajP .

I wasn't following the original thread so I have no idea what the big picture was supposed to do. But I do like passing in the parent and then calling the method in the parent to raise the event. My only real objection is hard coding the number of command buttons i = 1 to 27. My preference is to iterate the controls collection looking for command buttons and calling a factory every time one is found. But that is just personal preference.

Again though, I don't know what the big picture is.
Does anyone know what the original poster was trying to accomplish. I see a reference to a recordset or table or something but why?
 
Does anyone know what the original poster was trying to accomplish. I see a reference to a recordset or table or something but why?
I do not think the original problem required any custom classes, I think Gasman was trying it as a practice exercise. In the original the OP had 27 command buttons A,B,C,D.... When you click on one of those buttons it would move to the first record with that letter. The original had event procedures for each button, so Gasman wanted to make a custom class to avoid the multiple procedures. The problem however is that you do not want to create 27 class variables, because that does not save any effort. So you need either a listener class that each instance reports to or something like I did where the custom collection serves as the listener class.


The simple solution would probably just been to build a common function and put the function in event property. Probably Something like

=MoveToLetter

Public Function MoveToLetter
dim ctrl as access.commandbutton
set ctrl = me.activeControl
me.recordset.movefirst "SomeField Like '" & ctrl.caption & "'*"
end function
 
Yep, simpler is more better (in this case). But I like your solution because it can be easily reused. The listener class can be used by another form, custom code etc. A function is simpler, and simpler to understand but it is single use. But the listener class is generic as you describe it.

I do the same type of thing a lot. I always called the listener a "supervisor" class because it performed all the "stuff" that was required for whatever. I used one (clsSysVars) to load my SysVars tables and get them cached into memory.

I use one (clsOpenArgs) to grab the openargs and parse then into Openarg instances, stored in a collection. The clsFrm just has one of these OpenArgs defined in the header and calls it to discover if it has any Openargs. My OpenArgs supervisor also attempts to match the OpenArg name with form property names, and if the OpenArg name matches a property name, then it attempts to place that openarg value into the form property. Kind of a generic way for the programmer to do stuff to the form as it opens.

I never exposed an event that could be raised by the child however. That is a neat addition.
 
The simple solution would probably just been to build a common function and put the function in event property.
And that is what I did. :)

However having started on John's pdf book, I could see where a class would be better for that, getting rid of those 27 events in the form.
I changed one line in my sub, and the O/P implemented it in 27 subs. :(

SO, *I thought* that would be a simple excercise. boy was I so wrong. :(

When I am reading all this code, it does seem logical to me, but is not sinking in. Having a very bad memory these days, does not help.

Going to try again today.
 
@MajP
I have copied your code (or so I believe) and an getting compile error.
1748503327351.png
 
Where is the LocateRecord code meant to go please?

Edit: I have now added it to the form, as it balked on the recordset in the class.
I had to remove strCaption and use the commandbutton caption (I hope?)
Code:
Private Sub LocateRecord(ClickedCommand As CommandButton)
    MsgBox "Trap in Form Command caption " & ClickedCommand.Caption
    DoCmd.RunCommand acCmdSaveRecord
    Me.Recordset.FindFirst "[AAC] = '" & ClickedCommand.Caption & "'"
    Me.NavMenu = Me.NavMenu.ItemData(AIN) 'RecordsetClone![AIN]
    'Debug.Print strCaption & " - " & Me.NavMenu.ItemData(AIN) & " - " & AIN

End Sub
 
Last edited:
OK @Gasman, I am not going to step on this thread. @MajP will explain in detail but...
You are correct, you need a collection to store the instances of the button class. A class only stays in existence as long as there is a pointer to it. The collection holds the pointer, keeping the individual instances of the button class in existence.

So the big picture...
Create a class to hold one instance of the button class. Let's call it clsBtn (singular).
Create a "supervisor" class (as I call it) clsBtns (plural) which finds each button somehow, and then creates an instance of the clsBtn, passes in the button to it, then stores that new clsBtn instance in a collection. This keeps clsBtn just created around.
Rinse and repeat until every button has been found, and a clsBtn instantiated for it, and stored in the collection.

The added unique thing that @MajP does is defines an event in clsBtns which can be raised. He exposes that event in a PUBLIC method. AND he passes a pointer to clsBtns (the supervisor) into clsBtn. Because he did that, clsBtn "knows about" it's parent class clsBtns, and clsBtn can call the public method up in the parent clsBtns to raise the event in the parent when the click event occurs in clsBtn - the actual click event of the physical button control. Very neat!

So back to the big picture, the clsBtn doesn't raise its own event. It depends on clsBtns (its parent) to raise the event for it.

As for the details of each part, @MajP will explain that to you. If you break it down into child (clsBtn) / parent (clsBtns) class and the form that uses the parent (clsBtns), it becomes much simpler conceptually. And his code is neatly broken down into those exact pieces, in far less that 5 pages I might add ;) .
 
And with that I am stepping back. This is @MajP's thread and his code, which very neatly performs the big picture task.

And BTW, Allie and I went back to the dock to fish some more tonight. I left my computer safely in the RV. And Allie caught two "gigantic" fish, a sunfish (about 6 inches) and a bass (about a foot).

As it happens I bring two computers and included in the second computer is a mouse so I am able to work.

Thanks @MajP. Very cool code if I do say so!
 
Where is the LocateRecord code meant to go please?

Edit: I have now added it to the form, as it balked on the recordset in the class.
I had to remove strCaption and use the commandbutton caption (I hope?)
Code:
Private Sub LocateRecord(ClickedCommand As CommandButton)
    MsgBox "Trap in Form Command caption " & ClickedCommand.Caption
    DoCmd.RunCommand acCmdSaveRecord
    Me.Recordset.FindFirst "[AAC] = '" & ClickedCommand.Caption & "'"
    Me.NavMenu = Me.NavMenu.ItemData(AIN) 'RecordsetClone![AIN]
    'Debug.Print strCaption & " - " & Me.NavMenu.ItemData(AIN) & " - " & AIN

End Sub
The locate record can go in the parent class, clsBtn or up in the form. Given that clsBtns raises an event for the child clsBtn classes, I assume the form itself will sink the event from clsBtns and use that to determine which button is calling, and perform the actual task of finding the record. If you break it down into whose business is it to find the record... the form owns the big picture. It is using the clsBtns to do the task of finding the buttons, getting them in clsBtns instances and raising events.
 
John,
When I put it in the class it complained about the recordset?
Possibly also the Me?
 
@MajP
Why do we convert iAlpha to a string to add to a collection?
m_AlphaCmds.Add NewAlphaCmd, CStr(iAlpha)

I notice MS do it in one of their examples?
Code:
        If Inst.InstanceName <> "" Then
            ' Add the named object to the collection.
            MyClasses.Add item := Inst, key := CStr(Num)
        End If
 
Thank you @cheekybuddha
Are you able to address my issue in post #7, while @MajP is absent please?
The TheAlphaCommand class does not have a public Name property or function, i.e it does not know how to respond when asked for its name. The compiler figures this out even before runtime (at compile time) and tells the programmer "hey, there is no name property"
 
Tell you what.
Let's just forget it all please.

I am obviously too stupid to work this out for myself, and fortunately for me, I have no real need for the classes in the first place.
I have got by without them for the little work I have done in Access. I can see their value, but obviously not for me. :(
 
John,
When I put it in the class it complained about the recordset?
Possibly also the Me?
We want to break things down to small pieces so that each part has a well defined purpose.

TheAlphaCommand does nothing but store a pointer to the command button and a pointer to its parent AlphaCmds, and sinks the command button's click event, calling up to the parent class to raise an event when the button is clicked.

AlphaCmds does nothing but find the instances of the command button, create an instance of TheAlphaCommand class, store the command button in it and store that class instance in a collection. Oh... and raise an event for any TheAlphaCommand when a command button is clicked.

But it is not the job of the AlphaCmds to actually do the find of the record. That is the job (presumably) of the form.
When a button is clicked, the event is sunk in TheAlphaCommand. That TheAlphaCommand class calls up to the parent class AlphaCmds and tells it "hey, I have been clicked, raise an event for me".

AlphaCmds sees the request from some command button to "raise an event for me" and raises the event. The function that raises the event has a parameter passed in which is the command button which has just been clicked. So AlphaCmds raises the event, passing on through a pointer to the actual button that was just clicked.

The magic of this is that the form can now sink that event and do something. What does it do? Nobody we have discussed so far knows or cares what the form does. Remember my discussion in the book about raising an event is like a radio. Who is listening doesn't matter to the event raiser.

And that is why AlphaCmds cannot (or should not) have anything to do with finding records. That is not its job. Finding records is the job of the form.

So the form dimensions a variable in its header.

Private Withevents AlphaCommands as AlphaCmds.

AlphaCmds can and does raise one event - AlphaCommandClicked. It raises a single event every time one of the TheAlphaCmds classes asks it to.

Now in the form, we create an event sink - AlphaCommands_AlphaCommandClicked - for AlphaCmds.

The form actually understands it's table and stuff so IT does the record find. This is an event sink in the form sinking an event AlphaCommands_AlphaCommandClicked being raised by AlphaCommands.

Code:
Private Sub AlphaCommands_AlphaCommands_AlphaCommandClicked(ClickedCommand As CommandButton)
 LocateRecord ClickedCommand

End Sub
Private Sub LocateRecord(ClickedCommand As CommandButton)
    Dim i As Integer
     MsgBox "Trap in Form Command caption " & ClickedCommand.Caption
    DoCmd.RunCommand acCmdSaveRecord
    Me.Recordset.FindFirst "[AAC] = '" & strCaption & "'"
    Me.NavMenu = Me.NavMenu.ItemData(AIN) 'RecordsetClone![AIN]
    'Debug.Print strCaption & " - " & Me.NavMenu.ItemData(AIN) & " - " & AIN

End Sub
 
Tell you what.
Let's just forget it all please.

I am obviously too stupid to work this out for myself, and fortunately for me, I have no real need for the classes in the first place.
I have got by without them for the little work I have done in Access. I can see their value, but obviously not for me. :(
No Gasman. Stupid means unable to learn. You are uneducated. We are trying to educate you. Just bear with us please. It takes that aha moment but it will happen.
 

Users who are viewing this thread

Back
Top Bottom