mirror of https://github.com/AMT-Cheif/Stylet.git
parent
6e2bc6e36c
commit
e195d29c01
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue