Allow actions to have an explicit target

Relates to #177
This commit is contained in:
Antony Male 2021-02-18 08:54:11 +00:00
parent 6e2bc6e36c
commit e195d29c01
8 changed files with 163 additions and 35 deletions

View File

@ -3,6 +3,7 @@ using System;
using System.Diagnostics;
using System.Globalization;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using System.Windows;
using System.Windows.Data;
@ -47,6 +48,7 @@ namespace Stylet.Xaml
public object Target
{
get { return this.GetValue(targetProperty); }
private set { this.SetValue(targetProperty, value); }
}
private static readonly DependencyProperty targetProperty =
@ -56,7 +58,7 @@ namespace Stylet.Xaml
}));
/// <summary>
/// Initialises a new instance of the <see cref="ActionBase"/> class
/// Initialises a new instance of the <see cref="ActionBase"/> class to use <see cref="View.ActionTargetProperty"/> to get the target
/// </summary>
/// <param name="subject">View to grab the View.ActionTarget from</param>
/// <param name="backupSubject">Backup subject to use if no ActionTarget could be retrieved from the subject</param>
@ -65,12 +67,9 @@ namespace Stylet.Xaml
/// <param name="actionNonExistentBehaviour">Behaviour for if the action doesn't exist on the View.ActionTarget</param>
/// <param name="logger">Logger to use</param>
public ActionBase(DependencyObject subject, DependencyObject backupSubject, string methodName, ActionUnavailableBehaviour targetNullBehaviour, ActionUnavailableBehaviour actionNonExistentBehaviour, ILogger logger)
: this(methodName, targetNullBehaviour, actionNonExistentBehaviour, logger)
{
this.Subject = subject;
this.MethodName = methodName;
this.TargetNullBehaviour = targetNullBehaviour;
this.ActionNonExistentBehaviour = actionNonExistentBehaviour;
this.logger = logger;
// If a 'backupSubject' was given, bind both that and 'subject' to this.Target (with a converter which picks the first
// one that isn't View.InitialActionTarget). If it wasn't given, just bind 'subject'.
@ -101,6 +100,31 @@ namespace Stylet.Xaml
}
}
/// <summary>
/// Initialises a new instance of the <see cref="ActionBase"/> class to use an explicit target
/// </summary>
/// <param name="target">Target to find the method on</param>
/// <param name="methodName">Method name. the MyMethod in Buttom Command="{s:Action MyMethod}".</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>
/// <param name="logger">Logger to use</param>
public ActionBase(object target, string methodName, ActionUnavailableBehaviour targetNullBehaviour, ActionUnavailableBehaviour actionNonExistentBehaviour, ILogger logger)
: this(methodName, targetNullBehaviour, actionNonExistentBehaviour, logger)
{
if (target == null)
throw new ArgumentNullException(nameof(target));
this.Target = target;
}
private ActionBase(string methodName, ActionUnavailableBehaviour targetNullBehaviour, ActionUnavailableBehaviour actionNonExistentBehaviour, ILogger logger)
{
this.MethodName = methodName ?? throw new ArgumentNullException(nameof(methodName));
this.TargetNullBehaviour = targetNullBehaviour;
this.ActionNonExistentBehaviour = actionNonExistentBehaviour;
this.logger = logger;
}
private void UpdateActionTarget(object oldTarget, object newTarget)
{
MethodInfo targetMethodInfo = null;
@ -143,7 +167,7 @@ namespace Stylet.Xaml
targetMethodInfo = newTargetType.GetMethod(this.MethodName, bindingFlags);
if (targetMethodInfo == null)
this.logger.Warn("Unable to find method {0} on {1}", this.MethodName, newTargetType.Name);
this.logger.Warn("Unable to find{0} method {1} on {2}", newTarget is Type ? " static" : "", this.MethodName, newTargetType.Name);
else
this.AssertTargetMethodInfo(targetMethodInfo, newTargetType);
}
@ -192,7 +216,7 @@ namespace Stylet.Xaml
if (this.TargetMethodInfo == null && this.ActionNonExistentBehaviour == ActionUnavailableBehaviour.Throw)
{
var ex = new ActionNotFoundException(String.Format("Unable to find method {0} on target {1}", this.MethodName, this.Target.GetType().Name));
var ex = new ActionNotFoundException(String.Format("Unable to find method {0} on {1}", this.MethodName, this.TargetName()));
this.logger.Error(ex);
throw ex;
}
@ -204,7 +228,7 @@ namespace Stylet.Xaml
/// <param name="parameters">Parameters to pass to the target method</param>
protected internal void InvokeTargetMethod(object[] parameters)
{
this.logger.Info("Invoking method {0} on target {1} with parameters ({2})", this.MethodName, this.Target, parameters == null ? "none" : String.Join(", ", parameters));
this.logger.Info("Invoking method {0} on {1} with parameters ({2})", this.MethodName, this.TargetName(), parameters == null ? "none" : String.Join(", ", parameters));
try
{
@ -215,12 +239,19 @@ namespace Stylet.Xaml
{
// Be nice and unwrap this for them
// They want a stack track for their VM method, not us
this.logger.Error(e.InnerException, String.Format("Failed to invoke method {0} on target {1} with parameters ({2})", this.MethodName, this.Target, parameters == null ? "none" : String.Join(", ", parameters)));
this.logger.Error(e.InnerException, String.Format("Failed to invoke method {0} on {1} with parameters ({2})", this.MethodName, this.TargetName(), parameters == null ? "none" : String.Join(", ", parameters)));
// http://stackoverflow.com/a/17091351/1086121
ExceptionDispatchInfo.Capture(e.InnerException).Throw();
}
}
private string TargetName()
{
return this.Target is Type t
? $"static target {t.Name}"
: $"target {this.Target.GetType().Name}";
}
private class MultiBindingToActionTargetConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)

