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?
CodePudding user response:
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:
CodePudding user response:
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