Replace BindWeak with a WeakEventManager, and let Screen have an instance

This commit is contained in:
Antony Male 2014-04-17 10:11:53 +01:00
parent f8bb1838d3
commit 6eb9c6dbef
5 changed files with 107 additions and 94 deletions

View File

@ -19,50 +19,6 @@ namespace Stylet
public static class PropertyChangedExtensions
{
/// <summary>
/// Mapping of thing doing the binding -> bindings. Makes sure we retain the bindings (and any compiler-generated classes) for as long as the thing which cares about the notifications exists
/// </summary>
private static ConditionalWeakTable<object, List<WeakPropertyChangedBinding>> eventMapping = new ConditionalWeakTable<object, List<WeakPropertyChangedBinding>>();
private static object eventMappingLock = new object();
internal class WeakPropertyChangedBinding : IPropertyChangedBinding
{
private readonly string propertyName;
private readonly EventHandler<PropertyChangedEventArgs> handler;
private readonly WeakReference binder;
private readonly WeakReference<INotifyPropertyChanged> inpc;
public WeakPropertyChangedBinding(object binder, INotifyPropertyChanged inpc, string propertyName, EventHandler<PropertyChangedEventArgs> handler)
{
this.binder = new WeakReference(binder);
this.inpc = new WeakReference<INotifyPropertyChanged>(inpc);
this.propertyName = propertyName;
// We need to keep this strongly, in case its target is a compiler-generated class instance
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<WeakPropertyChangedBinding> listeners;
if (eventMapping.TryGetValue(handler, out listeners))
listeners.Remove(this);
}
}
}
}
internal class StrongPropertyChangedBinding : IPropertyChangedBinding
{
private WeakReference<INotifyPropertyChanged> inpc;
@ -84,51 +40,6 @@ namespace Stylet
}
}
/// <summary>
/// Weakly bind to PropertyChanged events for a particular property on a particular object. This won't retain the object registered to receive PropertyChange notifications
/// </summary>
/// <example>someObject.Bind(this, 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="binder">Object on which the handler is defined. This is needed because you might use an anonymous delegate as the handler, which might cause a compiler-generated class to be set as the handler's Target, and that needs to be retained for as this parameter</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 IPropertyChangedBinding BindWeak<TBindTo, TMember>(this TBindTo target, object binder, Expression<Func<TBindTo, TMember>> targetSelector, Action<TMember> handler) where TBindTo : class, INotifyPropertyChanged
{
var propertyName = targetSelector.NameForProperty();
var propertyAccess = targetSelector.Compile();
var weakTarget = new WeakReference<TBindTo>(target);
EventHandler<PropertyChangedEventArgs> ourHandler = (o, e) =>
{
TBindTo strongTarget;
if (weakTarget.TryGetTarget(out strongTarget))
handler(propertyAccess(strongTarget));
};
WeakPropertyChangedBinding weakListener = new WeakPropertyChangedBinding(binder, target, propertyName, ourHandler);
// 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<WeakPropertyChangedBinding> listeners;
if (!eventMapping.TryGetValue(binder, out listeners))
{
listeners = new List<WeakPropertyChangedBinding>();
eventMapping.Add(binder, listeners);
}
listeners.Add(weakListener);
}
PropertyChangedEventManager.AddHandler(target, ourHandler, propertyName);
return weakListener;
}
/// <summary>
/// Strongly bind to PropertyChanged events for a particular property on a particular object
/// </summary>

View File

