Use a DependencyProperty in CommandAction/EventAction to watch ActionTarget for changes

Seems to work much better. Could probably remove quite a bit of code though
a sensible base class
This commit is contained in:
Antony Male 2015-02-23 17:41:12 +00:00
parent f75833f508
commit b98226b29f
7 changed files with 59 additions and 221 deletions

View File

@ -1,95 +0,0 @@
using System;
using System.Diagnostics;
using System.Windows;
using System.Windows.Data;
namespace Stylet
{
/// <summary>
/// DependencyProperty change notifier which does not root the DependencyObject
/// </summary>
// Adapted from https://agsmith.wordpress.com/2008/04/07/propertydescriptor-addvaluechanged-alternative/
public class DependencyPropertyChangeNotifier : DependencyObject, IDisposable
{
/// <summary>
/// Watch for changes of the given property on the given propertySource
/// </summary>
/// <param name="propertySource">Object to observe a property on</param>
/// <param name="property">Property on the object to observe</param>
/// <param name="handler">Handler to invoke when the property changes</param>
/// <returns>The constructed PropertyChangeNotifier</returns>
public static DependencyPropertyChangeNotifier AddValueChanged(DependencyObject propertySource, PropertyPath property, PropertyChangedCallback handler)
{
return new DependencyPropertyChangeNotifier(propertySource, property, handler);
}
/// <summary>
/// Watch for changes of the given property on the given propertySource
/// </summary>
/// <param name="propertySource">Object to observe a property on</param>
/// <param name="property">Property on the object to observe</param>
/// <param name="handler">Handler to invoke when the property changes</param>
/// <returns>The constructed PropertyChangeNotifier</returns>
public static DependencyPropertyChangeNotifier AddValueChanged(DependencyObject propertySource, DependencyProperty property, PropertyChangedCallback handler)
{
if (property == null)
throw new ArgumentNullException("property");
return AddValueChanged(propertySource, new PropertyPath(property), handler);
}
private PropertyChangedCallback handler;
private readonly WeakReference<DependencyObject> propertySource;
private DependencyPropertyChangeNotifier(DependencyObject propertySource, PropertyPath property, PropertyChangedCallback handler)
{
if (propertySource == null)
throw new ArgumentNullException("propertySource");
if (property == null)
throw new ArgumentNullException("property");
if (handler == null)
throw new ArgumentNullException("handler");
this.propertySource = new WeakReference<DependencyObject>(propertySource);
var binding = new Binding()
{
Path = property,
Mode = BindingMode.OneWay,
Source = propertySource
};
BindingOperations.SetBinding(this, ValueProperty, binding);
// Needs to be set after binding set, so it doesn't catch the initial property set
this.handler = handler;
}
private void OnValueChanged(DependencyPropertyChangedEventArgs e)
{
// This happens on the firsrt invocation ever, when the initial value is set
// and on disposal
if (this.handler == null)
return;
// Target *should* never be null at this point...
DependencyObject propertySource = null;
this.propertySource.TryGetTarget(out propertySource);
Debug.Assert(propertySource != null);
this.handler(propertySource, e);
}
private static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(object), typeof(DependencyPropertyChangeNotifier), new FrameworkPropertyMetadata(null, (d, e) =>
{
((DependencyPropertyChangeNotifier)d).OnValueChanged(e);
}));
/// <summary>
/// Releases the binding
/// </summary>
public void Dispose()
{
this.handler = null; // Otherwise it's called as the binding is unset
BindingOperations.ClearBinding(this, ValueProperty);
}
}
}

View File

@ -52,7 +52,6 @@
<Compile Include="Logging\ILogger.cs" />
<Compile Include="Logging\NullLogger.cs" />
<Compile Include="Logging\TraceLogger.cs" />
<Compile Include="DependencyPropertyChangeNotifier.cs" />
<Compile Include="StyletIoC\Creation\ICreator.cs" />
<Compile Include="StyletIoC\Creation\IRegistration.cs" />
<Compile Include="StyletIoC\Internal\Builders\BuilderAbstractFactoryBinding.cs" />

View File

