Raising event from object in custom collection class - vba

If an object is contained within a collection, can that object still raise events to a parent class?
Clearly you could tell the child class a reference to the parent class, and then call a public method within the parent class within the child class, however that would result in a circular reference, which as I understand it would make it so the garbage collector would not ever get rid of either object.
Details:
I have two classes, one a person named clsPerson, and the second a custom collection class named clsPeople. clsPerson has a public boolean property named Selected. If selected is changed, I call an event SelectedChange. At that point, I need to do something in clsPeople. How can I trap the event in the custom collection class clsPeople? The person class can be changed from outside of the scope of People, otherwise I would look at another solution.
<<Class clsPerson>>
Private pSelected as boolean
Public Event SelectedChange()
Public Property Let Selected (newVal as boolean)
pSelected = newVal
RaiseEvent SelectedChange
End Property
Public Property Get Selected as boolean
Selected = pSelected
End Property
<<Class clsPeople>>
Private colPeople as Collection
' Item set as default interface by editing vba source code files
Public Property Get Item(Index As Variant) As clsPerson
Set Item = colPeople.Item(Index)
End Property
' New Enum set to -4 to enable for ... each to work
Public Property Get NewEnum() As IUnknown
Set NewEnum = colPeople.[_NewEnum]
End Property
' If selected changes on a person, do something
Public Sub ???_SelectedChange
' Do Stuff
End Sub

You can easily raise an event from a class in a collection, the problem is that there's no direct way for another class to receive events from multiples of the same class.
The way that your clsPeople would normally receive the event would be like this:
Dim WithEvents aPerson As clsPerson
Public Sub AddPerson(p As clsPerson)
Set aPerson = p ' this automagically registers p to the aPerson event-handler `
End Sub
Public Sub aPerson_SelectedChange
...
End Sub
So setting an object into any variable declared WithEvents automatically registers it so that it's events will be received by that variable's event handlers. Unfortunately, a variable can only hold one object at a time, so any previous object in that variable also gets automatically de-registered.
The solution to this (while still avoiding the problems of reference cycles in COM) is to use a shared delegate for this.
So you make a class like this:
<<Class clsPersonsDelegate>>
Public Event SelectedChange
Public Sub Raise_SelectedChange
RaiseEvent SelectedChange
End Sub
Now instead of raising their own event or all calling their parent (making a reference cycle), you have them all call the SelectedChange sub in a single instance of the delegate class. And you have the parent/collection class receive events from this single delegate object.
The Details
There are a lot of technical details to work out for various cases, depending on how you may use this approach, but here are the main ones:
Don't have the child objects (Person) create the delegate. Have the parent/container object (People) create the single delegate and then pass it to each child as they are added to the collection. The child would then assign it to a local object variable, whose methods it can then call later.
Typically, you will want to know which member of your collection raised the event, so add a parameter of type clsPerson to the delegate Sub and the Event. Then when the delegate Sub is called, the Person object should pass a reference to itself through this parameter, and the delegate should also pass it along to the parent through the Event. This does not cause reference-cycle problems so long as the delegate does not save a local copy of it.
If you have more events that you want the parent to receive, just add more Subs and more matching Events to the same delegate class.
Example Implementation
Responding to requests for a more concrete example of "Have the parent/container object (People) create the single delegate and then pass it to each child as they are added to the collection."
Here's our delegate class. Notice that I've added the parameter for the calling child object to the method and the event.
<<Class clsPersonsDelegate>>
Public Event SelectedChange(obj As clsPerson)
Public Sub RaiseSelectedChange(obj As clsPerson)
RaiseEvent SelectedChange(obj)
End Sub
Here's our child class (Person). I have replaced the original event, with a public variable to hold the delegate. I have also replaced the RaiseEvent with a call to the delegate's method for that event, passing along an object pointer to itself.
<<Class clsPerson>>
Private pSelected as boolean
'Public Event SelectedChange()'
' Instead of Raising an Event, we will use a delegate'
Public colDelegate As clsPersonsDelegate
Public Property Let Selected (newVal as boolean)
pSelected = newVal
'RaiseEvent SelectedChange'
colDelegate.RaiseSelectedChange(Me)
End Property
Public Property Get Selected as boolean
Selected = pSelected
End Property
And here's our parent/custom-collection class (People). I have added the delegate as an object vairable WithEvents (it should be created at the same time as the collection). I have also added an example Add method that shows setting the child objects delegate property when you add (or create) it to the collection. You should also have a corresponding Set item.colDelegate = Nothing when it is removed from the collection.
<<Class clsPeople>>
Private colPeople as Collection
Private WithEvents colDelegate as clsPersonsDelegate
Private Sub Class_Initialize()
Set colPeople = New Collection
Set colDelegate = New clsPersonsDelegate
End Sub
' Item set as default interface by editing vba source code files'
Public Property Get Item(Index As Variant) As clsPerson
Set Item = colPeople.Item(Index)
End Property
' New Enum set to -4 to enable for ... each to work'
Public Property Get NewEnum() As IUnknown
Set NewEnum = colPeople.[_NewEnum]
End Property
' If selected changes on any person in our collection, do something'
Public Sub colDelegate_SelectedChange(objPerson as clsPerson)
' Do Stuff with objPerson, (just don't make a permanent local copy)'
End Sub
' Add an item to our collection '
Public Sub Add(ExistingItem As clsPerson)
Set ExistingItem.colDelegate = colDelegate
colPeople.Add ExistingItem
' ... '
End Sub