View File

@ -44,6 +44,11 @@ namespace Stylet.Xaml
[ConstructorArgument("method")]
public string Method { get; set; }
/// <summary>
/// Gets or sets a target to override that set with View.ActionTarget
/// </summary>
public object Target { get; set; }
/// <summary>
/// Gets or sets the behaviour if the View.ActionTarget is nulil
/// </summary>
@ -101,15 +106,13 @@ namespace Stylet.Xaml
throw new InvalidOperationException("Method has not been set");
var valueService = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
var rootObjectProvider = (IRootObjectProvider)serviceProvider.GetService(typeof(IRootObjectProvider));
var rootObject = rootObjectProvider?.RootObject as DependencyObject;
switch (valueService.TargetObject)
{
case DependencyObject targetObject:
return this.HandleDependencyObject(valueService, targetObject, rootObject);
return this.HandleDependencyObject(serviceProvider, valueService, targetObject);
case CommandBinding commandBinding:
return this.HandleCommandBinding(rootObject, ((EventInfo)valueService.TargetProperty).EventHandlerType);
return this.CreateEventAction(serviceProvider, null, ((EventInfo)valueService.TargetProperty).EventHandlerType, isCommandBinding: true);
default:
// Seems this is the case when we're in a template. We'll get called again properly in a second.
// http://social.msdn.microsoft.com/Forums/vstudio/en-US/a9ead3d5-a4e4-4f9c-b507-b7a7d530c6a9/gaining-access-to-target-object-instead-of-shareddp-in-custom-markupextensions-providevalue-method?forum=wpf
@ -117,39 +120,66 @@ namespace Stylet.Xaml
}
}
private object HandleDependencyObject(IProvideValueTarget valueService, DependencyObject targetObject, DependencyObject rootObject)
private object HandleDependencyObject(IServiceProvider serviceProvider, IProvideValueTarget valueService, DependencyObject targetObject)
{
switch (valueService.TargetProperty)
{
case DependencyProperty dependencyProperty when dependencyProperty.PropertyType == typeof(ICommand):
// If they're in design mode and haven't set View.ActionTarget, default to looking sensible
return new CommandAction(targetObject, rootObject, this.Method, this.CommandNullTargetBehaviour, this.CommandActionNotFoundBehaviour);
return this.CreateCommandAction(serviceProvider, targetObject);
case EventInfo eventInfo:
{
var ec = new EventAction(targetObject, rootObject, eventInfo.EventHandlerType, this.Method, this.EventNullTargetBehaviour, this.EventActionNotFoundBehaviour);
return ec.GetDelegate();
}
return this.CreateEventAction(serviceProvider, targetObject, eventInfo.EventHandlerType);
case MethodInfo methodInfo: // For attached events
{
var parameters = methodInfo.GetParameters();
if (parameters.Length == 2 && typeof(Delegate).IsAssignableFrom(parameters[1].ParameterType))
{
var ec = new EventAction(targetObject, rootObject, parameters[1].ParameterType, this.Method, this.EventNullTargetBehaviour, this.EventActionNotFoundBehaviour);
return ec.GetDelegate();
var parameters = methodInfo.GetParameters();
if (parameters.Length == 2 && typeof(Delegate).IsAssignableFrom(parameters[1].ParameterType))
{
return this.CreateEventAction(serviceProvider, targetObject, parameters[1].ParameterType);
}
throw new ArgumentException("Action used with an attached event (or something similar) which didn't follow the normal pattern");
}
throw new ArgumentException("Action used with an attached event (or something similar) which didn't follow the normal pattern");
}
default:
throw new ArgumentException("Can only use ActionExtension with a Command property or an event handler");
}
}
private object HandleCommandBinding(DependencyObject rootObject, Type propertyType)
private ICommand CreateCommandAction(IServiceProvider serviceProvider, DependencyObject targetObject)
{
if (rootObject == null)
throw new InvalidOperationException("Action may only be used with CommandBinding from a XAML view (unable to retrieve IRootObjectProvider.RootObject)");
if (this.Target == null)
{
var rootObjectProvider = (IRootObjectProvider)serviceProvider.GetService(typeof(IRootObjectProvider));
var rootObject = rootObjectProvider?.RootObject as DependencyObject;
return new CommandAction(targetObject, rootObject, this.Method, this.CommandNullTargetBehaviour, this.CommandActionNotFoundBehaviour);
}
else
{
return new CommandAction(this.Target, this.Method, this.CommandNullTargetBehaviour, this.CommandActionNotFoundBehaviour);
}
}
private Delegate CreateEventAction(IServiceProvider serviceProvider, DependencyObject targetObject, Type eventType, bool isCommandBinding = false)
{
EventAction ec;
if (this.Target == null)
{
var rootObjectProvider = (IRootObjectProvider)serviceProvider.GetService(typeof(IRootObjectProvider));
var rootObject = rootObjectProvider?.RootObject as DependencyObject;
if (isCommandBinding)
{
if (rootObject == null)
throw new InvalidOperationException("Action may only be used with CommandBinding from a XAML view (unable to retrieve IRootObjectProvider.RootObject)");
ec = new EventAction(rootObject, null, eventType, this.Method, this.EventNullTargetBehaviour, this.EventActionNotFoundBehaviour);
}
else
{
ec = new EventAction(targetObject, rootObject, eventType, this.Method, this.EventNullTargetBehaviour, this.EventActionNotFoundBehaviour);
}
}
else
{
ec = new EventAction(this.Target, eventType, this.Method, this.EventNullTargetBehaviour, this.EventActionNotFoundBehaviour);
}
var ec = new EventAction(rootObject, null, propertyType, this.Method, this.EventNullTargetBehaviour, this.EventActionNotFoundBehaviour);
return ec.GetDelegate();
}
}

