diff --git a/Stylet/Xaml/ActionBase.cs b/Stylet/Xaml/ActionBase.cs
index 783a4ee..66a491d 100644
--- a/Stylet/Xaml/ActionBase.cs
+++ b/Stylet/Xaml/ActionBase.cs
@@ -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
}));
///
- /// Initialises a new instance of the class
+ /// Initialises a new instance of the class to use to get the target
///
/// View to grab the View.ActionTarget from
/// Backup subject to use if no ActionTarget could be retrieved from the subject
@@ -65,12 +67,9 @@ namespace Stylet.Xaml
/// Behaviour for if the action doesn't exist on the View.ActionTarget
/// Logger to use
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
}
}
+ ///
+ /// Initialises a new instance of the class to use an explicit target
+ ///
+ /// Target to find the method on
+ /// Method name. the MyMethod in Buttom Command="{s:Action MyMethod}".
+ /// Behaviour for it the relevant View.ActionTarget is null
+ /// Behaviour for if the action doesn't exist on the View.ActionTarget
+ /// Logger to use
+ 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
/// Parameters to pass to the target method
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)
diff --git a/Stylet/Xaml/ActionExtension.cs b/Stylet/Xaml/ActionExtension.cs
index 16ac98e..def8cba 100644
--- a/Stylet/Xaml/ActionExtension.cs
+++ b/Stylet/Xaml/ActionExtension.cs
@@ -44,6 +44,11 @@ namespace Stylet.Xaml
[ConstructorArgument("method")]
public string Method { get; set; }
+ ///
+ /// Gets or sets a target to override that set with View.ActionTarget
+ ///
+ public object Target { get; set; }
+
///
/// Gets or sets the behaviour if the View.ActionTarget is nulil
///
@@ -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();
}
}
diff --git a/Stylet/Xaml/CommandAction.cs b/Stylet/Xaml/CommandAction.cs
index 177aecc..d12c485 100644
--- a/Stylet/Xaml/CommandAction.cs
+++ b/Stylet/Xaml/CommandAction.cs
@@ -26,7 +26,7 @@ namespace Stylet.Xaml
private Func guardPropertyGetter;
///
- /// Initialises a new instance of the class
+ /// Initialises a new instance of the class to use to get the target
///
/// View to grab the View.ActionTarget from
/// Backup subject to use if no ActionTarget could be retrieved from the subject
@@ -37,6 +37,17 @@ namespace Stylet.Xaml
: base(subject, backupSubject, methodName, targetNullBehaviour, actionNonExistentBehaviour, logger)
{ }
+ ///
+ /// Initialises a new instance of the class to use an explicit target
+ ///
+ /// Target to find the method on
+ /// Method name. the MyMethod in Buttom Command="{s:Action MyMethod}".
+ /// Behaviour for it the relevant View.ActionTarget is null
+ /// Behaviour for if the action doesn't exist on the View.ActionTarget
+ public CommandAction(object target, string methodName, ActionUnavailableBehaviour targetNullBehaviour, ActionUnavailableBehaviour actionNonExistentBehaviour)
+ : base(target, methodName, targetNullBehaviour, actionNonExistentBehaviour, logger)
+ { }
+
private string GuardName
{
get { return "Can" + this.MethodName; }
diff --git a/Stylet/Xaml/EventAction.cs b/Stylet/Xaml/EventAction.cs
index 367c2f3..5710446 100644
--- a/Stylet/Xaml/EventAction.cs
+++ b/Stylet/Xaml/EventAction.cs
@@ -23,7 +23,7 @@ namespace Stylet.Xaml
private readonly Type eventHandlerType;
///
- /// Initialises a new instance of the class
+ /// Initialises a new instance of the classto use to get the target
///
/// View whose View.ActionTarget we watch
/// Backup subject to use if no ActionTarget could be retrieved from the subject
@@ -33,13 +33,32 @@ namespace Stylet.Xaml
/// Behaviour for if the action doesn't exist on the View.ActionTarget
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;
+ }
+
+ ///
+ /// Initialises a new instance of the class to use an explicit target
+ ///
+ /// Target to find the method on
+ /// Type of event handler we're returning a delegate for
+ /// The MyMethod in {s:Action MyMethod}, this is what we call when the event's fired
+ /// Behaviour for it the relevant View.ActionTarget is null
+ /// Behaviour for if the action doesn't exist on the View.ActionTarget
+ 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;
}
///
diff --git a/StyletUnitTests/ActionExtensionTests.cs b/StyletUnitTests/ActionExtensionTests.cs
index 1b3f174..82d3260 100644
--- a/StyletUnitTests/ActionExtensionTests.cs
+++ b/StyletUnitTests/ActionExtensionTests.cs
@@ -126,6 +126,19 @@ namespace StyletUnitTests
Assert.Throws(() => 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.
}
}
diff --git a/StyletUnitTests/CommandActionTests.cs b/StyletUnitTests/CommandActionTests.cs
index 731ed36..28311e7 100644
--- a/StyletUnitTests/CommandActionTests.cs
+++ b/StyletUnitTests/CommandActionTests.cs
@@ -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);
+ }
}
}
diff --git a/StyletUnitTests/EventActionTests.cs b/StyletUnitTests/EventActionTests.cs
index 7c2c7c5..970ce11 100644
--- a/StyletUnitTests/EventActionTests.cs
+++ b/StyletUnitTests/EventActionTests.cs
@@ -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);
+ }
}
}
diff --git a/StyletUnitTests/StyletUnitTests.csproj b/StyletUnitTests/StyletUnitTests.csproj
index 5d5aa34..16e0bc9 100644
--- a/StyletUnitTests/StyletUnitTests.csproj
+++ b/StyletUnitTests/StyletUnitTests.csproj
@@ -1,7 +1,7 @@
- net5.0-windows
+ net472;net5.0-windows
true
false