Introduction
This utility allows you to monitor changes in nested collections and properties with a single line of code. This can be useful, for example, to provide change notification for a property that computes its value in its get
-method. Especially when many properties affect the calculation, it saves a lot of boilerplate code.
Features
- Supports monitoring of classes that implement
INotifyPropertyChanged
, INotifyCollectionChanged
, or DependencyObject
- Supports unlimited nesting of collections and properties
- Always uses only one event handler per object and automatically detaches handlers when no longer used
Using the Code
A simple situation might look like this:
private ChangeListener _Listener;
public void AttachListeners() {
_Listener = new ChangeListener();
listener.AddAction(this, "Dog.Name", () => UpdateDogName());
}
We can nest as far as we want:
listener.AddAction(this, "Dog.Owner.Country.Anthem.Name", () => UpdateAnthemName());
We can also monitor collections with "[?]
" (without the quotes):
listener.AddAction(this, "Dog.Puppies[?].Name", () => UpdatePuppyNames());
Nesting collections is also possible:
listener.AddAction(this, "Dog.Puppies[?].Friends[?].Name",
() => UpdatePuppyFriendNames());
Optionally, you can specify some additional options:
listener.AddAction(this, "Dog.Puppies[?].Friends[?].Name",
() => UpdatePuppyFriendNames(),
ChangeListenerOptions.ExecuteOnCollectionReorder);
listener.AddAction(this, "Dog.Puppies[?].Friends[?].Name",
() => UpdatePuppyFriendNames(), ChangeListenerOptions.IgnoreParentChange);
listener.AddAction(this, "Dog.Puppies[?].Friends[?].Name",
() => UpdatePuppyFriendNames(),
ChangeListenerOptions.AllowNonObservableProperties);
To detach the event handlers:
listener.RemoveActions(this, "Dog.Puppies[?].Friends[?].Name");
listener.RemoveAllActions();
Points of Interest
This turned out to be harder to implement than I initially thought, as always. Every time I had just revised the code, I discovered another special case which invalidated my design. Several rewrites later, the system works something like this:
Every time AddAction
is called, a Trigger
is added. It contains a list of Link
objects which represent the parts of the path (the chain). Part of the constructor code:
public Trigger(ChangeListener changeListener, object root, string path,
Action action, ChangeListenerOptions options, int number)
{
var pathParts = InterpretPath(path);
Type currentSourceType = root.GetType();
foreach(string pathPart in pathParts) {
Link link = null;
Type valueType = null; if(pathPart == _CollectionIndexer) {
link = CreateCollectionLink(currentSourceType, out valueType);
} else {
link = CreatePropertyLink(currentSourceType, pathPart, out valueType);
}
Links.Add(link);
currentSourceType = valueType;
}
Links[0].AddSourceUser(root);
}
Each Link
(which is a base class inherited by PropertyLink
and CollectionLink
) manages all objects for which it must monitor the property or collection. For example, the Link
that monitors the Puppy.Name
property monitors all the puppies of the monitored dog. To add an object that has to be monitored, AddSourceUser
is called. The reuse of listeners can also be seen here:
private abstract class Link
{
public abstract void AddSourceUser(object source);
protected virtual int AddSourceUserInternal(object source)
{
int index = _Listeners.FindIndex(
listener => listener.Source == source);
if(index != -1) {
_ListenerUserCounts[index]++;
} else {
Listener listener = Trigger.ChangeListener._Listeners.Find(
listenerParam => listenerParam.Source == source &&
listenerParam.GetType() == _ListenerType);
if(listener == null) { listener = CreateListener(source);
Trigger.ChangeListener._Listeners.Add(listener);
}
listener.AddLink(this);
_Listeners.Add(listener);
_ListenerUserCounts.Add(1);
index = _Listeners.Count-1;
}
return index;
}
}
As said, two classes implement the Link
class: PropertyLink
and CollectionLink
. Part of the PropertyLink
class is displayed below. Every time a listener detects a change that is relevant for a link, it will notify it; in case of a PropertyLink
, it will call HandlePropertyChanged
. Also displayed are the overrides of AddSourceUser
and AddSourceUserInternal
:
private class PropertyLink : Link
{
public void HandlePropertyChanged(Listener listener)
{
int listenerIndex = _Listeners.IndexOf(listener);
object oldValue = _CurrentValues[listenerIndex];
object newValue = Property.GetValue(listener.Source, null);
_CurrentValues[listenerIndex] = newValue;
int linkIndex = Trigger.Links.IndexOf(this);
if(linkIndex < Trigger.Links.Count-1) {
var nextLink = Trigger.Links[linkIndex+1];
if(oldValue != null) {
nextLink.RemoveSourceUser(oldValue);
}
if(newValue != null) {
nextLink.AddSourceUser(newValue);
}
}
if(!Trigger.IgnoreParentChange || linkIndex == Trigger.Links.Count-1) {
Trigger.Action();
}
}
public override void AddSourceUser(object source)
{
int index = AddSourceUserInternal(source);
object value = _CurrentValues[index];
int linkIndex = Trigger.Links.IndexOf(this);
if(linkIndex < Trigger.Links.Count-1) {
if(value != null) {
Trigger.Links[linkIndex+1].AddSourceUser(value);
}
}
}
protected override int AddSourceUserInternal(object source)
{
int index = base.AddSourceUserInternal(source);
if(_Listeners.Count > _CurrentValues.Count) { object value = Property.GetValue(_Listeners[index].Source, null);
_CurrentValues.Add(value);
}
return index;
}
}
There are also two types of listeners, both derived from the abstract Listener
class: PropertyListener
and CollectionListener
. Each link type only uses the corresponding listener type. The listener contains a list of all the attached links. Displayed is the PropertyChanged
handler in a PropertyListener
:
private class PropertyListener : Listener
{
private void PropertyChanged(object sender, PropertyChangedEventArgs e)
{
var linksToSignal = new List<link />();
foreach(var link in _Links) {
if(((PropertyLink)link).Property.Name == e.PropertyName) {
Link linkWithSameTrigger = linksToSignal.FirstOrDefault(
linkParam => linkParam.Trigger == link.Trigger);
if(linkWithSameTrigger != null) {
if(link.Trigger.Links.IndexOf(link) <
linkWithSameTrigger.Trigger.Links.IndexOf(linkWithSameTrigger)) {
linksToSignal.Remove(linkWithSameTrigger);
} else {
continue; }
}
linksToSignal.Add(link);
}
}
foreach(Link link in linksToSignal.OrderBy(link =>
link.Trigger.ChangeListener._Triggers.IndexOf(link.Trigger))) {
((PropertyLink)link).HandlePropertyChanged(this);
}
}
}
I added dependency property support later on. To my surprise, this was not so straightforward. Turned out that the only property change event that you could hook up to at runtime did not supply information about what property had changed. I solved this by adding a subclass that holds the property changed method and relays it:
private class PropertyListener : Listener
{
public override void AddLink(Link link)
{
base.AddLink(link);
if(Source is DependencyObject) {
var dependencyProperty = DependencyPropertyDescriptor.FromName(
((PropertyLink)link).Property.Name,
Source.GetType(), Source.GetType());
_DependencyProperties.Add((dependencyProperty != null) ?
new DependencyPropertyListener(this, dependencyProperty) : null);
}
}
private class DependencyPropertyListener
{
public DependencyPropertyListener(PropertyListener parentListener,
DependencyPropertyDescriptor dependencyProperty)
{
_DependencyProperty.AddValueChanged(_ParentListener.Source, PropertyChanged);
}
private void PropertyChanged(object sender, EventArgs e)
{
_ParentListener.PropertyChanged(sender,
new PropertyChangedEventArgs(_DependencyProperty.Name));
}
}
}
History
- July 12 2010: Initial release.
- July 14 2010: Expanded Points of Interest.