'Handling property change several layers down

I am new to events and handling them, and this is a simplified example of the structure I have:

Public Class Main
    Public world As World
    Public totalWorldMass As Double

    'this function needs to be called if mass changes on any object in the world.
    Sub RecalculateTotalMass()
        totalWorldMass = world.object1.mass + world.object2.mass + world.object3.mass
    End Sub
End Class

Public Class World
    Public Property object1 As objectWithMass
    Public Property object2 As objectWithMass
    Public Property object3 As objectWithMass

    'other properties and methods
End Class

Public Class ObjectWithMass
    Public Property name As String
    Public Property mass As Double

    'other properties and methods
End Class

Please disregard the flaws of structure itself - as I said, this is a simplified example, there are valid reasons to have this structure in the real code that are beyond the scope of this question.

In essence, what I need is for the Main to observe the world, and run RecalculateTotalMass if the mass changes in any of it's properties.

What I tried so far is to implement INotifyPropertyChanged in the ObjectWithMass class, declare object1, object2 and object3 in the World class with WithEvents, and then have this function in the main:

Public Sub HandleMassPropertyChanges() Handles world.object1.PropertyChanged, world.object2.PropertyChanged, world.object3.PropertyChanged
    RecalculateTotalMass()
End Sub

However, I cannot access world.object1.PropertyChanged like this. I can reach only as deep as world.PropertyChanged. However, if I implement INotifyPropertyChanged in the World instead of the ObjectWithMass, the event does not get fired, because the property is an object, and only the internal property of that object is changed.

Ideally, I would like not to specify what objects to handle in the HandleMassPropertyChanges() - I would like it to be called if mass changes on ANY property in the World. Not sure if this can be achieved without reflection, though.

How should I implement this?



Solution 1:[1]

My two cents on the matter.
When you have nested objects that need to notify Property value changes, you may want to implement INotifyPropertyChanged in all these objects.
Or, if using Windows Forms as GUI, follow the PropertyName / PropertyNameChanged pattern.
Both will let you bind all Properties that raise notification events.

Here's an alternative method, slightly less invasive, which make use of a simple Interface, which defines a method that is then called to generate all property change notifications.

Friend Interface IWorldModel
    Sub OnPropertyChanged(sender As Object, propertyName As String)
End Interface

Implementing the Interface in the top-level model, you can get Property change notifications from all nested classes that are made aware of the existence of the Interface.
In this case, the World class receives notifications when the Name and the Mass of any ObjectWithMass changes, so it can recalculate the TotalMass value, which also cause notification events (it also raises the PropertyChanged event).

In this implementation, all properties of all classes that are made aware of the Interface, can be used for data bindings (because all raise PropertyChanged notifications).
Unless the top-level class decides otherwise, based of whatever property value or any combination of values or other rules you can specify.

Friend Class World
    Implements IWorldModel
    Implements INotifyPropertyChanged

    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
    Private m_TotalMass As Double

    Public ReadOnly Property TotalMass As Double
        Get
            Return m_TotalMass
        End Get
    End Property

    Public Property Object1 As ObjectWithMass(Of IWorldModel)
    Public Property Object2 As ObjectWithMass(Of IWorldModel)
    Public Property Object3 As ObjectWithMass(Of IWorldModel)

    Protected Sub OnPropertyChanged(sender As Object, propertyName As String) Implements IWorldModel.OnPropertyChanged
        ' Using LINQ to get the value of the Mass properties. Any other method will do
        m_TotalMass = Me.GetType().GetProperties().
            Where(Function(p) p.PropertyType.IsGenericType AndAlso p.PropertyType.GenericTypeArguments.
                Any(Function(a) a.IsAssignableFrom(GetType(IWorldModel)))).
            Sum(Function(p) DirectCast(p.GetValue(Me), ObjectWithMass(Of IWorldModel))?.Mass).Value

        RaiseEvent PropertyChanged(sender, New PropertyChangedEventArgs(propertyName))
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(NameOf(TotalMass)))
    End Sub
End Class

Any class that defines a nested object type can then call the method specified by the Interface, the top-level class will intercept all notifications calls and decide what to do.

Clearly, your ObjectWithMass objects, in this implementation, are strictly tied to the top-level class and the Interface it implements.

Friend Class ObjectWithMass(Of T As IWorldModel)
    Private m_Name As String
    Private m_Mass As Double
    Private m_World As T

    Sub New(world As T, name As String, mass As Double)
        m_World = world
        Me.Name = name
        Me.Mass = mass
    End Sub

    Public Property Name As String
        Get
            Return m_Name
        End Get
        Set
            If m_Name <> Value Then
                m_Name = Value
                m_World.OnPropertyChanged(Me, NameOf(Name))
            End If
        End Set
    End Property

    Public Property Mass As Double
        Get
            Return m_Mass
        End Get
        Set
            If m_Mass <> Value Then
                m_Mass = Value
                m_World.OnPropertyChanged(Me, NameOf(Mass))
            End If
        End Set
    End Property
End Class

To create a World class object and assign the value of one of its properties, specify the Interface type and the parent class object that implements it:

Private worldObject As World = Nothing
' [...]
worldObject = New World()
worldObject.Object1 = New ObjectWithMass(Of IWorldModel)(worldObject, "World 1", 100.84545)
'[...]

This is how it works, when Controls (WinForms, here) are bound to the top-level class:

DataBindings Nested Classes

Solution 2:[2]

Change totalWorldMass to be a readonly property that returns the sum of the properties:

Public ReadOnly Property totalWorldMass As Double
    Get
        Return world.object1.mass + world.object2.mass + world.object3.mass
    End Get
End Property

Fiddle: https://dotnetfiddle.net/O9QHst

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 David