ActionExtension: Handle attached events

This commit is contained in:
Antony Male 2015-01-14 13:39:09 +00:00
parent f7a7654984
commit c777d295af
4 changed files with 81 additions and 28 deletions

View File

@ -61,6 +61,26 @@ namespace Stylet.Xaml
this.Method = method;
}
private ActionUnavailableBehaviour commandNullTargetBehaviour
{
get { return this.NullTarget == ActionUnavailableBehaviour.Default ? (Execute.InDesignMode ? ActionUnavailableBehaviour.Enable : ActionUnavailableBehaviour.Disable) : this.NullTarget; }
}
private ActionUnavailableBehaviour commandActionNotFoundBehaviour
{
get { return this.ActionNotFound == ActionUnavailableBehaviour.Default ? ActionUnavailableBehaviour.Throw : this.ActionNotFound; }
}
private ActionUnavailableBehaviour eventNullTargetBehaviour
{
get { return this.NullTarget == ActionUnavailableBehaviour.Default ? ActionUnavailableBehaviour.Enable : this.NullTarget; }
}
private ActionUnavailableBehaviour eventActionNotFoundBehaviour
{
get { return this.ActionNotFound == ActionUnavailableBehaviour.Default ? ActionUnavailableBehaviour.Throw : this.ActionNotFound; }
}
/// <summary>
/// When implemented in a derived class, returns an object that is provided as the value of the target property for this markup extension.
/// </summary>
@ -79,19 +99,28 @@ namespace Stylet.Xaml
if (propertyAsDependencyProperty != null && propertyAsDependencyProperty.PropertyType == typeof(ICommand))
{
// If they're in design mode and haven't set View.ActionTarget, default to looking sensible
var nullTarget = this.NullTarget == ActionUnavailableBehaviour.Default ? (Execute.InDesignMode ? ActionUnavailableBehaviour.Enable : ActionUnavailableBehaviour.Disable) : this.NullTarget;
var actionNotFound = this.ActionNotFound == ActionUnavailableBehaviour.Default ? ActionUnavailableBehaviour.Throw : this.ActionNotFound;
return new CommandAction((DependencyObject)valueService.TargetObject, this.Method, nullTarget, actionNotFound);
return new CommandAction((DependencyObject)valueService.TargetObject, this.Method, this.commandNullTargetBehaviour, this.commandActionNotFoundBehaviour);
}
var propertyAsEventInfo = valueService.TargetProperty as EventInfo;
if (propertyAsEventInfo != null)
{
var nullTarget = this.NullTarget == ActionUnavailableBehaviour.Default ? ActionUnavailableBehaviour.Enable : this.NullTarget;
var actionNotFound = this.ActionNotFound == ActionUnavailableBehaviour.Default ? ActionUnavailableBehaviour.Throw : this.ActionNotFound;
var ec = new EventAction((DependencyObject)valueService.TargetObject, propertyAsEventInfo, this.Method, nullTarget, actionNotFound);
var ec = new EventAction((DependencyObject)valueService.TargetObject, propertyAsEventInfo.EventHandlerType, this.Method, this.eventNullTargetBehaviour, this.eventActionNotFoundBehaviour);
return ec.GetDelegate();
}
// For attached events
var propertyAsMethodInfo = valueService.TargetProperty as MethodInfo;
if (propertyAsMethodInfo != null)
{
var parameters = propertyAsMethodInfo.GetParameters();
if (parameters.Length == 2 && typeof(RoutedEventHandler).IsAssignableFrom(parameters[1].ParameterType))
{
var ec = new EventAction((DependencyObject)valueService.TargetObject, parameters[1].ParameterType, this.Method, this.eventNullTargetBehaviour, this.eventActionNotFoundBehaviour);
return ec.GetDelegate();
}
}
throw new ArgumentException("Can only use ActionExtension with a Command property or an event handler");
}

View File

