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