diff --git a/Stylet/PropertyChangedExtensions.cs b/Stylet/PropertyChangedExtensions.cs index d73a110..6aa3933 100644 --- a/Stylet/PropertyChangedExtensions.cs +++ b/Stylet/PropertyChangedExtensions.cs @@ -9,44 +9,148 @@ using System.Threading.Tasks; namespace Stylet { + public interface IPropertyChangedBinding + { + void Unbind(); + } + public static class PropertyChangedExtensions { - private static ConditionalWeakTable eventMapping = new ConditionalWeakTable(); + private static ConditionalWeakTable> eventMapping = new ConditionalWeakTable>(); private static object eventMappingLock = new object(); - private class Listener + public class WeakPropertyChangedBinding : IPropertyChangedBinding { - public readonly string PropertyName; - public readonly EventHandler Handler; + private readonly string propertyName; + private readonly EventHandler handler; + private readonly WeakReference binder; + private readonly WeakReference inpc; - public Listener(string propertyName, EventHandler handler) + public WeakPropertyChangedBinding(object binder, INotifyPropertyChanged inpc, string propertyName, EventHandler handler) { - this.PropertyName = propertyName; - this.Handler = handler; + this.binder = new WeakReference(binder); + this.inpc = new WeakReference(inpc); + this.propertyName = propertyName; + this.handler = handler; + } + + public void Unbind() + { + INotifyPropertyChanged inpc; + + // If the target's still around, unregister ourselves + if (this.inpc.TryGetTarget(out inpc)) + PropertyChangedEventManager.RemoveHandler(inpc, this.handler, this.propertyName); + + // If the handler's still around (there's a possibility it isn't), remove us from the ConditionalWeakTable + var handler = this.handler.Target; + if (handler != null) + { + lock (eventMappingLock) + { + List listeners; + if (eventMapping.TryGetValue(handler, out listeners)) + listeners.Remove(this); + } + } } } - public static void Bind(this TClass item, Expression> selector, Action handler) where TClass : INotifyPropertyChanged + public class StrongPropertyChangedBinding : IPropertyChangedBinding { - var propertyName = selector.NameForProperty(); - var compiledSelector = selector.Compile(); - List listeners; - EventHandler ourHandler; + private WeakReference inpc; + private PropertyChangedEventHandler handler; - lock (eventMappingLock) + public StrongPropertyChangedBinding(INotifyPropertyChanged inpc, PropertyChangedEventHandler handler) { - if (!eventMapping.TryGetValue(item, out listeners)) - { - listeners = new List(); - eventMapping.Add(item, listeners); - } - - ourHandler = (s, e) => handler(compiledSelector(item)); - - listeners.Add(new Listener(propertyName, ourHandler)); + this.inpc = new WeakReference(inpc); + this.handler = handler; } - PropertyChangedEventManager.AddHandler(item, ourHandler, propertyName); + public void Unbind() + { + INotifyPropertyChanged inpc; + if (this.inpc.TryGetTarget(out inpc)) + { + inpc.PropertyChanged -= handler; + } + } + } + + public static IPropertyChangedBinding BindWeak(this object binder, Expression> targetSelector, Action handler) + { + return BindInternal(binder, targetSelector, handler, true); + } + + public static IPropertyChangedBinding Bind(this object binder, Expression> targetSelector, Action handler) + { + return BindInternal(binder, targetSelector, handler, false); + } + + private static IPropertyChangedBinding BindInternal(object binder, Expression> targetSelector, Action handler, bool weak) + { + var memberSelector = targetSelector.Body as MemberExpression; + if (memberSelector == null) + throw new ArgumentException("Must be in the form () => someInstance.SomeProperty", "targetSelector"); + + var propertyName = memberSelector.Member.Name; + var targetExpression = memberSelector.Expression as MemberExpression; + if (targetExpression == null) + throw new ArgumentException("Must be in the form () => someInstance.SomeProperty", "targetSelector"); + + var target = Expression.Lambda>(targetExpression).Compile()(); + var inpc = target as INotifyPropertyChanged; + if (inpc == null) + throw new ArgumentException("The someInstance in () => someInstance.SomeProperty must be an INotifyPropertyChanged", "targetSelector"); + + var propertyAccess = Expression.Lambda>(memberSelector).Compile(); + + IPropertyChangedBinding listener; + + if (weak) + { + EventHandler ourHandler = (o, e) => + { + handler(propertyAccess()); + }; + + WeakPropertyChangedBinding weakListener = new WeakPropertyChangedBinding(binder, inpc, propertyName, ourHandler); + listener = weakListener; + + // Right, we have target, propertyName, binder. + // Now we have to keep the handler we're about to build (which has a reference to the handler we were passed) alive as long as + // binder's alive (the handler that was passed to us might refer to a method on a compiler-generated class, which we've got + // the only reference to, so we've got to keep that alive). + lock (eventMappingLock) + { + List listeners; + if (!eventMapping.TryGetValue(binder, out listeners)) + { + listeners = new List(); + eventMapping.Add(binder, listeners); + } + + listeners.Add(weakListener); + } + + PropertyChangedEventManager.AddHandler(inpc, ourHandler, propertyName); + } + else + { + PropertyChangedEventHandler ourHandler = (o, e) => + { + if (e.PropertyName == propertyName || e.PropertyName == String.Empty) + { + handler(propertyAccess()); + } + }; + + inpc.PropertyChanged += ourHandler; + + listener = new StrongPropertyChangedBinding(inpc, ourHandler); + } + + return listener; } } }