-
-
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 | |
} | |
} |
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?
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
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
@bergerb-com Thank you for sharing, sorry this is very old and I have no idea any more ;-)
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.
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.