@ -29,9 +29,9 @@ namespace Stylet.Xaml
private readonly string methodName;
/// <summary>
/// Property on the WPF element we're returning a delegate for
/// Type of event handler
/// </summary>
private readonly EventInfo targetProperty;
private readonly Type eventHandlerType;
/// <summary>
/// MethodInfo for the method to call. This has to exist, or we throw a wobbly
@ -44,11 +44,11 @@ namespace Stylet.Xaml
/// Initialises a new instance of the <see cref="EventAction"/> class
/// </summary>
/// <param name="subject">View whose View.ActionTarget we watch</param>
/// <param name="targetProperty">Property on the WPF element we're returning a delegate for</param>
/// <param name="eventHandlerType">Type of event handler we're returning a delegate for</param>
/// <param name="methodName">The MyMethod in {s:Action MyMethod}, this is what we call when the event's fired</param>
/// <param name="targetNullBehaviour">Behaviour for it the relevant View.ActionTarget is null</param>
/// <param name="actionNonExistentBehaviour">Behaviour for if the action doesn't exist on the View.ActionTarget</param>
public EventAction(DependencyObject subject, EventInfo targetProperty, string methodName, ActionUnavailableBehaviour targetNullBehaviour, ActionUnavailableBehaviour actionNonExistentBehaviour)
public EventAction(DependencyObject subject, Type eventHandlerType, string methodName, ActionUnavailableBehaviour targetNullBehaviour, ActionUnavailableBehaviour actionNonExistentBehaviour)
{
if (targetNullBehaviour == ActionUnavailableBehaviour.Disable)
throw new ArgumentException("Setting NullTarget = Disable is unsupported when used on an Event");
@ -56,7 +56,7 @@ namespace Stylet.Xaml
throw new ArgumentException("Setting ActionNotFound = Disable is unsupported when used on an Event");
this.subject = subject;
this.targetProperty = targetProperty;
this.eventHandlerType = eventHandlerType;
this.methodName = methodName;
this.targetNullBehaviour = targetNullBehaviour;
this.actionNonExistentBehaviour = actionNonExistentBehaviour;
@ -133,8 +133,7 @@ namespace Stylet.Xaml
/// <returns>An event hander, which, when invoked, will invoke the action</returns>
public Delegate GetDelegate()
{
var parameterType = this.targetProperty.EventHandlerType;
var del = Delegate.CreateDelegate(parameterType, this, invokeCommandMethodInfo, false);
var del = Delegate.CreateDelegate(this.eventHandlerType, this, invokeCommandMethodInfo, false);
if (del == null)
{
var e = new ActionEventSignatureInvalidException(String.Format("Event being bound to does not have the '(object sender, EventArgsOrSubclass e)' signature we were expecting. Method {0} on target {1}", this.methodName, this.target));

View File

@ -20,6 +20,23 @@ namespace StyletUnitTests
private Mock<IProvideValueTarget> provideValueTarget;
private Mock<IServiceProvider> serviceProvider;
private class TestExtensions
{
public static readonly RoutedEvent TestEvent = EventManager.RegisterRoutedEvent("Test", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(TestExtensions));
public static void AddTestHandler(DependencyObject d, RoutedEventHandler handler)
{
UIElement uie = d as UIElement;
if (uie != null)
uie.AddHandler(TestExtensions.TestEvent, handler);
}
public static void RemoveTestHandler(DependencyObject d, RoutedEventHandler handler)
{
UIElement uie = d as UIElement;
if (uie != null)
uie.RemoveHandler(TestExtensions.TestEvent, handler);
}
}
[SetUp]
public void SetUp()
{
@ -62,6 +79,14 @@ namespace StyletUnitTests
Assert.IsInstanceOf<RoutedEventHandler>(this.actionExtension.ProvideValue(this.serviceProvider.Object));
}
[Test]
public void ReturnsEventActionIfTargetIsAttachedEvent()
{
this.provideValueTarget.Setup(x => x.TargetProperty).Returns(typeof(TestExtensions).GetMethod("AddTestHandler"));
Assert.IsInstanceOf<RoutedEventHandler>(this.actionExtension.ProvideValue(this.serviceProvider.Object));
}
[Test]
public void ThrowsArgumentExceptionIfTargetObjectNotDependencyPropertyOrEventInfo()
{

View File

@ -80,57 +80,57 @@ namespace StyletUnitTests
[Test]
public void ThrowsIfNullTargetBehaviourIsDisable()
{
Assert.Throws<ArgumentException>(() => new EventAction(this.subject, this.eventInfo, "DoSomething", ActionUnavailableBehaviour.Disable, ActionUnavailableBehaviour.Enable));
Assert.Throws<ArgumentException>(() => new EventAction(this.subject, this.eventInfo.EventHandlerType, "DoSomething", ActionUnavailableBehaviour.Disable, ActionUnavailableBehaviour.Enable));
}
[Test]
public void ThrowsIfNonExistentActionBehaviourIsDisable()
{
Assert.Throws<ArgumentException>(() => new EventAction(this.subject, this.eventInfo, "DoSomething", ActionUnavailableBehaviour.Enable, ActionUnavailableBehaviour.Disable));
Assert.Throws<ArgumentException>(() => new EventAction(this.subject, this.eventInfo.EventHandlerType, "DoSomething", ActionUnavailableBehaviour.Enable, ActionUnavailableBehaviour.Disable));
}
[Test]
public void ThrowsIfTargetNullBehaviourIsThrowAndTargetBecomesNull()
{
var cmd = new EventAction(this.subject, this.eventInfo, "DoSomething", ActionUnavailableBehaviour.Throw, ActionUnavailableBehaviour.Enable);
var cmd = new EventAction(this.subject, this.eventInfo.EventHandlerType, "DoSomething", ActionUnavailableBehaviour.Throw, ActionUnavailableBehaviour.Enable);
Assert.Throws<ActionTargetNullException>(() => View.SetActionTarget(this.subject, null));
}
[Test]
public void ThrowsIfActionNonExistentBehaviourIsThrowAndActionIsNonExistent()
{
var cmd = new EventAction(this.subject, this.eventInfo, "DoSomething", ActionUnavailableBehaviour.Enable, ActionUnavailableBehaviour.Throw);
var cmd = new EventAction(this.subject, this.eventInfo.EventHandlerType, "DoSomething", ActionUnavailableBehaviour.Enable, ActionUnavailableBehaviour.Throw);
Assert.Throws<ActionNotFoundException>(() => View.SetActionTarget(this.subject, new Target2()));
}
[Test]
public void ThrowsIfMethodHasTooManyArguments()
{
Assert.Throws<ActionSignatureInvalidException>(() => new EventAction(this.subject, this.eventInfo, "DoSomethingWithTooManyArguments", ActionUnavailableBehaviour.Enable, ActionUnavailableBehaviour.Enable));
Assert.Throws<ActionSignatureInvalidException>(() => new EventAction(this.subject, this.eventInfo.EventHandlerType, "DoSomethingWithTooManyArguments", ActionUnavailableBehaviour.Enable, ActionUnavailableBehaviour.Enable));
}
[Test]
public void ThrowsIfMethodHasBadParameter()
{
Assert.Throws<ActionSignatureInvalidException>(() => new EventAction(this.subject, this.eventInfo, "DoSomethingWithBadArgument", ActionUnavailableBehaviour.Enable, ActionUnavailableBehaviour.Enable));
Assert.Throws<ActionSignatureInvalidException>(() => new EventAction(this.subject, this.eventInfo.EventHandlerType, "DoSomethingWithBadArgument", ActionUnavailableBehaviour.Enable, ActionUnavailableBehaviour.Enable));
}
[Test]
public void ThrowsIfMethodHasBadEventArgsParameter()
{
Assert.Throws<ActionSignatureInvalidException>(() => new EventAction(this.subject, this.eventInfo, "DoSomethingWithSenderAndBadArgument", ActionUnavailableBehaviour.Enable, ActionUnavailableBehaviour.Enable));
Assert.Throws<ActionSignatureInvalidException>(() => new EventAction(this.subject, this.eventInfo.EventHandlerType, "DoSomethingWithSenderAndBadArgument", ActionUnavailableBehaviour.Enable, ActionUnavailableBehaviour.Enable));
}
[Test]
public void ThrowsIfMethodHasTooManyParameters()
{
Assert.Throws<ActionSignatureInvalidException>(() => new EventAction(this.subject, this.eventInfo, "DoSomethingWithTooManyArguments", ActionUnavailableBehaviour.Enable, ActionUnavailableBehaviour.Enable));
Assert.Throws<ActionSignatureInvalidException>(() => new EventAction(this.subject, this.eventInfo.EventHandlerType, "DoSomethingWithTooManyArguments", ActionUnavailableBehaviour.Enable, ActionUnavailableBehaviour.Enable));
}
[Test]
public void InvokingCommandDoesNothingIfTargetIsNull()
{
var cmd = new EventAction(this.subject, this.eventInfo, "DoSomething", ActionUnavailableBehaviour.Enable, ActionUnavailableBehaviour.Enable);
var cmd = new EventAction(this.subject, this.eventInfo.EventHandlerType, "DoSomething", ActionUnavailableBehaviour.Enable, ActionUnavailableBehaviour.Enable);
View.SetActionTarget(this.subject, null);
cmd.GetDelegate().DynamicInvoke(null, null);
}
@ -138,7 +138,7 @@ namespace StyletUnitTests
[Test]
public void InvokingCommandDoesNothingIfActionIsNonExistent()
{
var cmd = new EventAction(this.subject, this.eventInfo, "DoSomething", ActionUnavailableBehaviour.Enable, ActionUnavailableBehaviour.Enable);
var cmd = new EventAction(this.subject, this.eventInfo.EventHandlerType, "DoSomething", ActionUnavailableBehaviour.Enable, ActionUnavailableBehaviour.Enable);
View.SetActionTarget(this.subject, new Target2());
cmd.GetDelegate().DynamicInvoke(null, null);
}
@ -146,7 +146,7 @@ namespace StyletUnitTests
[Test]
public void InvokingCommandCallsMethod()
{
var cmd = new EventAction(this.subject, this.eventInfo, "DoSomething", ActionUnavailableBehaviour.Enable, ActionUnavailableBehaviour.Enable);
var cmd = new EventAction(this.subject, this.eventInfo.EventHandlerType, "DoSomething", ActionUnavailableBehaviour.Enable, ActionUnavailableBehaviour.Enable);
cmd.GetDelegate().DynamicInvoke(null, null);
Assert.True(this.target.DoSomethingCalled);
}
@ -154,7 +154,7 @@ namespace StyletUnitTests
[Test]
public void InvokingCommandCallsMethodWithEventArgs()
{
var cmd = new EventAction(this.subject, this.eventInfo, "DoSomethingWithEventArgs", ActionUnavailableBehaviour.Enable, ActionUnavailableBehaviour.Enable);
var cmd = new EventAction(this.subject, this.eventInfo.EventHandlerType, "DoSomethingWithEventArgs", ActionUnavailableBehaviour.Enable, ActionUnavailableBehaviour.Enable);
var arg = new RoutedEventArgs();
cmd.GetDelegate().DynamicInvoke(null, arg);
Assert.AreEqual(arg, this.target.EventArgs);
@ -163,7 +163,7 @@ namespace StyletUnitTests
[Test]
public void InvokingCommandCallsMethodWithSenderAndEventArgs()
{
var cmd = new EventAction(this.subject, this.eventInfo, "DoSomethingWithObjectAndEventArgs", ActionUnavailableBehaviour.Enable, ActionUnavailableBehaviour.Enable);
var cmd = new EventAction(this.subject, this.eventInfo.EventHandlerType, "DoSomethingWithObjectAndEventArgs", ActionUnavailableBehaviour.Enable, ActionUnavailableBehaviour.Enable);
var sender = new object();
var arg = new RoutedEventArgs();
cmd.GetDelegate().DynamicInvoke(sender, arg);
@ -175,14 +175,14 @@ namespace StyletUnitTests
[Test]
public void BadEventHandlerSignatureThrows()
{
var cmd = new EventAction(this.subject, typeof(Subject).GetEvent("BadEventHandler"), "DoSomething", ActionUnavailableBehaviour.Enable, ActionUnavailableBehaviour.Enable);
var cmd = new EventAction(this.subject, typeof(Subject).GetEvent("BadEventHandler").EventHandlerType, "DoSomething", ActionUnavailableBehaviour.Enable, ActionUnavailableBehaviour.Enable);
Assert.Throws<ActionEventSignatureInvalidException>(() => cmd.GetDelegate());
}
[Test]
public void PropagatesActionException()
{
var cmd = new EventAction(this.subject, this.eventInfo, "DoSomethingUnsuccessfully", ActionUnavailableBehaviour.Enable, ActionUnavailableBehaviour.Enable);
var cmd = new EventAction(this.subject, this.eventInfo.EventHandlerType, "DoSomethingUnsuccessfully", ActionUnavailableBehaviour.Enable, ActionUnavailableBehaviour.Enable);
var e = Assert.Throws<TargetInvocationException>(() => cmd.GetDelegate().DynamicInvoke(null, null));
Assert.IsInstanceOf<InvalidOperationException>(e.InnerException);
Assert.AreEqual("foo", e.InnerException.Message);