PDA

View Full Version : [SOLVED:] Custom Object Self-referential Events



mikerickson
07-26-2015, 02:39 PM
I have a custom userform Control, clsTwoInput.

Basically, it is a Frame that contains a CheckBox and two InputBoxes.
The CheckBox toggles the visibility of the InputBoxes and the clsTwoInput.Value is the .Value of the visible InputBox.

Each of the InputBoxes can be either a TextBox or a clsTwoInput.

This works fine, until I try to get the clsTwoInput to generate a Change event.

Here is my current clsTwoInput code.
When I try to add WithEvents to the declaration of clOneInput, I get an compile error "Cannot handle events for the object specified"

' in clsTwoInput Class module

Public WithEvents txtOneBox As MSForms.TextBox
Public WithEvents txtTwoBox As MSForms.TextBox
Public clOneInput As clsTwoInput
Public clTwoInput As clsTwoInput

Public WithEvents chkChooser As MSForms.CheckBox

Event Change()

Property Get OneBox() As Object
If txtOneBox Is Nothing Then
Set OneBox = clOneInput
Else
Set OneBox = txtOneBox
End If
End Property
Property Set OneBox(InputObject As Object)
Set txtOneBox = Nothing
Set clOneInput = Nothing
Select Case TypeName(InputObject)
Case "TextBox"
Set txtOneBox = InputObject
Case "clsTwoInput"
Set clOneInput = InputObject
End Select
End Property

Property Get TwoBox() As Object
If txtTwoBox Is Nothing Then
Set TwoBox = clTwoInput
Else
Set TwoBox = txtTwoBox
End If
End Property
Property Set TwoBox(InputObject As Object)
Set txtTwoBox = Nothing
Set clTwoInput = Nothing
Select Case TypeName(InputObject)
Case "TextBox"
Set txtTwoBox = InputObject
Case "clsTwoInput"
Set clTwoInput = InputObject
End Select
End Property

Property Get Value() As String
If OneBox.Visible Then
Value = OneBox.Value
Else
Value = TwoBox.Value
End If
End Property
Property Let Value(inValue As String)
If OneBox.Visible Then
OneBox.Value = inValue
Else
TwoBox.Value = inValue
End If
End Property

Property Get Visible() As Boolean
Visible = chkChooser.Parent.Visible
End Property
Property Let Visible(Visibility As Boolean)
chkChooser.Parent.Visible = Visibility
End Property

Private Sub chkChooser_Click()
OneBox.Visible = chkChooser.Value
TwoBox.Visible = Not (chkChooser.Value)
RaiseEvent Change
End Sub

Private Sub txtOneBox_Change()
RaiseEvent Change
End Sub

Private Sub txtTwoBox_Change()
RaiseEvent Change
End Sub


In the attached user form, OuterInput is a clsTwoInput that has its checkbox, one text box and one clsTwoInput, InnerBox.
Changing OuterInput by clicking the checkbox or by typing into the checkbox will trigger the OuterInput_Change event, but changing InnerBox will not.

Is there a way for a custom object, that has a different instance of that object as a property of that object, to receive Events from the sub-instance of itself?

Thank you for helping with this.
(The interface at ExcelForum.com is acting a little weird today, I may have cross-posted this there.)