Related

Change control text when property changes

I need to change my label MyLabel.Text once property Path is changed. Look below my code. I know how to do it from outside my class FrmImport that i could just subscribe to PropertyChanged event however i am not sure how to do it from FrmImport class itself. You can see that i've implemented INotifyPropertyChanged and created OnPropertyChanged which is called once path is set: OnPropertyChanged("Path"). Now i do not belive to subscribe my event in FrmImport itself but rather than that I think i just need to make somehow: MyLabel.DataBindings.Add(??) to make it work but right now stack on it. Can anybody help?
Public Class FrmImport
Implements INotifyPropertyChanged
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Protected Sub OnPropertyChanged(propName As String)
If propName IsNot Nothing Then
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propName))
End If
End Sub
Private _path As String
Public Property Path As String
Get
Return _path
End Get
Set
_path = Value
OnPropertyChanged("Path")
End Set
End Property
Private Sub FrmImport_Load(sender As Object, e As EventArgs) Handles MyBase.Load
' ??? MyLabel.DataBindings.Add("Text", Me.Path)
End Sub
or should i just make like this without any INotifyProperty usage?:
Public Property Path As String
Get
Return _path
End Get
Set
_path = Value
OnPropertyChanged("Path")
MyLabel.Text= Me.Path
End Set
End Property
I usually do binding when the data is stored outside of the form. For example, if you had a PathInformation class with a bunch of properties that needed to be displayed. There need to be something in place for the class to tell the form it got updated and the form to tell to class to update itself.
When the variable is directly in the form, binding isn't really needed. Just update the path directly. No need for OnPropertyChanged because nothing outside of the form need to know of the change (since they already called the Path property).
But this is borderline opinion and depends also on the rest of the code.

User-defined events in Access 2007 doesn't work properly

I have some trouble with user-defined events in Access 2007:
The events are declared within a class module - let's name this class Controller - as follows (simplified code examples):
Public Event EventA()
[...]
Public Property Let PropertyA(RHS As String)
mPropertyA = RHS
RaiseEvent EventA
End Property
[...]
The class is instantiated in a module as a "self-healing" object as follows:
Public Property Get objController() As Controller
Static c As Controller
If c Is Nothing Then _
Set c = New Controller
Set objController = c
End Property
In a form the Controller class is declared and set within the sub Form_Load() as follows:
Private WithEvents mController As Controller
[...]
Private Sub Form_Load()
[...]
Set mController = objController
[...]
End Sub
Within the same form I implemented the event actions:
Private Sub mController_EventA()
[...]
Me!PropertyA = mController.PropertyA
[...]
End Sub
After clicking a button on the form a dialog form with a treeview is opened. After clicking a node in the treeview PropertyA in the Controller object is changed:
Private Sub tvwRDS_NodeClick(ByVal node As Object)
[...]
objController.PropertyA = node.key
[...]
End Sub
My intention was this:
You click a node.
PropertyA of instantiated class Controller is set, and event EventA() is raised.
Main form is handling the event; controls in the form are updated.
The first time everything worked as intended. After using the Compact and Repair feature as well as after compiling and creating an ACCDE file the sub mController_EventA() seems to be lost in space: nothing happens after the event was fired. Why that?!
The WithEvents clause is only allowed in object modules. But I need to instantiate self-healing objects from a normal module.
Thanks a lot in advance!
D.C.
It seems that the Property itself must be declared as Static:
Public Static Property Get objController() As Controller
Static c As Controller
If c Is Nothing Then _
Set c = New Controller
Set objController = c
End Property
Since then it works fine.

