Skip to content

Instantly share code, notes, and snippets.

@thojaw
Created November 18, 2010 19:16
  • Star 11 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save thojaw/705450 to your computer and use it in GitHub Desktop.
using System;
using System.Collections.Specialized;
using System.ComponentModel;
namespace ThomasJaworski.ComponentModel
{
public abstract class ChangeListener : INotifyPropertyChanged, IDisposable
{
#region *** Members ***
protected string _propertyName;
#endregion
#region *** Abstract Members ***
protected abstract void Unsubscribe();
#endregion
#region *** INotifyPropertyChanged Members and Invoker ***
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void RaisePropertyChanged(string propertyName)
{
var temp = PropertyChanged;
if (temp != null)
temp(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
#region *** Disposable Pattern ***
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
Unsubscribe();
}
}
~ChangeListener()
{
Dispose(false);
}
#endregion
#region *** Factory ***
public static ChangeListener Create(INotifyPropertyChanged value)
{
return Create(value, null);
}
public static ChangeListener Create(INotifyPropertyChanged value, string propertyName)
{
if (value is INotifyCollectionChanged)
{
return new CollectionChangeListener(value as INotifyCollectionChanged, propertyName);
}
else if (value is INotifyPropertyChanged)
{
return new ChildChangeListener(value as INotifyPropertyChanged, propertyName);
}
else
return null;
}
#endregion
}
}
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
namespace ThomasJaworski.ComponentModel
{
public class ChildChangeListener : ChangeListener
{
#region *** Members ***
protected static readonly Type _inotifyType = typeof(INotifyPropertyChanged);
private readonly INotifyPropertyChanged _value;
private readonly Type _type;
private readonly Dictionary<string, ChangeListener> _childListeners = new Dictionary<string, ChangeListener>();
#endregion
#region *** Constructors ***
public ChildChangeListener(INotifyPropertyChanged instance)
{
if (instance == null)
throw new ArgumentNullException("instance");
_value = instance;
_type = _value.GetType();
Subscribe();
}
public ChildChangeListener(INotifyPropertyChanged instance, string propertyName)
: this(instance)
{
_propertyName = propertyName;
}
#endregion
#region *** Private Methods ***
private void Subscribe()
{
_value.PropertyChanged += new PropertyChangedEventHandler(value_PropertyChanged);
var query =
from property
in _type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
where _inotifyType.IsAssignableFrom(property.PropertyType)
select property;
foreach (var property in query)
{
// Declare property as known "Child", then register it
_childListeners.Add(property.Name, null);
ResetChildListener(property.Name);
}
}
/// <summary>
/// Resets known (must exist in children collection) child event handlers
/// </summary>
/// <param name="propertyName">Name of known child property</param>
private void ResetChildListener(string propertyName)
{
if (_childListeners.ContainsKey(propertyName))
{
// Unsubscribe if existing
if (_childListeners[propertyName] != null)
{
_childListeners[propertyName].PropertyChanged -= new PropertyChangedEventHandler(child_PropertyChanged);
// Should unsubscribe all events
_childListeners[propertyName].Dispose();
_childListeners[propertyName] = null;
}
var property = _type.GetProperty(propertyName);
if (property == null)
throw new InvalidOperationException(string.Format("Was unable to get '{0}' property information from Type '{1}'", propertyName, _type.Name));
object newValue = property.GetValue(_value, null);
// Only recreate if there is a new value
if (newValue != null)
{
if (newValue is INotifyCollectionChanged)
{
_childListeners[propertyName] =
new CollectionChangeListener(newValue as INotifyCollectionChanged, propertyName);
}
else if (newValue is INotifyPropertyChanged)
{
_childListeners[propertyName] =
new ChildChangeListener(newValue as INotifyPropertyChanged, propertyName);
}
if (_childListeners[propertyName] != null)
_childListeners[propertyName].PropertyChanged += new PropertyChangedEventHandler(child_PropertyChanged);
}
}
}
#endregion
#region *** Event Handler ***
void child_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
RaisePropertyChanged(e.PropertyName);
}
void value_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
// First, reset child on change, if required...
ResetChildListener(e.PropertyName);
// ...then, notify about it
RaisePropertyChanged(e.PropertyName);
}
protected override void RaisePropertyChanged(string propertyName)
{
// Special Formatting
base.RaisePropertyChanged(string.Format("{0}{1}{2}",
_propertyName, _propertyName != null ? "." : null, propertyName));
}
#endregion
#region *** Overrides ***
/// <summary>
/// Release all child handlers and self handler
/// </summary>
protected override void Unsubscribe()
{
_value.PropertyChanged -= new PropertyChangedEventHandler(value_PropertyChanged);
foreach (var binderKey in _childListeners.Keys)
{
if (_childListeners[binderKey] != null)
_childListeners[binderKey].Dispose();
}
_childListeners.Clear();
System.Diagnostics.Debug.WriteLine("ChildChangeListener '{0}' unsubscribed", _propertyName);
}
#endregion
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
namespace ThomasJaworski.ComponentModel
{
public class CollectionChangeListener : ChangeListener
{
#region *** Members ***
private readonly INotifyCollectionChanged _value;
private readonly Dictionary<INotifyPropertyChanged, ChangeListener> _collectionListeners = new Dictionary<INotifyPropertyChanged, ChangeListener>();
#endregion
#region *** Constructors ***
public CollectionChangeListener(INotifyCollectionChanged collection, string propertyName)
{
_value = collection;
_propertyName = propertyName;
Subscribe();
}
#endregion
#region *** Private Methods ***
private void Subscribe()
{
_value.CollectionChanged += new NotifyCollectionChangedEventHandler(value_CollectionChanged);
foreach (INotifyPropertyChanged item in (IEnumerable)_value)
{
ResetChildListener(item);
}
}
private void ResetChildListener(INotifyPropertyChanged item)
{
if (item == null)
throw new ArgumentNullException("item");
RemoveItem(item);
ChangeListener listener = null;
// Add new
if (item is INotifyCollectionChanged)
listener = new CollectionChangeListener(item as INotifyCollectionChanged, _propertyName);
else
listener = new ChildChangeListener(item as INotifyPropertyChanged);
listener.PropertyChanged += new PropertyChangedEventHandler(listener_PropertyChanged);
_collectionListeners.Add(item, listener);
}
private void RemoveItem(INotifyPropertyChanged item)
{
// Remove old
if (_collectionListeners.ContainsKey(item))
{
_collectionListeners[item].PropertyChanged -= new PropertyChangedEventHandler(listener_PropertyChanged);
_collectionListeners[item].Dispose();
_collectionListeners.Remove(item);
}
}
private void ClearCollection()
{
foreach (var key in _collectionListeners.Keys)
{
_collectionListeners[key].Dispose();
}
_collectionListeners.Clear();
}
#endregion
#region *** Event handlers ***
void value_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Reset)
{
ClearCollection();
}
else
{
// Don't care about e.Action, if there are old items, Remove them...
if (e.OldItems != null)
{
foreach (INotifyPropertyChanged item in (IEnumerable)e.OldItems)
RemoveItem(item);
}
// ...add new items as well
if (e.NewItems != null)
{
foreach (INotifyPropertyChanged item in (IEnumerable)e.NewItems)
ResetChildListener(item);
}
}
}
void listener_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
// ...then, notify about it
RaisePropertyChanged(string.Format("{0}{1}{2}",
_propertyName, _propertyName != null ? "[]." : null, e.PropertyName));
}
#endregion
#region *** Overrides ***
/// <summary>
/// Releases all collection item handlers and self handler
/// </summary>
protected override void Unsubscribe()
{
ClearCollection();
_value.CollectionChanged -= new NotifyCollectionChangedEventHandler(value_CollectionChanged);
System.Diagnostics.Debug.WriteLine("CollectionChangeListener unsubscribed");
}
#endregion
}
}
@dantuck
Copy link