mikerickson
07-26-2015, 03:17 PM
I just tried adding another custom object as a work-around and found out about the "circular dependencies between modules" error.
:(

The issue that I'm seeing is that I'm not creating circular references between instances, but Excel is seeing circular references between modules.

Aussiebear
07-26-2015, 03:51 PM
Mike, I'm presuming its just a typo but....

Public clOneInput As clsTwoInput
Public clTwoInput As clsTwoInput


uhoh, never mind I'll just go back to my corner of the world.

mikerickson
07-26-2015, 04:35 PM
Just verified that I actually did cross-post
http://www.excelforum.com/excel-programming-vba-macros/1095861-custom-object-resursive-events.html

pike
07-26-2015, 09:30 PM
I don't know why or of any work round but once the property is set with the first reference that's it you can not change the property

Property Set TwoBox(InputObject As Object)
Set txtTwoBox = Nothing
Set clTwoInput = Nothing
Select Case TypeName(InputObject)
Case "TextBox"
Set txtTwoBox = InputObject
Case "clsTwoInput"
Set clTwoInput = InputObject
End Select
End Property

It just makes it the default property


can you loop

Public Function All() As stdole.IUnknown
'Uses: Attribute All.VB_UserMemID = -4 (Makes function available to For Each.)
Set All = InputObject.[_NewEnum]
End Function

mikerickson
07-26-2015, 10:30 PM
I don't know why or of any work round but once the property is set with the first reference that's it you can not change the property
Actualy the .TwoBox property can be changed.
In use, it wouldn't be changed, (clsTwoInput is a custom control assembled out of existing UF controls, the .OneBox and .TwoBox) but it could. There's nothing in the coding that would prevent it.

pike
07-27-2015, 12:09 AM
bit like you can not save type to collections

But you did solve that problem by custom class type


Set OuterInput = New clsTwoInput
With OuterInput
Set .chkChooser = CheckBox2
Set .OneBox = TextBox3
Set .TwoBox = InnerInput
End With

pike
07-27-2015, 12:42 AM
or
Set .TwoBox = InnerInput.TwoBox

Aflatoon
07-27-2015, 01:02 AM
Per the VBA language spec, for a WithEvents declaration:
"The specified type of <class-type-name> element must not be the class defined by the class modules containing this declaration."

Perhaps you can reverse the logic and have the class's Change code call a routine in a parent clsTwoInput if it's not Nothing.

mikerickson
07-27-2015, 06:28 AM
^ Aflatoon's approach sounds like the easiest to impliment.
I can write thing so that tbxOneBox_Change and tbxTwoBox_Change call a routine in the Userform code module. That will alert the UF that something has changed, and, if it has to search to find exactly what has changed, it will work.

Aflatoon
07-27-2015, 06:42 AM
If you use a parent clsTwoInput, you won't need to search.

mikerickson
07-27-2015, 07:07 AM
Hmm...It's not as smooth as I'd like, but it works.

To the user form, I changed OuterInput to be without events. I added

Public WithEvents ActiveTwoInput As clsTwoInput

I altered the clsTwoInput code

Private Sub chkChooser_Click()
OneBox.Visible = chkChooser.Value
TwoBox.Visible = Not (chkChooser.Value)
Set ParentUF.ActiveTwoInput = Me
RaiseEvent Change
End Sub

Private Sub txtOneBox_Change()
Set ParentUF.ActiveTwoInput = Me
RaiseEvent Change
End Sub

Private Sub txtTwoBox_Change()
Set ParentUF.ActiveTwoInput = Me
RaiseEvent Change
End Sub

Function ParentUF() As Object
Set ParentUF = chkChooser
On Error Resume Next
Do
Set ParentUF = ParentUF.Parent
Loop Until Err
On Error GoTo 0
End Function

That way all the change events are sent to the Userform via ActiveTwoInput_Change.

In the attached I added another Inner/Outer pair.
I think it works. and the user form's labels show the changing Values of the two clsTwoInputs.

Thanks.

mikerickson
07-27-2015, 07:11 AM
If you use a parent clsTwoInput, you won't need to search.

I'll know which clsTwoInput has been changed, but for example, if I had two labels and two clsTwoInput, I'd have to switch between Labels depending on which of my clsTwoInput was the changed one. A short loop, but the same consept.

mikerickson
07-27-2015, 07:45 AM
Actually, I think there's a better approach than dumping everything back to the userform.

in clsTwoInput, add a new property .ParentTwoInput, that is set in the Property Set OneBox or TwoBox routines.
Also add a Public Sub Subordinate_Change routine to clsTwoInput

Then, throughout, replace

RaiseEvent Change
with

If TypeName(Me.ParentTwoInput) = "clsTwoInput" Then
Me.ParentTwoInput Subordinate_Change
Else
RaiseEvent Change
End If
This will allow change events to be passed to clsTwoInput via Subordinate_Change and to other objects via the Change event.

I'll develop this after my day job.

Thanks.

Paul_Hossler
07-27-2015, 12:37 PM
Mike -- what is the reason(s) that you are creating your own classes? It seems easier to me to just stay with the 'standard' userform module and keep the control events there

mikerickson
07-27-2015, 03:50 PM
Why am I using Custom Controls?
Because the situation I'm dealing with involves a lot more control than this reduced example.
The simple "what is visible when" code becomes absurdly cumbersome when dealing with a dozen "InputTwo" groups of controls.
(Both the number of controls in a frame and the way they interact were simplified to highlight the circular dependency issue.)

Putting the mechanics of these duplicated controls in a Class Module, leaving the Userform code to handle the user's entries and its meaning,... that's what Class modules are for.

mikerickson
07-27-2015, 05:02 PM
Ok, I've changed the code for clsTwoInput in the OP user form. It's a lot smoother than work-around in post #12.

The code in the UF is unchanged, but here are the changes made to clsTwoInput.

Option Explicit
' in clsTwoInput Class module

Public WithEvents txtOneBox As MSForms.TextBox
Public WithEvents txtTwoBox As MSForms.TextBox
Public clOneInput As clsTwoInput
Public clTwoInput As clsTwoInput
Public ParentTwoInput As clsTwoInput

Public WithEvents chkChooser As MSForms.CheckBox

Event Change()

Property Get Name() As String
Name = "TwoInput" & chkChooser.Name
End Property

Property Get OneBox() As Object
If txtOneBox Is Nothing Then
Set OneBox = clOneInput
Else
Set OneBox = txtOneBox
End If
End Property
Property Set OneBox(InputObject As Object)
Set txtOneBox = Nothing
Set clOneInput = Nothing
Select Case TypeName(InputObject)
Case "TextBox"
Set txtOneBox = InputObject
Case "clsTwoInput"
Set clOneInput = InputObject
Set clOneInput.ParentTwoInput = Me
End Select
End Property

Property Get TwoBox() As Object
If txtTwoBox Is Nothing Then
Set TwoBox = clTwoInput
Else
Set TwoBox = txtTwoBox
End If
End Property
Property Set TwoBox(InputObject As Object)
Set txtTwoBox = Nothing
Set clTwoInput = Nothing
Select Case TypeName(InputObject)
Case "TextBox"
Set txtTwoBox = InputObject
Case "clsTwoInput"
Set clTwoInput = InputObject
Set clTwoInput.ParentTwoInput = Me
End Select
End Property

Property Get Value() As String
If OneBox.Visible Then
Value = OneBox.Value
Else
Value = TwoBox.Value
End If
End Property
Property Let Value(inValue As String)
If OneBox.Visible Then
OneBox.Value = inValue
Else
TwoBox.Value = inValue
End If
End Property

Property Get Visible() As Boolean
Visible = chkChooser.Parent.Visible
End Property
Property Let Visible(Visibility As Boolean)
chkChooser.Parent.Visible = Visibility
End Property

Private Sub chkChooser_Click()
OneBox.Visible = chkChooser.Value
TwoBox.Visible = Not (chkChooser.Value)
Call RaiseChange
End Sub

Public Sub SubordianteBox_Change(ChangedBox As Object)
If ChangedBox.Name = OneBox.Name Then
Call txtOneBox_Change
ElseIf ChangedBox.Name = TwoBox.Name Then
Call txtTwoBox_Change
End If
End Sub

Private Sub txtOneBox_Change()
Call RaiseChange
End Sub

Private Sub txtTwoBox_Change()
Call RaiseChange
End Sub

Private Sub RaiseChange()
If TypeName(ParentTwoInput) = "clsTwoInput" Then
ParentTwoInput.SubordianteBox_Change Me
Else
RaiseEvent Change
End If
End Sub

Thanks for all your ideas and help.