Automatically terminate class while children still reference it

I have created a class where its children hold a necessary reference to their parent. This means, when the parent class goes out of scope in the routine which calls it, its terminate code is not called, as the children still hold a reference to it.
Is there a way of terminating the parent without having to manually terminate the children first too?
Current work around is to make the parent's terminate code (which contains code to terminate the children) public and to call it from the routine, but this is not ideal as then the parent is terminated twice (once manually, once when it leaves the caller sub's scope). But mainly I don't like having to call it manually
'Caller code
Sub runTest()
Dim testClass As parentClass
Set testClass = New parentClass
Set testClass = Nothing
End Sub
'parentClass
Private childrenGroup As New Collection
Private Sub Class_Initialize()
Dim childA As New childClass
Dim childB As New childClass
childA.Setup "childA", Me 'give name and parent reference to child class
childB.Setup "childB", Me
childrenGroup.Add childA 'add children to collection
childrenGroup.Add childB
End Sub
Public Sub Class_Terminate()
Set childrenGroup = Nothing
Debug.Print "Parent terminated"
End Sub
'childClass
Private className As String
Private parent As classParent
Public Sub Setup(itemName As String, parentObj As classParent)
Set parent = parentObj 'set the reference
className = itemName
End Sub
Public Property Get Name() As String
Name = className
End Property
Private Sub Class_Terminate()
Debug.Print Name;" was terminated" 'only called when parent terminates child
End Sub
Calling runTest() prints
childA was terminated
childB was terminated
Parent terminated
Parent terminated 'to get around this, you could just make another sub in the parent class
'to terminate its children
'but that still requires a manual call
Having reviewed your comments, I'm still not convinced you need to pass the parent into the child class. If your only reason for doing so is to create a kind of callback then you'd probably be better off passing an event delegate class to the child instead of the parent class and then simply handle the delegate's events in your parent class. As you've seen, your current structure is causing some object disposal issues and these can be avoided.
Simply create a class containing your child events. In this example I've called the class clsChildEvents:
Option Explicit
Public Event NameChange(obj As clsChild)
Public Sub NotifyNameChange(obj As clsChild)
RaiseEvent NameChange(obj)
End Sub
Now your code remains pretty much as is. The key difference is that you are passing the delegate instead of the parent to the child objects.
Parent class:
Option Explicit
Private WithEvents mChildEvents As clsChildEvents
Private mChildren As Collection
Public Sub SetUp()
Dim child As clsChild
Set child = New clsChild
child.SetUp "ChildA", mChildEvents
mChildren.Add child
Set child = New clsChild
child.SetUp "ChildB", mChildEvents
mChildren.Add child
End Sub
Private Sub mChildEvents_NameChange(obj As clsChild)
Debug.Print "New name for "; obj.Name
End Sub
Private Sub Class_Initialize()
Set mChildEvents = New clsChildEvents
Set mChildren = New Collection
End Sub
Private Sub Class_Terminate()
Debug.Print "Parent terminated."
End Sub
Child class:
Option Explicit
Private mClassName As String
Private mEvents As clsChildEvents
Public Sub SetUp(className As String, delegate As clsChildEvents)
Set mEvents = delegate
Me.Name = className
End Sub
Public Property Get Name() As String
Name = mClassName
End Property
Public Property Let Name(RHS As String)
mClassName = RHS
mEvents.NotifyNameChange Me
End Property
Private Sub Class_Terminate()
Debug.Print mClassName; " terminated."
End Sub
And then your module code:
Option Explicit
Public Sub Test()
Dim parent As clsParent
Set parent = New clsParent
parent.SetUp
Set parent = Nothing
End Sub
The immediate window output is as follows:
New name for ChildA
New name for ChildB
Parent terminated.
ChildA terminated.
ChildB terminated.
No, you can't automatically terminate the children by simply terminating the collection:
Public Sub Class_Terminate()
Set childrenGroup = Nothing
The Collection object does not clean up any object references when it is set equal to Nothing, and it doesn't have a 'RemoveAll' mechanism which does.
...And I wouldn't bet on 'Remove' doing that either, one item at a time. So you do indeed need to loop backwards through the members, in exactly the 'manual' process you're trying to avoid:
Public Sub Class_Terminate()
Dim i As Integer
For i = childrenGroup.Count To 1 Step - 1
Set childrenGroup(i) = Nothing
childrenGroup.Remove i
Next I
Set childrenGroup = Nothing
My advice would be: use a Scripting.Dictionary object instead of the VBA Collection:
Private childrenGroup As New Scripting.Dictionary
You'll need a reference to the Scripting Runtime (or to the Windows Scripting Host Object Model) and you may be surprised by the changed order of .Add Item, Key - and you will definitely be surprised by what happens when you request an item with a non-existent key.
Nevertheless, this works. The Dictionary does have a RemoveAll method, and this will clear all the references in its .Items when you call it:
Public Sub Class_Terminate()
childrenGroup.RemoveAll
You do need to call RemoveAll - it doesn't happen automatically if the dictionary goes out of scope - but that's as close as you'll get.
Note, also, that VBA runs on reference counting: if anything else has a reference to the children, the objects won't be released.