dantuck commented Jul 12, 2011

How do I implement this? I saw the stackoverflow page. I tried to implement from there but the child properties are not being watched when their properties are changed. Do you have a demo bit of code?

@thojaw
Copy link
Author

thojaw commented Jul 12, 2011

@dantuck, you'd need something like this (partially taken from my original example @ StackOverflow).
Hope it works, I wrote that from scratch without compiler so you might need to correct little mistakes I made? ;-)

public class Person : INotifyPropertyChanged {

    private string _firstName;
    private int _age;
    private Person _bestFriend;

    public string FirstName {
        get { return _firstName; }
        set {
        // Short implementation for simplicity reasons
        _firstName = value;
        RaisePropertyChanged("FirstName");
        }
    }

    public int Age {
        get { return _age; }
        set {
        // Short implementation for simplicity reasons
        _age = value;
        RaisePropertyChanged("Age");
        }
    }

    public Person BestFriend {
        get { return _bestFriend; }
        set {
        _bestFriend = value;
        RaisePropertyChanged("BestFriend");
        }
    }

    #region *** INotifyPropertyChanged Members and Invoker ***
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void RaisePropertyChanged(string propertyName)
    {
    var temp = PropertyChanged;
    if (temp != null)
    temp(this, new PropertyChangedEventArgs(propertyName));
    }
    #endregion
}

