diff --git a/Stylet/PropertyChangedExtensions.cs b/Stylet/PropertyChangedExtensions.cs index c8c3604..3b110ab 100644 --- a/Stylet/PropertyChangedExtensions.cs +++ b/Stylet/PropertyChangedExtensions.cs @@ -11,6 +11,29 @@ using System.Reflection; namespace Stylet { + /// + /// Extension of PropertyChangedEventArgs, which includes the new value of the property + /// + /// + public class PropertyChangedExtendedEventArgs : PropertyChangedEventArgs + { + /// + /// New value of the property + /// + public virtual TProperty NewValue { get; private set; } + + /// + /// Instantiate a new PropertyChangedExtendedEventArgs + /// + /// Name of the property which changed + /// New value of the property which changed + public PropertyChangedExtendedEventArgs(string propertyName, TProperty newValue) + : base(propertyName) + { + this.NewValue = newValue; + } + } + /// /// A binding to a PropertyChanged event, which can be used to unbind the binding /// @@ -51,11 +74,11 @@ namespace Stylet internal class WeakPropertyChangedHandler : IEventBinding where TSource : class, INotifyPropertyChanged { private readonly WeakReference source; - private Action handler; + private EventHandler> handler; private string propertyName; private Func valueSelector; - public WeakPropertyChangedHandler(TSource source, Expression> selector, Action handler) + public WeakPropertyChangedHandler(TSource source, Expression> selector, EventHandler> handler) { // We keep a strong reference to the handler, and have the PropertyChangedEventManager keep a weak // reference to us. This means that anyone retaining us will also retain the handler. @@ -74,7 +97,7 @@ namespace Stylet var got = this.source.TryGetTarget(out source); // We should never hit this case. The PropertyChangedeventManager shouldn't call us if the source became null Debug.Assert(got); - this.handler(this.valueSelector(source)); + this.handler(source, new PropertyChangedExtendedEventArgs(this.propertyName, this.valueSelector(source))); } public void Unbind() @@ -110,21 +133,21 @@ namespace Stylet /// MemberExpression selecting the property to observe for changes (e.g x => x.PropertyName) /// Handler called whenever that property changed /// Something which can be used to undo the binding. You can discard it if you want - public static IEventBinding Bind(this TBindTo target, Expression> targetSelector, Action handler) where TBindTo : class, INotifyPropertyChanged + public static IEventBinding Bind(this TSource target, Expression> targetSelector, EventHandler> handler) where TSource : class, INotifyPropertyChanged { var propertyName = targetSelector.NameForProperty(); var propertyAccess = targetSelector.Compile(); // Make sure we don't capture target strongly, otherwise we'll retain it when we shouldn't // If it does get released, we're released from the delegate list - var weakTarget = new WeakReference(target); + var weakTarget = new WeakReference(target); PropertyChangedEventHandler ourHandler = (o, e) => { if (e.PropertyName == propertyName || e.PropertyName == String.Empty) { - TBindTo strongTarget; + TSource strongTarget; if (weakTarget.TryGetTarget(out strongTarget)) - handler(propertyAccess(strongTarget)); + handler(strongTarget, new PropertyChangedExtendedEventArgs(propertyName, propertyAccess(strongTarget))); } }; @@ -135,13 +158,21 @@ namespace Stylet return listener; } - public static IEventBinding BindWeak(this TBindTo target, Expression> targetSelector, Action handler) where TBindTo : class, INotifyPropertyChanged + /// + /// Weakly bind to PropertyChanged events for a particular property on a particular object + /// + /// someObject.Bind(x => x.PropertyNameToBindTo, newValue => /* do something with the new value */) + /// Object raising the PropertyChanged event you're interested in + /// MemberExpression selecting the property to observe for changes (e.g x => x.PropertyName) + /// Handler called whenever that property changed + /// Something which can be used to undo the binding. You can discard it if you want + public static IEventBinding BindWeak(this TSource target, Expression> targetSelector, EventHandler> handler) where TSource : class, INotifyPropertyChanged { var attribute = handler.Target.GetType().GetCustomAttribute(); if (attribute != null) throw new InvalidOperationException("Handler passed to BindWeak refers to a compiler-generated class. You may not capture local variables in the handler"); - var binding = new WeakPropertyChangedHandler(target, targetSelector, handler); + var binding = new WeakPropertyChangedHandler(target, targetSelector, handler); return new WeakPropertyChangedBinding(binding); } } diff --git a/StyletUnitTests/PropertyChangedExtensionsTests.cs b/StyletUnitTests/PropertyChangedExtensionsTests.cs index 2390259..d8af7c1 100644 --- a/StyletUnitTests/PropertyChangedExtensionsTests.cs +++ b/StyletUnitTests/PropertyChangedExtensionsTests.cs @@ -40,16 +40,17 @@ namespace StyletUnitTests public IEventBinding BindStrong(NotifyingClass notifying) { // Must make sure the compiler doesn't generate an inner class for this, otherwise we're not testing the right thing - return notifying.Bind(x => x.Foo, x => this.LastFoo = x); + return notifying.Bind(x => x.Foo, (o, e) => this.LastFoo = e.NewValue); } public IEventBinding BindWeak(NotifyingClass notifying) { - return notifying.BindWeak(x => x.Foo, x => this.LastFoo = x); + return notifying.BindWeak(x => x.Foo, (o, e) => this.LastFoo = e.NewValue); } } private string newVal; + private object sender; [TestFixtureSetUp] public void SetUpFixture() @@ -61,6 +62,7 @@ namespace StyletUnitTests public void SetUp() { this.newVal = null; + this.sender = null; } [Test] @@ -68,7 +70,7 @@ namespace StyletUnitTests { string newVal = null; var c1 = new NotifyingClass(); - c1.Bind(x => x.Foo, x => newVal = x); + c1.Bind(x => x.Foo, (o, e) => newVal = e.NewValue); c1.Foo = "bar"; Assert.AreEqual("bar", newVal); @@ -79,7 +81,7 @@ namespace StyletUnitTests { string newVal = null; var c1 = new NotifyingClass(); - c1.Bind(x => x.Bar, x => newVal = x); + c1.Bind(x => x.Bar, (o, e) => newVal = e.NewValue); c1.Foo = "bar"; Assert.AreEqual(null, newVal); @@ -91,7 +93,7 @@ namespace StyletUnitTests string newVal = null; var c1 = new NotifyingClass(); c1.Bar = "bar"; - c1.Bind(x => x.Bar, x => newVal = x); + c1.Bind(x => x.Bar, (o, e) => newVal = e.NewValue); c1.NotifyAll(); Assert.AreEqual("bar", newVal); @@ -112,12 +114,22 @@ namespace StyletUnitTests Assert.IsFalse(weakNotifying.TryGetTarget(out notifying)); } + [Test] + public void StrongBindingPassesTarget() + { + var c1 = new NotifyingClass(); + object sender = null; + c1.Bind(x => x.Foo, (o, e) => sender = o); + c1.Foo = "foo"; + Assert.AreEqual(c1, sender); + } + [Test] public void StrongBindingUnbinds() { string newVal = null; var c1 = new NotifyingClass(); - var binding = c1.Bind(x => x.Bar, x => newVal = x); + var binding = c1.Bind(x => x.Bar, (o, e) => newVal = e.NewValue); binding.Unbind(); c1.Bar = "bar"; @@ -128,7 +140,7 @@ namespace StyletUnitTests public void WeakBindingBinds() { var c1 = new NotifyingClass(); - c1.BindWeak(x => x.Foo, x => this.newVal = x); + c1.BindWeak(x => x.Foo, (o, e) => this.newVal = e.NewValue); c1.Foo = "bar"; Assert.AreEqual("bar", this.newVal); @@ -138,7 +150,7 @@ namespace StyletUnitTests public void WeakBindingIgnoresOtherProperties() { var c1 = new NotifyingClass(); - c1.BindWeak(x => x.Bar, x => this.newVal = x); + c1.BindWeak(x => x.Bar, (o, e) => this.newVal = e.NewValue); c1.Foo = "bar"; Assert.IsNull(this.newVal); @@ -149,7 +161,7 @@ namespace StyletUnitTests { var c1 = new NotifyingClass(); c1.Bar = "bar"; - c1.BindWeak(x => x.Bar, x => this.newVal = x); + c1.BindWeak(x => x.Bar, (o, e) => this.newVal = e.NewValue); c1.NotifyAll(); Assert.AreEqual("bar", this.newVal); @@ -166,6 +178,8 @@ namespace StyletUnitTests var notifying = new NotifyingClass(); binding.BindWeak(notifying); + + binding = null; GC.Collect(); Assert.IsFalse(weakBinding.TryGetTarget(out binding)); @@ -190,19 +204,28 @@ namespace StyletUnitTests public void WeakBindingUnbinds() { var c1 = new NotifyingClass(); - var binding = c1.BindWeak(x => x.Bar, x => this.newVal = x); + var binding = c1.BindWeak(x => x.Bar, (o, e) => this.newVal = e.NewValue); binding.Unbind(); c1.Bar = "bar"; Assert.IsNull(this.newVal); } + [Test] + public void BindWeakPassesSender() + { + var c1 = new NotifyingClass(); + c1.BindWeak(x => x.Foo, (o, e) => this.sender = o); + c1.Foo = "foo"; + Assert.AreEqual(c1, this.sender); + } + [Test] public void BindWeakThrowsIfTargetIsCompilerGenerated() { var c1 = new NotifyingClass(); string newVal = null; - Assert.Throws(() => c1.BindWeak(x => x.Foo, x => newVal = x)); + Assert.Throws(() => c1.BindWeak(x => x.Foo, (o, e) => newVal = e.NewValue)); } } }