How to list all timers of another form

I want to make a procedure that stops all timers of any given form. Though when building it says
"components' is not a member of 'System.Windows.Forms.Form".
Here is the code:
Public Sub _Timers_Stop(frm As Form)
For Each itm As Object In frm.components.components
If TypeOf (itm) Is Timer Then
itm.stop()
End If
Next
End Sub
You could use reflection for this:
Public Sub StopTimers(Form As Form)
For Each Item In Form.GetType.GetFields(Reflection.BindingFlags.NonPublic Or Reflection.BindingFlags.Instance Or Reflection.BindingFlags.Public).Where(Function(x) TypeOf x.GetValue(Form) Is Timer)
Dim Timer As Timer
Timer = Item.GetValue(Form)
Timer.Stop()
Next
End Sub
As I was corrected in the comments, the Controls collection does not contain components--it only contains controls. The references to the non-visual components, such as timers, are held in a private Container field, typically called components. That Container field is not a part of the base Form class at all. It is declared and implemented separately, on each form that needs it, by the form designer. Since it's not a member of the base class, there is no easy way to access it on any given form. Even if it was a member of the base class, accessibility would still be an issue since it's typically declared as a private field.
The safe way to do this, which retains proper type-checking, would be to create an interface:
Public Interface IFormWithComponents
ReadOnly Property Components As ComponentCollection
End Interface
Which you could then implement on every form, as applicable:
Public Class MyForm
Implements IFormWithComponents
Public ReadOnly Property Components As ComponentCollection Implements IFormWithComponents.Components
Get
Return components.Components
End Get
End Property
End Class
And then your timer-stopping method could take that interface as its parameter:
Public Sub _Timers_Stop(frm As IFormWithComponents)
For Each t As Timer In frm.Components.Cast(Of Component).OfType(Of Timer)
t.stop()
Next
End Sub
However, if you don't really care about the type-checking, and you don't mind the slight decrease in performance, you can alternatively use reflection to find the private field in the form object and extract its value:
Public Sub _Timers_Stop(frm As Form)
Dim timers As IEnumerable(Of Timer) = frm.
GetType().
GetFields(BindingFlags.FlattenHierarchy Or BindingFlags.Instance Or BindingFlags.NonPublic Or BindingFlags.Public).
Select(Function(fieldInfo) fieldInfo.GetValue(frm)).
OfType(Of Container)().
SelectMany(Function(container) container.Components.OfType(Of Timer)())
For Each t As Timer In timers
t.Stop()
Next
End Sub

VB.Net How to attach a handler to a generic class