public static class Program {
    public static void Main() {

        Person p1 = new Person();
        var personChangeListener = ChangeListener.Create(p1);

        personChangeListener.PropertyChanged += new PropertyChangedEventHandler(value_PropertyChanged);

        // Start to Change P1... you should be notified about every change

        p1.Age = 30;
        p1.FirstName = "John";

        p1.BestFriend = new Person();

        p1.BestFriend.Age = 31;
        p1.BestFriend.FirstName = "Mike";

        // etc.
    }

    static void value_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        Console.WriteLine("Changed Property: "+ e.PropertyName);
    }
}

@dantuck
Copy link

dantuck commented Jul 12, 2011

Thanks. I got it working but have also started expanding to capture more info. On top of being notified I need the old and new value with the property for change tracking.

@MarkPThomas
Copy link

Great example!

I needed to do this in VB.Net, and it requires a bit more than a simple translation like can be done with: http://converter.telerik.com/

In working out the kinks and implementation, I also made a brief WinForm example that nicely illustrates using this by changing a property of a child class that is referenced in a parent class, and catching the property changed event once it has bubbled up to the parent class from within the child class.

I figured I'd put it on my Github depository, but I was thinking it might be a better fit here. Or at least reference this source.

What would you think best?

@wesleygrimes
Copy link

Hey Mark,

Found this with a Google search. I am working on a WinForms project where I have done my best to implement an "MVVM"ish pattern. With the INotifyPropertyChanged I have a View Model with Properties that are actually nested objects. I need to cue in the Parent View Model when a property on the nested child object has been updated. This is so that I can trigger the data bound button on the UI to re-check the CanExecute on the RelayCommand.

Do you think something like the above would work for bubbling up changes?

Let me know if you would like to see the code.

Thanks,
Wes

@bergerb-com
Copy link

Hi,

I am using this code to Notify Changes of a Property which I am binding to.
Specifically I want to bind to DBContext.ChangeTracker.HasChanges()

Two changes I needed to make, to use it for my problem:
ChildChangeListener.cs

var query =
    from property
    in _type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
    where _inotifyType.IsAssignableFrom(property.PropertyType) &&
        typeof(System.Collections.IEnumerable).IsAssignableFrom(property.PropertyType)
    select property;

To make sure no StackOverflow Exception is thrown (I am using EntityFramework and there are navigation properties..)
Maybe there is a better solution, but this works for me, since I don't need to get notified if any referenced objects get updated, only Collections are important for me.

And in CollectionChangeListener.cs:
I had to add RaisePropertyChanged to the value_CollectionChanged event. Any reason why this isn't called when a collection changes?

Benjamin

@thojaw
Copy link
Author

thojaw commented Oct 15, 2019

@bergerb-com Thank you for sharing, sorry this is very old and I have no idea any more ;-)

@canton7
Copy link

canton7 commented Apr 4, 2022

Note that there's no point in ChangeListener defining a finalizer. The finalizer is responsible for unsubscribing the ChangeListener from the value.PropertyChanged event, but if it's subscribed then value.PropertyChanged will hold a reference to the ChangeListener, which means that it won't be GC'd, which means that the finalizer will never be called. So either the ChangeListener is subscribed to value.PropertyChanged, in which case the finalizer will never be called, or it isn't, in which case the finalizer will be called but it has nothing to unsubscribe from.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment