A Chained Property Observer
Demo code source : ChainedPropertyObserver.zip (289 Kb)
Introduction
I have mixed feelings in writing up this article, 1/2 of me thinks it is a really cool idea and 1/2 of me tends to agree with my old team leader Fredrik Bornander, who when I explained what I wanted to do, simply uttered the single word "TrainWreck", and then later sent me a link to this wikipedia article :
Now there is a reason young Fredrik used to be my team leader, and in general he is normally pretty right, but I just felt there was something of interest/use with the code I showed him, so I plowed on, and wrote the code that I am demonstrating in this article, sorry Frerik no disrespect intended.
Like I say I may be presenting something here that some folk don't site well with, but I think the code attached to this article does have some merit and demonstrates some good .NET techniques even if you do not decide to use the overall utility classes that I present in this article.
So now that you have read this far I suppose I should tell you what the article is all about, so you can decide whether to read on or not...so here we go.
Some of you may be aware of a interface that is quite common place in pretty
much most UI programmig in windows, it is the INotifyPropertyChanged
interface which allows object that implements this interface to raise
change notification events that other objects/framework subsystems (such
as the binding subsystem) can listen to.
What this article will show is how we can create a INotifyPropertyChanged
observer that is able to monitor any object within a chain of INotifyPropertyChanged
objects, which may sound strange at first, but think about something like
this scenario.
We need a database that allows the storage of people, where each person has an address, and an address is linked through to some GeoLocation data. That sounds pretty valid to me, and a good normalized database will be littered with similar scenarios.
Now common sense and database normalization laws would dictate that we would end up with something that looked like this
So we would translate that into a object graph something like this
public class Person : INotifyPropertyChanged
{
private Address address;
public Address Address
{
...
}
...
...
}
public class Address : INotifyPropertyChanged
{
private GeoLocation geoLocaation;
public GeoLocation GeoLocaation
{
...
}
...
...
}
public class GeoLocation : INotifyPropertyChanged
{
...
...
}
Which is the sort of graph we would get if we were to manually code the object graph, or what we would get if we were to use something like LINQ to SQL, or LINQ to Entities.
One point about this, is that to my mind the database is normalized correctly, we are storing things where and how we should, which again to my mind makes the object graph heirachy is correct, but as you can see there is definately a chain of related objects there.
So now that we have a object heirachy like that (or any other example you can
think of), it (using my example) would not be too much of an outlandish a
request to say that when ever a Person
s details change, we should
do some action.
To clarify what I mean by "When a Person
s data changes", what I
really mean is if the Person
s data changes, or the
Person
s Address
changes, or the Person
s
Addresses
data changes we should do something,
that is what I really mean.GeoLocation
Imagine we want to send a new Invoice
when any part of the Person
s
data changes, when we are using a Person
object as part of a larger
process. Now using conventional methods I would have to hook up 3 listeners (one
for Person
one for the Person
s Address
,
and one for the Address
) in some top level
class which held an instance of a GeoLocation
Person
object, which in itself is
not that bad, but what about if a new Address
object is assigned to
that Person
object at some point, that we could not predict?
We would effectively have an INPC event handler (for the old Address
object of the Person
object) pointing to an out of date object that
we really should have unhooked, but when would we do that , how would we know,
we would have to listen to changes on the Person
object where the
property being changed is "Address", and unhook the old Address INPC
listener there, which would mean we would
then need to rehook a new INPC event handler for the Person
s Address.
Ok weak events can help with here, so we do not nessecarily have to worry
about unhooking, but we would still need to rehook to the new Person
s
Address
object.......as you can see there is quite a bit of house keeping
that needs to be done, my thoughts on this were why should I have to do that
house keeping, surely I could write some smart property observer that could do
all that jazz for me.
This article demonstrates a group of classes that make it possible to more easily monitor a change anywhere in a chain of objects, in fact you can choose what to parts of the chain you want to monitor changes on. So if this sounds like it could be of use to you read on, otherwise no worries, hope to see you at the next article.
Similar Ideas
Now although as far as I am aware there is no existing solution out there that does the same as the code I will present in this article, there is one other project that this article certainly took some inspiration from which is the excellent lambda bindings from my fellow WPF Disciple Philip Sumi, who had the idea to allow 2 lambda expressions to be passed to a small class that would then be bound to each other either OneWay, TwoWay, and it also supports ValueConverters.
Although there are certain similarities in that we are both dealing with Lambda expressions (as you will see below), what we are trying to achieve is quite different functionality, my code is all about chained INPC callbacks, whilst philips is all about binding one lambda expression to another.
Table Of Contents
This is what will be covered in this article
How The Code Solves The Problem
The main class that deals with most of the work is called "ChainPropertyObserver
".Here
is how it works in words
- We create an initial chain of nodes that observe the object in the chain
which is supplying by the lambda expression, these are represented
internally as a
List<ChainNode>
. TheList<ChainNode>
is obtained by obtaining string values for the property names in the lambda expression for the properties visited, and then a little bit of reflection is used to obtain actual objects which we store against the actualChainNode
instances. We also hook up an overall listener that is called back when any node in the chain changes - We also accept n-many number of specific callback handlers to watch
specific property changes for within the chain, these are represented
internally as a
List<PropertyNode>
. TheList<PropertyNode>
is obtained by obtaining string values for the property names in the lambda expression for the properties visited, and then a little bit of reflection is used to obtain actual objects which we store against the actualChainNode
instances. - When the specific callback handlers are added we lookup the the
ChainNode
that has the same object type and property name as the callback and assign the specific callback delegate that was registers for a specific property change to theChainNode
that matches. Basically we only obtain one set of nodes that fire the callback delegates and that is theList<ChainNode>
, so we must find the correct one and assign it the correct callback delegate - When a node value is detected as being changed, we obtain its position
in the chain, remove any nodes after and including it from the overall
List<ChainNode>
and then recreate the rest of the changed chain using the new object graph using a tiny bit of reflection. We also rehook up any specific property change to theChainNode
, as described in step 3
In essence what the ChainPropertyObserver
holds internally is
one sets of nodes (known as ChainNode
) to represent the original
lambda that was passed into the CreateChain(..)
method, and another
list of nodes to represent the lambdas that were passed in using the
RegisterChainLinkHandler(..)
method.
Lets consider a quick diagram or 2
Chain Creation Nodes
When we first start up and create a chain using the
ChainPropertyObserver.CreateChain(..)
method, we would end up with (depending on
the
lambda expression that was passed into the method CreateChain(..)
method) a List<ChainNode>
that
are held inside the ChainPropertyObserver
. Where each internal ChainNode
holds a
reference to the property of the original object tree as specified by the lambda
expression passed in.
So for example if we passed in a lambda of Person.Address.GeoLocation
we would end with an internal List<ChainNode>
to represent this
lambda expression that would contain 3 ChainNode
s configured as
such
ChainNode
with object reference toPerson
object property of which ever object held the Person instanceChainNode
with object reference toAddress
object property of thePerson
object in previousChainNode
ChainNode
with object reference toGeoLocation
object property of theAddress
object in previousChainNode
INPC Callback Nodes
When we register specific callbacks using the
ChainPropertyObserver.RegisterChainLinkHandler(..)
method, we pretty much follow
the same process as before except this time we end up making a
List<PropertyNode>
when we parse the lambda expression passed to the
ChainPropertyObserver.RegisterChainLinkHandler(..)
method.
So for example if we passed in a lambda of Person.Address.GeoLocation
and we supplied the following when we called the
ChainPropertyObserver.RegisterChainLinkHandler(..)
method twice we would end up
with the following List<PropertyNode>
created internally
For the RegisterChainHandler(() => Person.Address)
PropertyNode
with object reference toPerson
object property of which ever object held thePerson
instancePropertyNode
with object reference toAddress
object property of thePerson
object in previousPropertyNode
For the RegisterChainHandler(() => Person.Address.GeoLocation)
PropertyNode
with object reference toPerson
object property of which ever object held thePerson
instancePropertyNode
with object reference toAddress
object property of thePerson
object in previousPropertyNode
PropertyNode
with object reference toGeoLocation
object property of theAddress
object in previousPropertyNode
So I hope that makes some sort of sense. Anyway that is how it all works in words, but I think you lot would probably like to see some code now right. So lets crack on and have a look at that.
Parsing The Original Chain
So as we now know there is a method that is available on the ChainPropertyObserver which MUST be used to set an initial chain, where the usage of this method (from one of the demos) is like this:
MyPerson = new Person();
MyPerson.Age = 12;
MyPerson.Name = "sacha";
MyPerson.Address = new Address()
{
Addressline1 = "21 maple street",
Addressline2 = "Hanover",
GeoLocation = new GeoLocation { Longitude=0.5, Latitude=0.5 }
};
//create chainPropertyObserver
chainPropertyObserver = new ChainPropertyObserver();
chainPropertyObserver.CreateChain(() => MyPerson.Address.GeoLocation, Changed);
...
...
private void Changed(string propertyThatChanged)
{
lastChanges.Add(propertyThatChanged);
}
There are several things of note there, namely that we have a property called
MyPerson
of type Person
is whatever test class this is, and we supply a lambda
expression to the ChainPropertyObserver.CreateChain(..)
taking a overall
chain, and a overall change handler delegate. So what happens after that.
Well lets start at the top, which is the ChainPropertyObserver.CreateChain(..)
method
public class ChainPropertyObserver : ExpressionVisitor
{
public void CreateChain<TSource>(Expression<Func<TSource>> source,
Action<string> overallChangedCallback)
{
this.overallChangedCallback =
new WeakDelegateReference(overallChangedCallback);
Visit(source);
CreateNodes();
isInitialised = true;
}
}
It can be seen that the ChainPropertyObserver
inherits from the
System.Linq.Expressions.ExpressionVisitor
class, as such it allows
us to Visit(..)
an expression tree, which in our case is the
Expression<Func<TSource>> source
supplied to the
ChainPropertyObserver.CreateChain(..)
method. It can be seen above in
the ChainPropertyObserver.CreateChain(..)
method that we do indeed
call the base class (System.Linq.Expressions.ExpressionVisitor
class) Visit(..)
method. So lets have a look at what happens when we
Visit(..)
some expressions.
It is pretty simple we simply need to provide overrides for the expressions we wish to visit. Here they are:
/// <summary>
/// Visits a member within a Expression tree
/// So for example if we had a console app called "ExpressionTest.Program" that had a public property like
/// public Person MyPerson { get; set; } and we then passed in a overall expression to this
/// class of chainPropertyObserver.CreateChain(() => MyPerson.Address.GeoLocation)
///
/// This method would visit the following members in this order :
/// 1. GeoLocation
/// 2. Address
/// 3. MyPerson
/// </summary>
protected override Expression VisitMember(MemberExpression node)
{
if (!isInitialised)
{
membersSeen.Insert(0, node.Member.Name);
}
else
{
propMemberSeen.Insert(0, node.Member.Name);
}
return base.VisitMember(node);
}
/// <summary>
/// Visits a constant within a Expression tree
/// So for example if we had a console app called "ExpressionTest.Program" that had a public property like
/// public Person MyPerson { get; set; } and we then passed in a overall expression to this
/// class of chainPropertyObserver.CreateChain(() => MyPerson.Address.GeoLocaation)
///
/// This method would visit a constant of the ExpressionTest.Program type object
/// </summary>
protected override Expression VisitConstant(ConstantExpression node)
{
if (!isInitialised)
{
wrRoot = new WeakReference(node.Value);
}
else
{
wrPropRoot = new WeakReference(node.Value);
}
return base.VisitConstant(node);
}
Visit MemberExpression
It can be seen that we store 2 things, when we visit a MemberExpression
we maintain a list of visited member names which are really just string
names of the properties we have seen in the expression tree. We maintain 2 lists
as we use these same visit methods to create ChainNode
and
PropertyNode
, so we just store things in 2 internal lists, "memberSeen
"
for ChainNode
s and "propMemberSeen
" for
PropertyNode
s.
Visit ConstantExpression
It can be seen that we store 2 things, when we visit a ConstantExpression we
maintain a 2 objects which are really just the original objects that hold the
source objects we have seen in the expression tree. We maintain 2 objects as we
use these same visit methods to create ChainNode
and
PropertyNode
, so we just store things in 2 internal objects, "wrRoot
"
for ChainNode
s and "wrPropRoot
" for PropertyNode
s.
After we have visited the entire expression tree we would end up with
something like this being stored in the ChainPropertyObserver
.
This assumes we are in one of the demo apps, that is why it is pointing to a
WeakReference
wrapped object that is the WPF app.
The last thing that happens is that an internal list of ChainNode
(which inherit from NodeBase
) are created using a bit of
relection, where we make use of the wrRoot
and the WeakReference
membersSeen List<string>
. These 2 bits of
information are enough to allow us to full reflect out an actual chain of
objects, which we store in a List<ChainNode>
, which internally also
use WeakReference
s to wrap the object that the node represents.
Here is how the CreateNodes method works.
private void CreateNodes()
{
ChainNode parent = null;
bool chainBroken = false;
for (int i = 0; i < membersSeen.Count; i++)
{
parent = (ChainNode)CreateNode(parent, membersSeen[i], NodeType.ChainNode, out chainBroken);
//only create new ChainNodes as long as the chain is entact, and has no null object references
if (!chainBroken)
{
nodes.Add(parent);
parent.DependencyChanged += Parent_DependencyChanged;
}
else
break;
}
}
private object CreateNode(NodeBase parent, string propNameForNewChild,
NodeType nodeType, out bool chainBroken)
{
object nodeValue;
if (parent == null)
{
object activeRoot = null;
if (wrRoot.IsAlive)
{
activeRoot = wrRoot.Target;
}
if (activeRoot == null)
throw new InvalidOperationException("Root has been garbage collected");
nodeValue = activeRoot.GetType().GetProperty(
propNameForNewChild).GetValue(activeRoot, null);
if (nodeValue== null)
{
chainBroken=true;
return null;
}
}
else
{
nodeValue = parent.NodeValue.GetType().GetProperty(propNameForNewChild)
.GetValue(parent.NodeValue, null);
if (nodeValue == null)
{
chainBroken = true;
return null;
}
}
if (!(nodeValue is INotifyPropertyChanged))
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("ChainedObserver is only able to work with objects " +
"that implement the INotifyPropertyChanged interface");
sb.AppendLine("All objects in a chain MUST implement the INotifyPropertyChanged interface");
throw new InvalidOperationException(sb.ToString());
}
NodeBase node = null;
switch (nodeType)
{
case NodeType.ChainNode:
node = new ChainNode((ChainNode)parent, nodeValue, propNameForNewChild);
break;
case NodeType.PropertyNode:
node = new PropertyNode((PropertyNode)parent, nodeValue, propNameForNewChild);
break;
}
chainBroken = false;
return node;
}
Some of you may notice that the CreateNode(..)
method also has the capability to create
PropertyNode
s, we will get to that shortly.
Just to complete the picture this is what a ChainNode
looks like
along with the other node types it inherits from:
using System;
using System.ComponentModel;
using ChainedObserver.Weak;
using ChainedObserver.WeakEvents;
namespace ChainedObserver
{
/// <summary>
/// Represents a node in the original Lambda expression that was
/// passed to the <c>ChainPropertyObserver</c>. This class also
/// provides an overall DependencyChanged weak event
/// (using Daniel Grunwalds<c>WeakEvent</c>) that is raised whenever
/// the property that this <c>ChainNode</c> is monitoring changes.
/// Basically this node hooks a <c>NotifyPropertyChanged</c>
/// listener to a source object to know when a overall dependency changes.
/// When the dependency changes the DependencyChanged is raied which
/// allows an overall callback delegate to be called when any node in the
/// chain within the original Lambda expression changes. There is also
/// a callback here may run a specific callback handler that was
/// registered within the <c>ChainPropertyObserver</c>. This callback
/// may or may not be null for any given <c>ChainNode</c>, it depends
/// on what property this <c>ChainNode</c> is monitoring from the original
/// Lambda expression, as to whether there will be an active specific
/// callback assigned for a given <c>ChainNode</c>
///
/// When Dispose() is called this class will unhook any <c>NotifyPropertyChanged</c>
/// listener that it has previously subscribed to
/// </summary>
public class ChainNode : GenericNodeBase<ChainNode>, IDisposable
{
private WeakEventHandler<PropertyChangedEventArgs> inpcHandler;
public ChainNode(ChainNode parent, object value, String name) : base(parent,value,name)
{
inpcHandler = new WeakEventHandler<PropertyChangedEventArgs>(PropertyChanged);
((INotifyPropertyChanged)value).PropertyChanged += inpcHandler.Handler;
}
public WeakDelegateReference PropertyChangedCallback { get; set; }
private void PropertyChanged(object sender, PropertyChangedEventArgs e)
{
//raise overall dependencyChangedEvent
dependencyChangedEvent.Raise(this, new ChainNodeEventArgs(this));
//now callback specific handler for this Node type/name
if (PropertyChangedCallback != null)
{
Action callback = (Action)PropertyChangedCallback.Target;
if (callback != null)
{
callback();
}
}
}
private readonly WeakEvent<EventHandler<ChainNodeEventArgs>> dependencyChangedEvent =
new WeakEvent<EventHandler<ChainNodeEventArgs>>();
public event EventHandler<ChainNodeEventArgs> DependencyChanged
{
add { dependencyChangedEvent.Add(value); }
remove { dependencyChangedEvent.Remove(value); }
}
public void Dispose()
{
((INotifyPropertyChanged)base.NodeValue).PropertyChanged -= inpcHandler.Handler;
}
}
}
using System;
namespace ChainedObserver
{
/// <summary>
/// Generic base class for all Nodes used. Simply
/// introduces a parent of type T, and passes
/// value and name constructor params to <c>NodeBase</c>
/// base class
/// </summary>
public class GenericNodeBase<T> : NodeBase
{
public GenericNodeBase(T parent, object value, String name)
: base(value, name)
{
this.Parent = parent;
}
public T Parent { get; set; }
}
}
using System;
namespace ChainedObserver
{
/// <summary>
/// Provides a common base class for all nodes.
/// Provides 2 properties :
/// <list type="Bullet">
/// <item>PropertyName : which is the name of the
/// property the node is monitoring from the overall lambda
/// expression provided to the
/// <c>ChainedPropertyObserver</c></item>
/// <item>NodeValue : Holds a WeakReference to the property
/// value the node is monitoring from the overall lambda expression
/// provided to the <c>ChainedPropertyObserver</c></item>
/// </list>
/// </summary>
public class NodeBase
{
private WeakReference wrNodeValue;
public NodeBase(object value, String name)
{
this.PropertyName = name;
wrNodeValue = new WeakReference(value);
}
public String PropertyName { get; set; }
public object NodeValue
{
get
{
object target = wrNodeValue.Target;
if (target != null)
{
return target;
}
return null;
}
}
}
}
Parsing The Registered Chain Callbacks
When a peice of user code calls the ChainedPropertyObserver RegisterChainLinkHandler(Expression<Func<object>> handlerExpression, Action propertyChangedCallback)
method, the
parsing of the handler expression is handled much the same way as it was for
when we parsed the orginal expression that was provided when the
ChainedPropertyObserver.CreateChain(..)
method was first called.
There are 2 other things that need to happen:
- We need to store an entry in an internal Dictionary which represents the
last object seen in the handlerExpression which was supplied in the
ChainedPropertyObserver.RegisterChainLinkHandler(..)
method. We choose the last node as this is obviously what the user intended when they supplied the handlerExpression - We also traverse the orginal
List<ChainNode>
trying to find theChainNode
that is a match for the specific handler the user supplied, and when we find the originalChainNode
we assign the callback delegate to it.
As elsewhere we do not actually hold a delegate (that would require a strong
reference), we use a WeakDelegateReference
to allow the original
object to be GC'd.
Lets now have a look at the relevant code:
public void RegisterChainLinkHandler(
Expression<Func<object>> handlerExpression,
Action propertyChangedCallback)
{
handlers.Add(handlerExpression, new WeakDelegateReference(propertyChangedCallback));
RecreateHandlers();
}
So all we do here is add the handlerExpression and a WeakDelegateReference
which wraps the original
callback delegate are added to an internal dictionary, and then the RecreateHandlers()
mehod is called. Let's have
a look at that now.
private void RecreateHandlers()
{
existingPropHandlerNodes.Clear();
foreach (KeyValuePair<Expression<Func<object>>,
WeakDelegateReference> handler in handlers)
{
propMemberSeen.Clear();
Visit(handler.Key);
CreatePropertyHandlerNodes();
//only add in existing PropertyHandler if we managed to create an entire
//chain of nodes, ie no nulls in chain
if (workingPropHandlerNodes.Last().PropertyName == propMemberSeen.Last())
{
existingPropHandlerNodes.Add(workingPropHandlerNodes.Last(), handler.Value);
}
}
ReCreateChainLinkHandlers();
}
It can be seen that this method simply loops through each Key/Value pair in
the maintained Dictionary
of specific callbacks that were registered. We then
traverse the handlerExpression in much the same way as we did before, and we
also create a List<PropertyNode>
for the handlerExpression visited.
The last peice to the puzzle is to use the just created List<PropertyNode>
to
look through the original List<ChainNode>
and find the one that matches the last
PropertyNode
using predefined matching rules. When a ChainNode
is matched, it
simply has its PropertyChangedCallback
assigned to the WeakDelegateReference
wrapped callback delegate. This is done using the
ReCreateChainLinkHandlers()
method as shown below.
private void ReCreateChainLinkHandlers()
{
foreach (KeyValuePair<PropertyNode,
WeakDelegateReference> propCallBack in existingPropHandlerNodes)
{
var matchedNodes = (from n in nodes
where n.PropertyName == propCallBack.Key.PropertyName &&
n.NodeValue.GetType() == propCallBack.Key.NodeValue.GetType()
select n);
if (matchedNodes.Count() == 1)
{
ChainNode actualNode = matchedNodes.First();
actualNode.PropertyChangedCallback = propCallBack.Value;
}
}
}
Just for completeness this is what a PropertyNode
looks like, you saw the
base class previously:
using System;
namespace ChainedObserver
{
/// <summary>
/// A convience class that simply inherits from <c>GenericNodeBase of T</c>
/// where T is PropertyNode
/// </summary>
public class PropertyNode : GenericNodeBase<PropertyNode>
{
#region Ctor
public PropertyNode(PropertyNode parent, object value, String name)
: base(parent,value,name)
{
}
#endregion
}
}
Listening To Changes In The Chain
One of the rather neat things about the ChainPropertyObserver
is
that it is kind of mends a broken link when a new object is assigned to any part
of the already observer chain, which we are monitoring using the internal
List<ChainNode>
.
In words what happens is as follows:
When a ChainNode nodeValue
is detected as being changed it fires
its DependencyChanged
event, which is listened to by the ChainPropertyObserver
,
when this event is seen from any of the internally held
List<ChainNode>
, we obtain the position of the changed ChainNode
within the internally held List<ChainNode>
, remove any nodes after and including it from the overall
List<ChainNode>
and then recreate the rest of the changed chain using
the new object graph using a tiny bit of reflection (basically the same as
we used to create the initial List<ChainNode>
by traversing the
list of membersSeen
as we went through already.
We also rehook up any
specific property change to the ChainNode
, that were
registered.
By carrying out these steps the attempts to recreate the original chain that
we wish to observer (as specified by original lambda) and also attempts to hook
up any specific callbacks that were registered to the current objects in the
List<ChainNode>
.
Time for some code I feel.
If we look at what happens when the ChainPropertyObserver
detects that one of its observed ChainNodes has changed.
private void Parent_DependencyChanged(object sender, ChainNodeEventArgs e)
{
int indexOfChangedNode = nodes.IndexOf(e.Node);
for (int i = indexOfChangedNode; i < nodes.Count; i++)
{
nodes[i].DependencyChanged -= Parent_DependencyChanged;
}
RecreateNodes((ChainNode)e.Node.Parent, indexOfChangedNode);
Action<string> callback = (Action<string>)overallChangedCallback.Target;
if (callback != null)
{
callback(e.Node.PropertyName);
}
}
We can see that we work out the index of the ChainNode
that changed, and then
make a call to the RecreateNodes(..)
method, where the changed part of the chain
will be removed and a new section of the chain will be created to replace the
changed section.
private void RecreateNodes(ChainNode oldParent, int indexOfChangedNode)
{
//remove changed section of chain, and also clean the removed node up
for (int i = nodes.Count - 1; i >= indexOfChangedNode; i--)
{
ChainNode node = nodes[i];
nodes.Remove(node);
node.Dispose();
node = null;
}
//recreate new section of chain
ChainNode parent = oldParent;
bool chainBroken = false;
for (int i = indexOfChangedNode; i < membersSeen.Count; i++)
{
parent = (ChainNode)CreateNode(parent, membersSeen[i],
NodeType.ChainNode, out chainBroken);
//only carry on creating the chain while the chain is
//intact (ie no null references seen)
if (!chainBroken)
{
nodes.Add(parent);
}
else
break;
}
for (int i = indexOfChangedNode; i < nodes.Count; i++)
{
nodes[i].DependencyChanged += Parent_DependencyChanged;
}
//recreate and attach specific callback handlers
RecreateHandlers();
}
After the ChainNode
s are recreated and reassigned the specific callbacks, we simply call the overall changed
callback that was supplied when the user called the ChainPropertyObserver.CreateChain(..)
method
How Do The Specific Callbacks Fire
Ok so we have done loads, but you are probably wondering how the specific callback that we assigned (as already described) are actually called. This is easy its all down to the following code in the ChainNode, which is the only node type that calls back the specific change callback that are registered.
public class ChainNode : GenericNodeBase<ChainNode>, IDisposable
{
private WeakEventHandler<PropertyChangedEventArgs> inpcHandler;
public ChainNode(ChainNode parent, object value, String name) : base(parent,value,name)
{
inpcHandler = new WeakEventHandler<PropertyChangedEventArgs>(PropertyChanged);
((INotifyPropertyChanged)value).PropertyChanged += inpcHandler.Handler;
}
public WeakDelegateReference PropertyChangedCallback { get; set; }
private void PropertyChanged(object sender, PropertyChangedEventArgs e)
{
//raise overall dependencyChangedEvent
dependencyChangedEvent.Raise(this, new ChainNodeEventArgs(this));
//now callback specific handler for this Node type/name
if (PropertyChangedCallback != null)
{
Action callback = (Action)PropertyChangedCallback.Target;
if (callback != null)
{
callback();
}
}
}
public void Dispose()
{
((INotifyPropertyChanged)base.NodeValue).PropertyChanged -= inpcHandler.Handler;
}
}
Dealing With Nulls In The Chain
When I 1st published this article a reader correctly stated that it would only work if all of the objects in the chain were pointing to non null object references, which was correct, an oversight for sure.
I had a small think about that, and have now reworked the code to deal with nulls. I will not go into all the inner workings of this but there are a few rules that are adhered to in the attached code, which we will go through below. Understanding these rules hould be enough to show you how it works internally:
Say you have an object graph that looks like this:
Person p = new Person();
p.Age = 12;
p.Name = "sacha";
p.Address = new Address() {
Addressline1="21 maple street",
Addressline2 = "Hanover",
GeoLocation = new GeoLocation { Longitude=0.555, Latitude=0.787 } };
Lets now look at a few use cases.
- If we have a specific listener registered on
GeoLocation
andAddress
is set to null, we will NOT get an callback on theGeoLocation
specific callback until we have aGeoLocation
object in the chain that is assigned to a non nullAddress
object - If we have a specific listener registered on
Address
andGeoLocation
is set to null, we will still get an callback on theAddress
specific callback asGeoLocation
is further down the chain.
I think that pretty much sums up how null object references are dealt with in a chain, the code in this article shows more details as does the Console Demo at the bottom of this article.
Clense
The ChainPropertyObserver
does use WeakReference
s/WeakEvent
s almost everywhere, but
there is also a simple house cleaning method that you can use when you are
completely finished with the ChainPropertyObserver
which will simply calls
Dispose()
on all its currently held List<ChainNode>
You can use this house cleaning as simply as this:
ChainPropertyObserver.Clense();
Known Issues / The Importance Of Weakness
As you can imagine maintained a chain of objects where any part of the chain could be swapped out under you feet at any moment is bad enough, but the attached demo code not only has to do that, it needs to know when this occcurs, and re-attach a registered callback handler delegate reference to the new object in the chain.
Now if you just read that paragraph and thought nothing was up with all of that, think again. We have a chain of objects A->B->C and we are not only holding references but we are hooking up callback handlers to these object, but what happens when the chain changed to A->B1-C, we still hold a reference to B somewhere, does that not sound like this little supposedly helpful utility of ours is hanging on to object references, sure sounds like it right?
If you think so too you would be correct, and it is a real concern, so we need to
do something about that, so what can we do, well luckily the .NET framework has
the answer in the form of the WeakReference
class, which is a very helpful class that
allows us to wrap an object reference but the original object we wrapped can
still be garbage collected.
Within the ChainPropertyObserver
code that I present in this article
nearly everything is weak, from the holding of object references in the chain
(which allows old redundant chain objects to be GCd), to the holding of callback
delegates, to custom events...They are all weak. Basically weak is good in the
scenario that the ChainPropertyObserver
is trying to solve. Let's see
some examples shall we.
Lambda Object References
When we first visit the expression tree constant nodes we use WeakReferences
to hold the constants (object at very top of object graph lmabda is
representing) as can be seen from the following
code from the ChainPropertyObserver
protected override Expression VisitConstant(ConstantExpression node)
{
if (!isInitialised)
{
wrRoot = new WeakReference(node.Value);
}
else
{
wrPropRoot = new WeakReference(node.Value);
}
return base.VisitConstant(node);
}
ChainNode/PropertyNode Object References
As we know each NodeBase
item (and ChainNode/PropertyNode
both inherit from NodeBase
) in the chain will hold a
NodeValue
, this is also the
case when constructing the registered callback chains too, so let's see how
these objects are stored against a NodeBase
object
The actual NodeBase
object looks like this
public class NodeBase
{
private WeakReference wrNodeValue;
public NodeBase(object value, String name)
{
this.PropertyName = name;
wrNodeValue = new WeakReference(value);
}
public String PropertyName { get; set; }
public object NodeValue
{
get
{
object target = wrNodeValue.Target;
if (target != null)
{
return target;
}
return null;
}
}
}
And when we create a NodeBase
derived class (either PropertyNode
or ChainNode
we would do
something like this
new ChainNode((ChainNode)parent, nodeValue, propNameForNewChild);
Where the nodeValue
is a reference to an object in the original
chain Expression
. So we really need to wrap that object reference
up in a WeakReference
, luckily the NodeBase
class does
that for us, as shown in the code snippet for NodeBase
above.
Registered CallBacks
Another area where we need WeakReferences is when we register the specific callback delegates that will eventually be assigned to a ChainNode. We handle this using a custom WeakDelegateReference
Here is how the callbacks are registered:
public void RegisterChainLinkHandler(
Expression<Func<object>> handlerExpression,
Action propertyChangedCallback)
{
handlers.Add(handlerExpression, new WeakDelegateReference(propertyChangedCallback));
.....
}
And here is the WeakDelegateReference code:
using System;
using System.Reflection;
namespace ChainedObserver.Weak
{
/// <summary>
/// A simple weak Delegate helper, that will maintain a Delegate
/// using a WeakReference but allow the original Delegate to be
/// reconstrcuted from the WeakReference
/// </summary>
public class WeakDelegateReference
{
private readonly Type delegateType;
private readonly MethodInfo method;
private readonly WeakReference weakReference;
public WeakDelegateReference(Delegate @delegate)
{
if (@delegate == null)
{
throw new ArgumentNullException("delegate");
}
this.weakReference = new WeakReference(@delegate.Target);
this.method = @delegate.Method;
this.delegateType = @delegate.GetType();
}
public Delegate Target
{
get
{
return this.TryGetDelegate();
}
}
private Delegate TryGetDelegate()
{
if (this.@method.IsStatic)
{
return Delegate.CreateDelegate(this.@delegateType, null, this.@method);
}
object target = this.@weakReference.Target;
if (target != null)
{
return Delegate.CreateDelegate(this.@delegateType, target, this.@method);
}
return null;
}
}
}
Chain Node Dependency Changes
Another area of concern is the fact that each item in the chain of objects
that we are holding a WeakReference
for (thanks to NodeBase
)
to is a INPC based object which exposes a PropertyChanged
event
that is being listened to. Now as I have also stated, the
ChainPropertyObserver
is also capable of reconstructing a chain whenever a new object is
introduced into the chain, but we still hooked up a listener to the old objects
INPC PropertyChanged
, sounds like trouble to me.
So we have to ask ourselves if we are hooking into some old objects
PropertyChanged
event, should we not also be unhooking? Or even better
listening to all the INPC objects using some sort of weak event handler. This
2nd approach is the one I choose to do.
Again this happens automatically for you both by holding a
WeakReference
object for NodeValue
which is done in
NodeBase
, and the weak event listening is done in ChainNode
.
If we look at the relevant parts from the ChainNode
code we can see
how the NodeBase
NodeValue
WeakReference
d
objects INPC PropertyChanged
event is listened to using a
weak event
listener. The
weak event
listener is by fellow WPF disciple
Paul Stovell.
I also unhook the
weak event
listener in an implementation of the IDisposable
which is
implemented by ChainNode
.
The other thing that is done in ChainNode
is that it also
exposes a DependencyChanged
event is response to the NodeBase
NodeValue WeakReference
d objects INPC PropertyChanged
event
changing. This ChainNode.DependencyChanged
event is what the
ChainPropertyObserver
uses internally. This event is actual a WeakEvent
right in the ChainNode
. This WeakEvent
is from
Daniel Grunwalds excellent codeproject article:
http://www.codeproject.com//KB/cs/WeakEvents.aspx
Anyway here is the relevant code from ChainNode
, that deals with
all this:
using System;
using System.ComponentModel;
using ChainPropertyObserver.Weak;
using ChainPropertyObserver.WeakEvents;
namespace ChainPropertyObserver
{
/// <summary>
/// Represents a node in the original Lambda expression that was
/// passed to the <c>ChainPropertyObserver</c>. This class also
/// provides an overall DependencyChanged weak event
/// (using Daniel Grunwalds<c>WeakEvent</c>) that is raised whenever
/// the property that this <c>ChainNode</c> is monitoring changes.
/// Basically this node hooks a <c>NotifyPropertyChanged</c>
/// listener to a source object to know when a overall dependency changes.
/// When the dependency changes the DependencyChanged is raied which
/// allows an overall callback delegate to be called when any node in the
/// chain within the original Lambda expression changes. There is also
/// a callback here may run a specific callback handler that was
/// registered within the <c>ChainPropertyObserver</c>. This callback
/// may or may not be null for any given <c>ChainNode</c>, it depends
/// on what property this <c>ChainNode</c> is monitoring from the original
/// Lambda expression, as to whether there will be an active specific
/// callback assigned for a given <c>ChainNode</c>
///
/// When Dispose() is called this class will unhook any <c>NotifyPropertyChanged</c>
/// listener that it has previously subscribed to
/// </summary>
public class ChainNode : GenericNodeBase<ChainNode>, IDisposable
{
private WeakEventHandler<PropertyChangedEventArgs> inpcHandler;
public ChainNode(ChainNode parent, object value, String name) : base(parent,value,name)
{
inpcHandler = new WeakEventHandler<PropertyChangedEventArgs>(PropertyChanged);
((INotifyPropertyChanged)value).PropertyChanged += inpcHandler.Handler;
}
public WeakDelegateReference PropertyChangedCallback { get; set; }
private void PropertyChanged(object sender, PropertyChangedEventArgs e)
{
//raise overall dependencyChangedEvent
dependencyChangedEvent.Raise(this, new ChainNodeEventArgs(this));
//now callback specific handler for this Node type/name
if (PropertyChangedCallback != null)
{
Action callback = (Action)PropertyChangedCallback.Target;
if (callback != null)
{
callback();
}
}
}
private readonly WeakEvent<EventHandler<ChainNodeEventArgs>>
dependencyChangedEvent =
new WeakEvent<EventHandler<ChainNodeEventArgs>>();
public event EventHandler<ChainNodeEventArgs> DependencyChanged
{
add { dependencyChangedEvent.Add(value); }
remove { dependencyChangedEvent.Remove(value); }
}
public void Dispose()
{
((INotifyPropertyChanged)base.NodeValue).PropertyChanged -= inpcHandler.Handler;
}
}
}
Ayway I hope going through this areas of concern, and how the
ChainPropertyObserver
handles these scenarios sets your mind at rest.
How To Use It
I think the code presented in this article is pretty easy to use, and basically comes down to these 3 steps
1. Create the ChainPropertyObserver
This is done just like this:
chainPropertyObserver = new ChainPropertyObserver();
2. Create some chain you wish to monitor, with an overall changed callback
This is done just like this:
chainPropertyObserver.CreateChain(() => MyPerson.Address.GeoLocation, Changed);
3. Optionally register a specific callback handler for a part of the chain
This is done just like this:
//And now hook up some specific Chain Listeners
chainPropertyObserver.RegisterChainLinkHandler(() => MyPerson.Address,
() =>
{
//Do whatever
});
The ChainPropertyObserver
also has a Clense()
method that you should call when you are completely done
with the ChainPropertyObserver
. I have not shown this in the demos, as it is essentially your call when
to call this Clense()
method, that is up to you. As I have also said all object references/events etc etc are done using
WeakReference
s so there should be no issues with memory being held, but you should call this method
if you can.
NOTE : Some people will more than likely say why did you not make it
implement IDisposable
which would allow the use of a
using(..)
statement, but that would be weird, as the code
would look something like this if I were to allow the use of a
using(..)
statement by implementing IDisposable
//create chainPropertyObserver
using (new ChainPropertyObserver())
{
chainPropertyObserver.CreateChain(() => MyPerson.Address.GeoLocation, Changed);
//And now hook up some specific Chain Listeners
chainPropertyObserver.RegisterChainLinkHandler(() => MyPerson,
() =>
{
personChanges.Add(MyPerson.ToString());
});
chainPropertyObserver.RegisterChainLinkHandler(() => MyPerson.Address,
() =>
{
addressChanges.Add(MyPerson.Address.ToString());
});
chainPropertyObserver.RegisterChainLinkHandler(() => MyPerson.Address.GeoLocation,
() =>
{
geoLocationChanges.Add(MyPerson.Address.GeoLocation.ToString());
});
}
So what would happen there is that the IDisposable.Dispose()
method would be called at the end of the using(..)
statement and
the ChainPropertyObserver
would not work correctly. So I opted for a
manual cleanup method, sorry about that.
Anyway now that you know how to use the attached codem lets have a quick look at the 2 demos I provide with the download code.
Console Demo
This is a pretty simple Console app example, where we simple follow the 3 mandatory steps as I just described, here is the complete demo code:
using System;
using System.Collections.Generic;
using CommonModel;
namespace ChainedObserver.Test
{
class Program
{
public Person MyPerson { get; set; }
public void Run()
{
//create INPC model
Person p = new Person();
p.Age = 12;
p.Name = "sacha";
p.Address = new Address() {
Addressline1="21 maple street",
Addressline2 = "Hanover",
GeoLocation = new GeoLocation { Longitude=0.555, Latitude=0.787 } };
MyPerson = p;
//create observer
ChainPropertyObserver chainPropertyObserver = new ChainPropertyObserver();
chainPropertyObserver.CreateChain(() => MyPerson.Address.GeoLocation, Changed);
List<string> personChanges = new List<string>();
List<string> addressChanges = new List<string>();
List<string> geoLocationChanges = new List<string>();
//create some INPC listeners
chainPropertyObserver.RegisterChainLinkHandler(() => MyPerson,
() =>
{
Console.WriteLine("The Person that changed is now : {0}", MyPerson.ToString());
personChanges.Add(MyPerson.ToString());
});
chainPropertyObserver.RegisterChainLinkHandler(() => MyPerson.Address,
() =>
{
Console.WriteLine("The Address that changed is now : {0}",
MyPerson.Address.ToString());
addressChanges.Add(MyPerson.Address.ToString());
});
chainPropertyObserver.RegisterChainLinkHandler(() => MyPerson.Address.GeoLocation,
() =>
{
Console.WriteLine("The GeoLocation that changed is now : {0}",
MyPerson.Address.GeoLocation.ToString());
geoLocationChanges.Add(MyPerson.Address.GeoLocation.ToString());
});
//Chain the Chain data, including setting new objects in the chain
MyPerson.Address = new Address()
{
Addressline1 = "45 there street",
Addressline2 = "Pagely",
GeoLocation = new GeoLocation { Longitude = 0.5, Latitude=0.5 }
};
MyPerson.Address = new Address()
{
Addressline1 = "101 new town road",
Addressline2 = "Exeter",
GeoLocation = new GeoLocation { Longitude = 0.5, Latitude = 0.5 }
};
MyPerson.Address = null;
MyPerson.Address = new Address()
{
Addressline1 = "12 fairweather Road",
Addressline2 = "Kent",
GeoLocation = new GeoLocation { Longitude = 0.5, Latitude = 0.5 }
};
MyPerson.Address = null;
MyPerson.Address = new Address()
{
Addressline1 = "45 plankton avenue",
Addressline2 = "bristol",
GeoLocation = new GeoLocation { Longitude = 0.5, Latitude = 0.5 }
};
MyPerson.Address.GeoLocation = new GeoLocation { Longitude = 0.49, Latitude = 0.49 };
MyPerson.Address.GeoLocation = new GeoLocation { Longitude = 0.49, Latitude = 0.49 };
MyPerson.Address.GeoLocation = null;
MyPerson.Address.GeoLocation = new GeoLocation { Longitude = 0.66, Latitude = 0.71 };
MyPerson.Address.GeoLocation.Longitude = 0.51;
MyPerson.Address.GeoLocation.Longitude = 0.52;
MyPerson.Address.GeoLocation.Longitude = 0.53;
MyPerson.Address.GeoLocation.Longitude = 0.54;
MyPerson.Address.GeoLocation.Longitude = 0.54;
Console.ReadLine();
}
private void Changed(string propertyThatChanged)
{
Console.WriteLine("The property that changed was : {0}", propertyThatChanged);
}
static void Main(string[] args)
{
Program p = new Program();
p.Run();
}
}
}
The most important part of this to demo code to actually prove things worked as expected is the changes to the objects in the chain, so to recap we did this
//Chain the Chain data, including setting new objects in the chain
MyPerson.Address = new Address()
{
Addressline1 = "45 there street",
Addressline2 = "Pagely",
GeoLocation = new GeoLocation { Longitude = 0.5, Latitude=0.5 }
};
MyPerson.Address = new Address()
{
Addressline1 = "101 new town road",
Addressline2 = "Exeter",
GeoLocation = new GeoLocation { Longitude = 0.5, Latitude = 0.5 }
};
MyPerson.Address = null;
MyPerson.Address = new Address()
{
Addressline1 = "12 fairweather Road",
Addressline2 = "Kent",
GeoLocation = new GeoLocation { Longitude = 0.5, Latitude = 0.5 }
};
MyPerson.Address = null;
MyPerson.Address = new Address()
{
Addressline1 = "45 plankton avenue",
Addressline2 = "bristol",
GeoLocation = new GeoLocation { Longitude = 0.5, Latitude = 0.5 }
};
MyPerson.Address.GeoLocation = new GeoLocation { Longitude = 0.49, Latitude = 0.49 };
MyPerson.Address.GeoLocation = new GeoLocation { Longitude = 0.49, Latitude = 0.49 };
MyPerson.Address.GeoLocation = null;
MyPerson.Address.GeoLocation = new GeoLocation { Longitude = 0.66, Latitude = 0.71 };
MyPerson.Address.GeoLocation.Longitude = 0.51;
MyPerson.Address.GeoLocation.Longitude = 0.52;
MyPerson.Address.GeoLocation.Longitude = 0.53;
MyPerson.Address.GeoLocation.Longitude = 0.54;
MyPerson.Address.GeoLocation.Longitude = 0.54;
Now based on that I would expect to see the following to occur, notice how the ChainedPropertyObserver recovers after seeing a null object reference in the chain, it just recovers.
Person
changes due to newAddress
object getting assigned (Person
changes = 1)Person
changes due to newAddress
object getting assigned (Person
changes = 2)Person
changes due to nullAddress
object getting assigned (Person
changes = 3)Person
changes due to newAddress
object getting assigned (Person
changes = 4)Person
changes due to nullAddress
object getting assigned (Person
changes = 5)Person
changes due to newAddress
object getting assigned (Person
changes = 6)Person.Address
changes due to newGeoLocation
object getting assigned (Person.Address
changes =1)- 2nd
Person.Address GeoLocation
change is ignored as it is the same data as previousGeoLocation
object (Person.Address
changes =1) Person.Address.GeoLocation
changes nullGeoLocation
object getting assigned (Person
changes = 2)Person.Address
changes due to newGeoLocation
object getting assigned (Latitude 0.66, Longitude 0.71) (Person.Address
changes =3)Person.Address.GeoLocation
changes due toGeoLocation
data object value changing to 0.51 (Person.Address.GeoLocation
changes =1)Person.Address.GeoLocation
changes due toGeoLocation
data object value changing to 0.52 (Person.Address.GeoLocation
changes =2)Person.Address.GeoLocation
changes due toGeoLocation
data object value changing to 0.53 (Person.Address.GeoLocation
changes =3)Person.Address.GeoLocation
changes due toGeoLocation
data object value changing to 0.54 (Person.Address.GeoLocation
changes =4)- 5th Person.Address.
GeoLocation
change is ignored as it is the same data as previousGeoLocation
object (Person.Address.GeoLocation
changes =4)
So let's see a screen shot of the demo code to see if our expectations hold true, we are expecting the following changes to have occurred:
Person
changes =6Address
changes = 3GeoLocation
changes = 4
So here are some screen shots of the demo app in debug which proves this to be true (unit tests would have been better, sorry)
Ok we are at the end after all changes have been made, and we can see only 6
Person
change
And only 3 Address
change
And 4 GeoLocation
changes
This is pretty cool, we only got the change notifications we were expecting,
the ChainPropertyObserver
was totally fine with new objects being swapped
for existing objects in the chain, and it was completely happy and knew how to
deal with that, and still provided us with the correct INPC callbacks that we
registered interest in.
I say this is pretty useful actually.
WPF Demo
I have also crafted a more elaborate demo which binding the object graph up
like {Binding Person.Address.GeoLocation}
which is something I do
not like so much, but it does demonstrate the demo quite well. The WPF demo
looks like this, and allows you to create changes manually or using several
Button
s provided, to see what properties are expected to change
when a Button
is clicked you should consult the code.
This demo included ListBox
es for the following INPC changes, so
you can see what happens when you change something manually or by using the
Button
s provided.
Anyway the WPF demo looks like this (click image for bigger version)
As I say you will need to consult the code for a better understanding of what
changes when you click a Button
. It has been setup quite
specifically.
History
- 09/03/11 : Initial Issue
- 10/03/11 : Changed implementation to cope with null object references as part of chain. The article talks about this.
That's It For Now
Anyway that is all I have to say right now, like I say I can not make my mind up if this is truly useful, or useless, but none the less I think it is an interesting peice of code that shows you how you can build up chains of objects using Lambda expression trees, and it also demonstrates some interesting weak event/delegate stuff, so for those reasons alone I am glad I published this article....If you like it and can see a use for it, please tell me about it, and if you feel uber inclined votes/cash/beer/old acid house records and wenches are always welcome....will also accept 1 lama.