@ -4,6 +4,7 @@ using System.ComponentModel;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Windows;
using System.Windows.Data;
using System.Windows.Input;
using Expressions = System.Linq.Expressions;
@ -17,7 +18,7 @@ namespace Stylet.Xaml
/// Watches the current View.ActionTarget, and looks for a method with the given name, calling it when the ICommand is called.
/// If a bool property with name Get(methodName) exists, it will be observed and used to enable/disable the ICommand.
/// </remarks>
public class CommandAction : ICommand
public class CommandAction : DependencyObject, ICommand
{
private static readonly ILogger logger = LogManager.GetLogger(typeof(CommandAction));
@ -41,11 +42,20 @@ namespace Stylet.Xaml
/// </summary>
private MethodInfo targetMethodInfo;
private object target;
private readonly ActionUnavailableBehaviour targetNullBehaviour;
private readonly ActionUnavailableBehaviour actionNonExistentBehaviour;
private object target
{
get { return (object)GetValue(targetProperty); }
}
private static readonly DependencyProperty targetProperty =
DependencyProperty.Register("target", typeof(object), typeof(CommandAction), new PropertyMetadata(null, (d, e) =>
{
((CommandAction)d).UpdateGuardAndMethod(e.OldValue, e.NewValue);
}));
/// <summary>
/// Initialises a new instance of the <see cref="CommandAction"/> class
/// </summary>
@ -60,10 +70,13 @@ namespace Stylet.Xaml
this.targetNullBehaviour = targetNullBehaviour;
this.actionNonExistentBehaviour = actionNonExistentBehaviour;
this.UpdateGuardAndMethod();
// Observe the View.ActionTarget for changes, and re-bind the guard property and MethodInfo if it changes
DependencyPropertyChangeNotifier.AddValueChanged(this.Subject, View.ActionTargetProperty, (o, e) => this.UpdateGuardAndMethod());
var binding = new Binding()
{
Path = new PropertyPath(View.ActionTargetProperty),
Mode = BindingMode.OneWay,
Source = this.Subject,
};
BindingOperations.SetBinding(this, targetProperty, binding);
}
private string GuardName
@ -71,9 +84,8 @@ namespace Stylet.Xaml
get { return "Can" + this.MethodName; }
}
private void UpdateGuardAndMethod()
private void UpdateGuardAndMethod(object oldTarget, object newTarget)
{
var newTarget = View.GetActionTarget(this.Subject);
MethodInfo targetMethodInfo = null;
// If it's being set to the initial value, ignore it
@ -82,7 +94,6 @@ namespace Stylet.Xaml
// We'll just wait until the ActionTarget is assigned, and we're called again
if (newTarget == View.InitialActionTarget)
{
this.target = newTarget;
return;
}
@ -146,15 +157,14 @@ namespace Stylet.Xaml
}
}
var oldTarget = this.target as INotifyPropertyChanged;
if (oldTarget != null)
PropertyChangedEventManager.RemoveHandler(oldTarget, this.PropertyChangedHandler, this.GuardName);
var oldInpc = oldTarget as INotifyPropertyChanged;
if (oldInpc != null)
PropertyChangedEventManager.RemoveHandler(oldInpc, this.PropertyChangedHandler, this.GuardName);
var inpc = newTarget as INotifyPropertyChanged;
if (this.guardPropertyGetter != null && inpc != null)
PropertyChangedEventManager.AddHandler(inpc, this.PropertyChangedHandler, this.GuardName);
this.target = newTarget;
this.targetMethodInfo = targetMethodInfo;
this.UpdateCanExecute();

View File

@ -4,13 +4,14 @@ using System.ComponentModel;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Windows;
using System.Windows.Data;
namespace Stylet.Xaml
{
/// <summary>
/// Created by ActionExtension, this can return a delegate suitable adding binding to an event, and can call a method on the View.ActionTarget
/// </summary>
public class EventAction
public class EventAction : DependencyObject
{
private static readonly ILogger logger = LogManager.GetLogger(typeof(EventAction));
private static readonly MethodInfo invokeCommandMethodInfo = typeof(EventAction).GetMethod("InvokeCommand", BindingFlags.NonPublic | BindingFlags.Instance);
@ -38,7 +39,17 @@ namespace Stylet.Xaml
/// </summary>
private MethodInfo targetMethodInfo;
private object target;
private object target
{
get { return (object)GetValue(targetProperty); }
}
// Using a DependencyProperty as the backing store for target. This enables animation, styling, binding, etc...
private static readonly DependencyProperty targetProperty =
DependencyProperty.Register("target", typeof(object), typeof(EventAction), new PropertyMetadata(null, (d, e) =>
{
((EventAction)d).UpdateMethod(e.NewValue);
}));
/// <summary>
/// Initialises a new instance of the <see cref="EventAction"/> class
@ -61,15 +72,17 @@ namespace Stylet.Xaml
this.targetNullBehaviour = targetNullBehaviour;
this.actionNonExistentBehaviour = actionNonExistentBehaviour;
this.UpdateMethod();
// Observe the View.ActionTarget for changes, and re-bind the guard property and MethodInfo if it changes
DependencyPropertyChangeNotifier.AddValueChanged(this.subject, View.ActionTargetProperty, (o, e) => this.UpdateMethod());
var binding = new Binding()
{
Path = new PropertyPath(View.ActionTargetProperty),
Mode = BindingMode.OneWay,
Source = this.subject,
};
BindingOperations.SetBinding(this, targetProperty, binding);
}
private void UpdateMethod()
private void UpdateMethod(object newTarget)
{
var newTarget = View.GetActionTarget(this.subject);
MethodInfo targetMethodInfo = null;
// If it's being set to the initial value, ignore it
@ -78,7 +91,6 @@ namespace Stylet.Xaml
// We'll just wait until the ActionTarget is assigned, and we're called again
if (newTarget == View.InitialActionTarget)
{
this.target = newTarget;
return;
}
@ -126,7 +138,6 @@ namespace Stylet.Xaml
}
}
this.target = newTarget;
this.targetMethodInfo = targetMethodInfo;
}