I have a class 'oBnd' defined as Object that can be assigned as type clsBound or clsClaim.
Claim and bound are identical from the outside, same methods etc.
I call the various properties using 'CallByName'
ie Dim Current As String = CallByName(oBnd, PropName, CallType.Get)
When a property is changed in either class the DirtyStatus event is raised by that class.
I am having a problem attaching to this event.
If I try
AddHandler oBnd.DirtyStatus, AddressOf oBnd_DirtyStatus
I get the error "DirtyStatus is not an event of Object" I guess that makes sense as clearly object know nothing of my dirtystatus.
I tried using:
AddHandler DirectCast(oBnd, clsBound).DirtyStatus, AddressOf oBnd_DirtyStatus
While this does fix the error it does not get called when the DirtyStatus event is raised.
oBnd is defined as
Private WithEvents oBnd As Object
It is global to the form
oBnd gets set as
oBnd = New clsBound(mvarBUDConnection)
AddHandler oBnd.DirtyStatus, AddressOf oBnd_DirtyStatus
oBnd.Load(CInt(txtTrans.Text))
BuildPage(oBnd)
Or
oBnd = New clsClaim(mvarBUDConnection)
AddHandler oBnd.DirtyStatus, AddressOf oBnd_DirtyStatus
oBnd.Load(CInt(txtTrans.Text))
BuildPage(oBnd)
The oBnd_DirtyStatus sub, that I am trying to attach to, looks like this
Private Sub oBnd_DirtyStatus(IsDirty As Boolean) ' Handles oBnd.DirtyStatus
Me.Text = "QFix"
If IsDirty Then
Me.Text = "QFix - Pending Save"
btnSave.Enabled = True
Else
btnSave.Enabled = False
End If
End Sub
How can I attach a handle to this event?
Here is how you can both get Events working and get away from using Reflection to access properties. Even given the public methods are similar but the data being carried is very different it should still possible to use OOP/Inheritance.
Public Enum ClaimBoundType
None ' error!!!!
Claim
Bound
End Enum
Public MustInherit Class ClaimBase
' type tracker usually rather handy
Public Property ItemType As ClaimBoundType
Public Sub New(t As ClaimBoundType)
ItemType = t
End Sub
' low rent INotifyPropertyChanged
Public Event DataChanged(sender As Object, e As EventArgs)
' "universal" prop: works the same for all derived types
Private _name As String = ""
Public Property Name As String
Get
Return _name
End Get
Set(value As String)
If value <> _name Then
_name = value
BaseDataChanged(Me)
End If
End Set
End Property
' props which must be implemented; 1 or 100 doesnt matter
MustOverride Property CurrentValue As Integer
' methods which must be implemented
MustOverride Function DoSomething() As Integer
' raise the changed event for base or derived classes
Protected Friend Sub BaseDataChanged(sender As Object)
RaiseEvent DataChanged(sender, New EventArgs())
End Sub
End Class
You'd have to do some basic data analysis to figure out which Properties and Methods can be implemented in the base class (as with Name above) and which in the inherited classes. There are usually at least some which can be done in the base class.
Your derived classes can implement the methods in totally different ways and load data from where ever:
Public Class Claim
Inherits ClaimBase ' the IDE will add all the MustInherits when
' you press enter
Public Sub New()
MyBase.New(ClaimBoundType.Claim)
End Sub
Public Overrides Function DoSomething() As Integer
' what happens here can be completely different
' class to class
End Function
Private _CurValue As Integer = 0
Public Overrides Property CurrentValue As Integer
Get
Return _CurValue
End Get
Set(Value As Integer)
If _CurValue <> Value Then
_CurValue = Value
OnDataChanged("CurrentValue")
End If
End Set
End Property
' name of prop that changed not actually used here, but
' is usually good to know (use custom args or INotifyPropertyChanged)
Public Sub OnDataChanged(pname As String)
' fire shared datachanged event
MyBase.BaseDataChanged(Me)
End Sub
End Class
How to Use Them
Now you can implement them without resorting to Object, subscribe to the event and not have to use Reflection to get/set properties:
' 'generic' object variable: DONT/CANT USE [New] w/ClaimBase
Private myCB As ClaimBase
...
' set it as a Claim instance...
' This is perfectly legal because Claim is also a ClaimBase Type:
myCB = New Claim
' hook up the event handler
AddHandler myCB.DataChanged, AddressOf cb_DataChanged
You can declare your object variables as ClaimBase, but you cannot create an instance of ClaimBase since it is abstract/MustInherit. Since the event is part of the base class, there is no problem with syntax. The form level handler:
' Use standard (sender, e) signature
' (CA will object to other signatures:)
Private Sub cb_DataChanged(sender As Object, e As EventArgs)
' do change stuff here
...
End Sub
Best of all, you can reference properties directly:
cbObj.Name = "Ziggy" ' will fire the event from the base class
cbObj.CurrentValue = 42 ' fires event from the Claim class
I added the ItemType property so you can tell them apart at run time (ie when you hold the mouse over a ClaimBase object variable). If/when there are Type specific properties/methods to access, cast it (from what you said, there cant be any of these now):
If cbObj.ItemType = ClaimBoundType.Claim Then
CType(cbObj, Claim).ClaimSomething = 5
End If
Also use ClaimBase as the declaration Type for Lists and method signatures also to allow either type to be passed rather than boxing them (converting to Object):
Private cbList As New List(Of ClaimBase)
...
' just an example of the declaration
Private Sub AddThingToList(cb As ClaimBase)
cbList.Add(cb)
End Sub
I did not go into INotifyProperty in order to focus on Inheritance, though the basics of it are in that base class. It is a more systemic way to implement the DataChanged/DirtyStatus event and detection.