@ -11,6 +11,9 @@ namespace Stylet
{
public class Screen : PropertyChangedBase, IScreen
{
private Lazy<IWeakEventManager> lazyWeakEventManager = new Lazy<IWeakEventManager>(() => new WeakEventManager(), true);
protected IWeakEventManager weakEventManager { get { return this.lazyWeakEventManager.Value; } }
#region IHaveDisplayName
private string _displayName;

View File

@ -48,6 +48,7 @@
<Compile Include="LabelledValue.cs" />
<Compile Include="SlightlyInternal\ConductorUtilities.cs" />
<Compile Include="INotifyPropertyChangedDispatcher.cs" />
<Compile Include="WeakEventManager.cs" />
<Compile Include="Xaml\ActionExtension.cs" />
<Compile Include="AssemblySource.cs" />
<Compile Include="BindableCollection.cs" />

View File

@ -0,0 +1,93 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
namespace Stylet
{
public interface IWeakEventManager
{
IPropertyChangedBinding BindWeak<TSource, TProperty>(TSource source, Expression<Func<TSource, TProperty>> selector, Action<TProperty> handler)
where TSource : class, INotifyPropertyChanged;
}
internal class WeakPropertyBinding<TSource, TProperty> : IPropertyChangedBinding where TSource : class, INotifyPropertyChanged
{
// Make sure we don't end up retaining the source
private readonly WeakReference<TSource> source;
private readonly string propertyName;
private readonly Func<TSource, TProperty> valueSelector;
private readonly Action<TProperty> handler;
private readonly Action<IPropertyChangedBinding> remover;
public WeakPropertyBinding(TSource source, Expression<Func<TSource, TProperty>> selector, Action<TProperty> handler, Action<IPropertyChangedBinding> remover)
{
this.source = new WeakReference<TSource>(source);
this.propertyName = selector.NameForProperty();
this.valueSelector = selector.Compile();
this.handler = handler;
this.remover = remover;
PropertyChangedEventManager.AddHandler(source, this.PropertyChangedHandler, this.propertyName);
}
internal void PropertyChangedHandler(object sender, PropertyChangedEventArgs e)
{
TSource source;
if (this.source.TryGetTarget(out source))
this.handler(this.valueSelector(source));
else
this.remover(this);
}
public void Unbind()
{
TSource source;
if (this.source.TryGetTarget(out source))
PropertyChangedEventManager.RemoveHandler(source, this.PropertyChangedHandler, this.propertyName);
this.remover(this);
}
}
public class WeakEventManager : IWeakEventManager
{
private object bindingsLock = new object();
private List<IPropertyChangedBinding> bindings = new List<IPropertyChangedBinding>();
public IPropertyChangedBinding BindWeak<TSource, TProperty>(TSource source, Expression<Func<TSource, TProperty>> selector, Action<TProperty> handler)
where TSource : class, INotifyPropertyChanged
{
// So, the handler's target might point to the class that owns us, or it might point to a compiler-generated class
// We assume we're owned by whatever determines how long the handler's target should live for
// Therefore we'll retain the handler's target for as long as we're alive (unless it's unregistered)
// The PropertyChangedEventManager is safe to use with delegates whose targets aren't compiler-generated, so we can
// ensure we provide a delegate with a non-compiler-generated target.
// To do this, we'll create a new WeakPropertyBinding instance, and retain it ourselves (so it lives as long as we do,
// and therefore as long as the thing that owns us does). The PropertyChangedEventManager will have a weak reference to
// the WeakPropertyBinding instance, so once we release it, it will too.
var propertyName = selector.NameForProperty();
var compiledSelector = selector.Compile();
var binding = new WeakPropertyBinding<TSource, TProperty>(source, selector, handler, this.Remove);
lock (this.bindingsLock)
{
this.bindings.Add(binding);
}
return binding;
}
internal void Remove(IPropertyChangedBinding binding)
{
lock (this.bindingsLock)
{
this.bindings.Remove(binding);
}
}
}
}

View File

@ -36,6 +36,7 @@ namespace StyletUnitTests
class BindingClass
{
public string LastFoo;
private WeakEventManager weakEventManager = new WeakEventManager();
public IPropertyChangedBinding BindStrong(NotifyingClass notifying)
{
@ -45,7 +46,7 @@ namespace StyletUnitTests
public IPropertyChangedBinding BindWeak(NotifyingClass notifying)
{
return notifying.BindWeak(this, x => x.Foo, x => this.LastFoo = x);
return this.weakEventManager.BindWeak(notifying, x => x.Foo, x => this.LastFoo = x);
}
}
@ -129,9 +130,10 @@ namespace StyletUnitTests
[Test]
public void WeakBindingBinds()
{
var manager = new WeakEventManager();
string newVal = null;
var c1 = new NotifyingClass();
c1.BindWeak(this, x => x.Foo, x => newVal = x);
manager.BindWeak(c1, x => x.Foo, x => newVal = x);
c1.Foo = "bar";
Assert.AreEqual("bar", newVal);
@ -140,9 +142,10 @@ namespace StyletUnitTests
[Test]
public void WeakBindingIgnoresOtherProperties()
{
var manager = new WeakEventManager();
string newVal = null;
var c1 = new NotifyingClass();
c1.BindWeak(this, x => x.Bar, x => newVal = x);
manager.BindWeak(c1, x => x.Bar, x => newVal = x);
c1.Foo = "bar";
Assert.AreEqual(null, newVal);
@ -151,10 +154,11 @@ namespace StyletUnitTests
[Test]
public void WeakBindingListensToEmptyString()
{
var manager = new WeakEventManager();
string newVal = null;
var c1 = new NotifyingClass();
c1.Bar = "bar";
c1.BindWeak(this, x => x.Bar, x => newVal = x);
manager.BindWeak(c1, x => x.Bar, x => newVal = x);
c1.NotifyAll();
Assert.AreEqual("bar", newVal);
@ -211,9 +215,10 @@ namespace StyletUnitTests
[Test]
public void WeakBindingUnbinds()
{
var manager = new WeakEventManager();
string newVal = null;
var c1 = new NotifyingClass();
var binding = c1.BindWeak(this, x => x.Bar, x => newVal = x);
var binding = manager.BindWeak(c1, x => x.Bar, x => newVal = x);
binding.Unbind();
c1.Bar = "bar";