View File

@ -270,5 +270,19 @@ namespace StyletUnitTests
Assert.False(weakView.IsAlive);
}
[Test]
public void OperatesAfterCollection()
{
var view = new DependencyObject();
var cmd = new CommandAction(view, "DoSomething", ActionUnavailableBehaviour.Throw, ActionUnavailableBehaviour.Throw);
GC.Collect();
View.SetActionTarget(view, this.target);
cmd.Execute(null);
Assert.IsTrue(this.target.DoSomethingCalled);
}
}
}

View File

@ -1,100 +0,0 @@
using NUnit.Framework;
using Stylet;
using Stylet.Xaml;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
namespace StyletUnitTests
{
[TestFixture]
public class DependencyPropertyChangeNotifierTests
{
[Test]
public void ThrowsIfTargetIsNull()
{
Assert.Throws<ArgumentNullException>(() => DependencyPropertyChangeNotifier.AddValueChanged(null, View.ActionTargetProperty, (d, e) => { }));
}
[Test]
public void ThrowsIfPropertyIsNull()
{
Assert.Throws<ArgumentNullException>(() => DependencyPropertyChangeNotifier.AddValueChanged(new DependencyObject(), (PropertyPath)null, (d, e) => { }));
Assert.Throws<ArgumentNullException>(() => DependencyPropertyChangeNotifier.AddValueChanged(new DependencyObject(), (DependencyProperty)null, (d, e) => { }));
}
[Test]
public void ThrowsIfHandlerIsNull()
{
Assert.Throws<ArgumentNullException>(() => DependencyPropertyChangeNotifier.AddValueChanged(new DependencyObject(), View.ActionTargetProperty, null));
}
[Test]
public void DoesNotRetainTarget()
{
var target = new DependencyObject();
var weakTarget = new WeakReference(target);
DependencyPropertyChangeNotifier.AddValueChanged(target, View.ActionTargetProperty, (d, e) => { });
target = null;
GC.Collect();
Assert.IsFalse(weakTarget.IsAlive);
}
[Test]
public void NotifiesOfChange()
{
var view = new DependencyObject();
var value1 = new object();
var value2 = new object();
View.SetActionTarget(view, value1);
DependencyObject subject = null;
DependencyPropertyChangedEventArgs ea = default(DependencyPropertyChangedEventArgs);
DependencyPropertyChangeNotifier.AddValueChanged(view, View.ActionTargetProperty, (d, e) =>
{
subject = d;
ea = e;
});
View.SetActionTarget(view, value2);
Assert.AreEqual(view, subject);
Assert.AreEqual(value1, ea.OldValue);
Assert.AreEqual(value2, ea.NewValue);
}
[Test]
public void HandlerNotCalledBeforeDependencyPropertyChanged()
{
var view = new DependencyObject();
var called = false;
DependencyPropertyChangeNotifier.AddValueChanged(view, View.ActionTargetProperty, (d, e) => called = true);
Assert.IsFalse(called);
}
[Test]
public void DisposeUnsubscribes()
{
var view = new DependencyObject();
var called = false;
var disposable = DependencyPropertyChangeNotifier.AddValueChanged(view, View.ActionTargetProperty, (d, e) => called = true);
disposable.Dispose();
View.SetActionTarget(view, new object());
Assert.IsFalse(called);
}
}
}

View File

@ -64,7 +64,6 @@
<Compile Include="ConductorOneActiveTests.cs" />
<Compile Include="ConductorTests.cs" />
<Compile Include="DebugConverterTests.cs" />
<Compile Include="DependencyPropertyChangeNotifierTests.cs" />
<Compile Include="StyletIoC\StyletIoCFuncFactoryTests.cs" />
<Compile Include="StyletIoC\StyletIoCInstanceBindingTests.cs" />
<Compile Include="StyletIoC\StyletIoCModuleTests.cs" />