View File

@ -26,7 +26,7 @@ namespace Stylet.Xaml
private Func<bool> guardPropertyGetter;
/// <summary>
/// Initialises a new instance of the <see cref="CommandAction"/> class
/// Initialises a new instance of the <see cref="CommandAction"/> class to use <see cref="View.ActionTargetProperty"/> to get the target
/// </summary>
/// <param name="subject">View to grab the View.ActionTarget from</param>
/// <param name="backupSubject">Backup subject to use if no ActionTarget could be retrieved from the subject</param>
@ -37,6 +37,17 @@ namespace Stylet.Xaml
: base(subject, backupSubject, methodName, targetNullBehaviour, actionNonExistentBehaviour, logger)
{ }
/// <summary>
/// Initialises a new instance of the <see cref="CommandAction"/> class to use an explicit target
/// </summary>
/// <param name="target">Target to find the method on</param>
/// <param name="methodName">Method name. the MyMethod in Buttom Command="{s:Action MyMethod}".</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 CommandAction(object target, string methodName, ActionUnavailableBehaviour targetNullBehaviour, ActionUnavailableBehaviour actionNonExistentBehaviour)
: base(target, methodName, targetNullBehaviour, actionNonExistentBehaviour, logger)
{ }
private string GuardName
{
get { return "Can" + this.MethodName; }

View File

@ -23,7 +23,7 @@ namespace Stylet.Xaml
private readonly Type eventHandlerType;
/// <summary>
/// Initialises a new instance of the <see cref="EventAction"/> class
/// Initialises a new instance of the <see cref="EventAction"/> classto use <see cref="View.ActionTargetProperty"/> to get the target
/// </summary>
/// <param name="subject">View whose View.ActionTarget we watch</param>
/// <param name="backupSubject">Backup subject to use if no ActionTarget could be retrieved from the subject</param>
@ -33,13 +33,32 @@ namespace Stylet.Xaml
/// <param name="actionNonExistentBehaviour">Behaviour for if the action doesn't exist on the View.ActionTarget</param>
public EventAction(DependencyObject subject, DependencyObject backupSubject, Type eventHandlerType, string methodName, ActionUnavailableBehaviour targetNullBehaviour, ActionUnavailableBehaviour actionNonExistentBehaviour)
: base(subject, backupSubject, methodName, targetNullBehaviour, actionNonExistentBehaviour, logger)
{
AssertBehaviours(targetNullBehaviour, actionNonExistentBehaviour);
this.eventHandlerType = eventHandlerType;
}
/// <summary>
/// Initialises a new instance of the <see cref="EventAction"/> class to use an explicit target
/// </summary>
/// <param name="target">Target to find the method on</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(object target, Type eventHandlerType, string methodName, ActionUnavailableBehaviour targetNullBehaviour, ActionUnavailableBehaviour actionNonExistentBehaviour)
: base(target, methodName, targetNullBehaviour, actionNonExistentBehaviour, logger)
{
AssertBehaviours(targetNullBehaviour, actionNonExistentBehaviour);
this.eventHandlerType = eventHandlerType;
}
private static void AssertBehaviours(ActionUnavailableBehaviour targetNullBehaviour, ActionUnavailableBehaviour actionNonExistentBehaviour)
{
if (targetNullBehaviour == ActionUnavailableBehaviour.Disable)
throw new ArgumentException("Setting NullTarget = Disable is unsupported when used on an Event");
if (actionNonExistentBehaviour == ActionUnavailableBehaviour.Disable)
throw new ArgumentException("Setting ActionNotFound = Disable is unsupported when used on an Event");
this.eventHandlerType = eventHandlerType;
}
/// <summary>

View File

@ -126,6 +126,19 @@ namespace StyletUnitTests
Assert.Throws<InvalidOperationException>(() => this.actionExtension.ProvideValue(this.serviceProvider.Object));
}
[Test]
public void OverridesTargetIfSetCommand()
{
var target = new object();
this.actionExtension.Target = target;
this.provideValueTarget.Setup(x => x.TargetProperty).Returns(Button.CommandProperty);
var cmd = (CommandAction)this.actionExtension.ProvideValue(this.serviceProvider.Object);
Assert.AreSame(target, cmd.Target);
}
// Can't really test Target on EventAction. Oh well.
}
}

