Home > Software design >  VBA - How to refresh parent class value when child value changes
VBA - How to refresh parent class value when child value changes

Time:02-06

What I'm trying to do / the problem:

I'm effectively building a task list with a tree format; this would be made up of two class types Task_List and Task.

Task_Lists contain other Task_Lists or Tasks and many of their properties are the sum of the 'same' properties of the children that it contains. For example a property might be Duration, and so the Duration of a Task_List would be the sum of all the Durations of the Tasks and Task_Lists contained within it.

I've worked out how to do most of this (albeit after being stuck for quite awhile trying to find out how to access a parent class from a child one), but what I'm struggling with is an eloquent way to update the properties of a Task_List when the relevant property of one of its children is updated. The two methods I can get to work right now are both, in my opinion, flawed; for different reasons.

Option 1)

Method: Every time a value is changed for parameter X in a child, trigger a public function in the parent which resets X in the parent, then iterates through all the children and sums X; this is then set as the parent's value.

My issue with this: Whilst this works it feels messy and also perhaps 'wasteful' in that it has to loop through all children any time one of them is changed. If there's a lot of tasks and each of them is changed this could stack-up.

Option 2)

Method: Have the Task_List also have a Let property for X, and have the child call it to first remove its old value of X, and then add its new value of X.

My issue with this: Less 'wasteful' than Option 1, however this leaves the parent open to having its value for X changed by something other than the child. It would be nice to restrict it in some way so that it can only be called by a child.

Option 3)

Method: Basically the same as Option 2 but instead of passing a value through the function call, pass the child in its entirety. Then have the parent check that what has been passed is actually one of its children (or at least that the child reports that the parent is its parent; not perfect but I think that would be good enough and it's easier to implement imo).

My issue with this: I can't get it to work; and I have a feeling in VBA it might not be possible?

Any help, guidance, etc. would be greatly appreciated; and if any further information/clarification is required please just say and I'll provide it via edits/comments :)

Simplified example code:

Task class module:

Private Type Task
   Name As String
   Value As Long
   Parent as Task_List
End Type

Private self As Task 

Public Sub Initialize(Parent as Task_List)
   Set self.Parent = Parent
End Sub

Public Property Get Parent() as Task_List
   Set Parent = Self.Parent
End Property

Property Let Name(ByVal Name As String)
   self.Name = Name   
End Property
 
Property Get Name() As String
   Name = self.Name
End Property

' I've put both options 2 & 3 here, but naturally only one would really be used at a time.
Property Let Value(ByVal Value As Long)

   Call self.Parent.Option_2(-self.Value)
   Call self.Parent.Option_3(self,-1)

   self.Value = Value

   Call self.Parent.Option_3(self,1)
   Call self.Parent.Option_2(self.Value)
 
End Property

Property Get Value() As Long
   Value = self.Value
End Property

Task list class:

Private Type Task_List

   Name As String
   SubTasks As New Collection
   Value As Long

End Type

Private self As Task_List

Public Property Get SubTasks() As Collection
   Set SubTasks = self.SubTasks
End Property

Public Property Let SubTasks(SubTask_Coll As Collection)
   Set self.SubTasks = SubTask_Coll
End Property

Property Let Name(ByVal Name As String)
   self.Name = Name
End Property

Property Get Name() As String
   Name = self.Name
End Property

Property Get Value() As Long
   Value = self.Value
End Property

Public Sub Option_2(ByVal Update_Value as long, _
                    Optional ByVal Multiplier as Long = 1)

   self.Value = self.Value   (Multiplier * Update_Value)

End Sub

Public Sub Option_3(ByVal Child_Task as Variant, _
                    Optional ByVal Multiplier as Long = 1)

   On Error Resume Next

      If Child_Task.Parent = self Then
         self.Value = self.Value   (Multiplier * Child_Task.Value)
      End If

   On Error GoTo 0

End Sub

Public Sub Option_1()

   Dim Cur_Sum As Long, Cur_Task as Variant
   For Each Cur_Task In self.SubTasks

      Cur_Sum = Cur_Sum   Cur_Task.Value

   Next Cur_Task
   self.Value = Cur_Sum

End Sub

Example of them in use:

Sub Example()

   Dim Main_List As New Task_List
   Main_List.Name = "Bake Bread"

   Dim T1 As New Task
   T1.Initialize Main_List
   Main_List.Subtasks.Add T1

   T1.Name = "Buy Ingredients"
   T1.Value = 250

   Dim T2 As New Task
   T2.Initialize Main_List
   Main_List.Subtasks.Add T2

   T2.Name = "Do Baking"
   T2.Value = 400

   Debug.Print Main_List.SubTasks(1).Name
   Debug.Print Main_List.SubTasks(2).Name

   Debug.Print Main_List.Value
   
End Sub

CodePudding user response:

So I've found the issue was trying to pass "self" when those needed to be "Me"s (I also 'TIL'ed that On Error Resume Next causes errors in If statements to 'activate' them as if they were True).

So the Update_Value function in the Task_List class becomes:

Public Sub Update_Value(ByVal Child_Task as Variant, _
                        Optional ByVal Multiplier as Long = 1)

   On Error GoTo Exit_Sub

      If Child_Task.Parent Is Me Then

         self.Value = self.Value   (Multiplier * Child_Task.Value)

      End If

   On Error GoTo 0

Exit_Sub:

End Sub

And the Let Value property of the Task class becomes:

Property Let Value(ByVal Value As Long)

   Call self.Parent.Update_Value(Me, -1)

   self.Value = Value

   Call self.Parent.Update_Value(Me, 1)

End Property

(In the real code I have an Enum for Multiplier to restrict it to just -1 and 1).

Additionally, I've used this ability (of using Me) to modify the Initialize sub in the Task class to also add it to the parent whilst defining the parent. Thus:

Public Sub Initialize(Parent as Task_List)
   
   Set self.Parent = Parent
   Parent.SubTasks.Add Me

End Sub
  • Related