Change the handler type for Bind and BindWeak, to include the sender and property name

This commit is contained in:
Antony Male 2014-06-22 17:22:24 +01:00
parent c18563267f
commit 58d5a6e55e
2 changed files with 74 additions and 20 deletions

View File

@ -11,6 +11,29 @@ using System.Reflection;
namespace Stylet
{
/// <summary>
/// Extension of PropertyChangedEventArgs, which includes the new value of the property
/// </summary>
/// <typeparam name="TProperty"></typeparam>
public class PropertyChangedExtendedEventArgs<TProperty> : PropertyChangedEventArgs
{
/// <summary>
/// New value of the property
/// </summary>
public virtual TProperty NewValue { get; private set; }
/// <summary>
/// Instantiate a new PropertyChangedExtendedEventArgs
/// </summary>
/// <param name="propertyName">Name of the property which changed</param>
/// <param name="newValue">New value of the property which changed</param>
public PropertyChangedExtendedEventArgs(string propertyName, TProperty newValue)
: base(propertyName)
{
this.NewValue = newValue;
}
}
/// <summary>
/// A binding to a PropertyChanged event, which can be used to unbind the binding
/// </summary>
@ -51,11 +74,11 @@ namespace Stylet
internal class WeakPropertyChangedHandler<TSource, TProperty> : IEventBinding where TSource : class, INotifyPropertyChanged
{
private readonly WeakReference<TSource> source;
private Action<TProperty> handler;
private EventHandler<PropertyChangedExtendedEventArgs<TProperty>> handler;
private string propertyName;
private Func<TSource, TProperty> valueSelector;
public WeakPropertyChangedHandler(TSource source, Expression<Func<TSource, TProperty>> selector, Action<TProperty> handler)
public WeakPropertyChangedHandler(TSource source, Expression<Func<TSource, TProperty>> selector, EventHandler<PropertyChangedExtendedEventArgs<TProperty>> 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<TProperty>(this.propertyName, this.valueSelector(source)));
}
public void Unbind()
@ -110,21 +133,21 @@ namespace Stylet
/// <param name="targetSelector">MemberExpression selecting the property to observe for changes (e.g x => x.PropertyName)</param>
/// <param name="handler">Handler called whenever that property changed</param>
/// <returns>Something which can be used to undo the binding. You can discard it if you want</returns>
public static IEventBinding Bind<TBindTo, TMember>(this TBindTo target, Expression<Func<TBindTo, TMember>> targetSelector, Action<TMember> handler) where TBindTo : class, INotifyPropertyChanged
public static IEventBinding Bind<TSource, TProperty>(this TSource target, Expression<Func<TSource, TProperty>> targetSelector, EventHandler<PropertyChangedExtendedEventArgs<TProperty>> 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<TBindTo>(target);
var weakTarget = new WeakReference<TSource>(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<TProperty>(propertyName, propertyAccess(strongTarget)));
}
};
@ -135,13 +158,21 @@ namespace Stylet
return listener;
}
public static IEventBinding BindWeak<TBindTo, TMember>(this TBindTo target, Expression<Func<TBindTo, TMember>> targetSelector, Action<TMember> handler) where TBindTo : class, INotifyPropertyChanged
/// <summary>
/// Weakly bind to PropertyChanged events for a particular property on a particular object
/// </summary>
/// <example>someObject.Bind(x => x.PropertyNameToBindTo, newValue => /* do something with the new value */)</example>
/// <param name="target">Object raising the PropertyChanged event you're interested in</param>
/// <param name="targetSelector">MemberExpression selecting the property to observe for changes (e.g x => x.PropertyName)</param>
/// <param name="handler">Handler called whenever that property changed</param>
/// <returns>Something which can be used to undo the binding. You can discard it if you want</returns>
public static IEventBinding BindWeak<TSource, TProperty>(this TSource target, Expression<Func<TSource, TProperty>> targetSelector, EventHandler<PropertyChangedExtendedEventArgs<TProperty>> handler) where TSource : class, INotifyPropertyChanged
{
var attribute = handler.Target.GetType().GetCustomAttribute<CompilerGeneratedAttribute>();
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<TBindTo, TMember>(target, targetSelector, handler);
var binding = new WeakPropertyChangedHandler<TSource, TProperty>(target, targetSelector, handler);
return new WeakPropertyChangedBinding(binding);
}
}

View File

@ -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<InvalidOperationException>(() => c1.BindWeak(x => x.Foo, x => newVal = x));
Assert.Throws<InvalidOperationException>(() => c1.BindWeak(x => x.Foo, (o, e) => newVal = e.NewValue));
}
}
}