View File

@ -24,8 +24,11 @@ namespace StyletUnitTests
get { return this._canDoSomethingWithGuard; }
set { SetAndNotify(ref this._canDoSomethingWithGuard, value); }
}
public bool DoSomethingWithGuardCalled;
public void DoSomethingWithGuard()
{
this.DoSomethingWithGuardCalled = true;
}
public object DoSomethingArgument;
@ -334,5 +337,18 @@ namespace StyletUnitTests
cmd.Execute(null);
Assert.True(StaticTarget.DidSomething);
}
[Test]
public void UsesExplicitTarget()
{
var cmd = new CommandAction(this.target, "DoSomethingWithGuard", ActionUnavailableBehaviour.Throw, ActionUnavailableBehaviour.Throw);
Assert.False(cmd.CanExecute(null));
this.target.CanDoSomethingWithGuard = true;
Assert.True(cmd.CanExecute(null));
cmd.Execute(null);
Assert.True(this.target.DoSomethingWithGuardCalled);
}
}
}

View File

@ -294,5 +294,13 @@ namespace StyletUnitTests
cmd.GetDelegate().DynamicInvoke(null, null);
Assert.True(StaticTarget.DidSomething);
}
[Test]
public void UsesExplicitTarget()
{
var cmd = new EventAction(this.target, this.eventInfo.EventHandlerType, "DoSomething", ActionUnavailableBehaviour.Throw, ActionUnavailableBehaviour.Throw);
cmd.GetDelegate().DynamicInvoke(null, null);
Assert.True(this.target.DoSomethingCalled);
}
}
}

View File

@ -1,7 +1,7 @@
<Project Sdk="MSBuild.Sdk.Extras/3.0.23">
<PropertyGroup>
<TargetFrameworks>net5.0-windows</TargetFrameworks>
<TargetFrameworks>net472;net5.0-windows</TargetFrameworks>
<UseWpf>true</UseWpf>
<IsPackable>false</IsPackable>