是否可以使用带有参数的 CallMethodAction 交互?
Is it possible to use the CallMethodAction interaction with an argument?
鉴于以下情况:
<Storyboard x:Key="Foo" AutoReverse="True" RepeatBehavior="3x">
<Storyboard.Children/>
</Storyboard>
<DoubleAnimationUsingKeyFrames x:Key = "Bar"/>
<ei:DataTrigger
Binding="{
Binding SomeVar,
ElementName=SomeElement,
FallbackValue=False,
Mode=OneWay}"
Value="True">
<ei:CallMethodAction
TargetObject="{
Binding Mode=OneWay,
Path=Children,
Source={StaticResource Foo}}"
MethodName="Add"/>
</ei:DataTrigger>
有什么方法可以将 Bar
作为参数传递给方法调用 Children.Add
?
您可以尝试使用来自 Interactivity
的 InvokeCommandAction
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
<ei:DataTrigger
Binding="{
Binding SomeVar,
ElementName=SomeElement,
FallbackValue=False,
Mode=OneWay}"
Value="True">
<i:InvokeCommandAction Command="{Binding SomeCommand, Source={StaticResource SomeViewModel}}" CommandParameter="Bar"/>
</ei:DataTrigger>
CallMethodAction
只能用于调用不带参数的方法或具有两个参数的方法,其中第一个参数是对象类型,第二个可以分配给 EventArgs 类型的变量。
鉴于此,您将无法使用 CallMethodAction
做您想做的事。但是,您可以制作自己的触发操作,该操作将调用您的方法并传入您指定的值。我只对此做了一些简单的测试,但它应该非常接近您的需要。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Interactivity;
namespace LocalActions
{
public class CallUnaryMethodAction : TargetedTriggerAction<DependencyObject>
{
// The name of the method to invoke.
public static readonly DependencyProperty MethodNameProperty =
DependencyProperty.Register( "MethodName",
typeof( string ),
typeof( CallUnaryMethodAction ),
new PropertyMetadata( OnNeedsMethodInfoUpdated ) );
public string MethodName
{
get { return (string)GetValue( MethodNameProperty ); }
set { SetValue( MethodNameProperty, value ); }
}
// Flag that lets us determine if we want to search non-public methods in our target object.
public static readonly DependencyProperty AllowNonPublicMethodsProperty =
DependencyProperty.Register( "AllowNonPublicMethods",
typeof( bool ),
typeof( CallUnaryMethodAction ),
new PropertyMetadata( OnNeedsMethodInfoUpdated ) );
public bool AllowNonPublicMethods
{
get { return (bool)GetValue( AllowNonPublicMethodsProperty ); }
set { SetValue( AllowNonPublicMethodsProperty, value ); }
}
// Parameter we want to pass to our method. If this has not been set, then the value passed
// to the trigger action's Invoke method will be used instead.
public static readonly DependencyProperty ParameterProperty =
DependencyProperty.Register( "Parameter",
typeof( object ),
typeof( CallUnaryMethodAction ) );
public object Parameter
{
get { return GetValue( ParameterProperty ); }
set { SetValue( ParameterProperty, value ); }
}
private static void OnNeedsMethodInfoUpdated( DependencyObject d, DependencyPropertyChangedEventArgs e )
{
var action = d as CallUnaryMethodAction;
if( action != null )
action.UpdateMethodInfo();
}
protected override void OnAttached()
{
UpdateMethodInfo();
}
protected override void OnTargetChanged( DependencyObject oldTarget, DependencyObject newTarget )
{
UpdateMethodInfo();
}
protected override void Invoke( object parameter )
{
object target = this.TargetObject ?? this.AssociatedObject;
if( target == null )
return;
// Determine what we are going to pass to our method.
object methodParam = ReadLocalValue( ParameterProperty ) == DependencyProperty.UnsetValue ?
parameter : this.Parameter;
// Pick the best method to call given the parameter we want to pass.
Method methodToCall = m_methods.FirstOrDefault( method =>
(methodParam != null) && method.ParameterInfo.ParameterType.IsAssignableFrom( methodParam.GetType() ) );
if( methodToCall == null )
throw new InvalidOperationException( "No suitable method found." );
methodToCall.MethodInfo.Invoke( target, new object[] { methodParam } );
}
private void UpdateMethodInfo()
{
m_methods.Clear();
object target = this.TargetObject ?? this.AssociatedObject;
if( target == null || string.IsNullOrEmpty( this.MethodName ) )
return;
// Find all unary methods with the given name.
BindingFlags flags = BindingFlags.Public | BindingFlags.Instance;
if( this.AllowNonPublicMethods )
flags |= BindingFlags.NonPublic;
foreach( MethodInfo methodInfo in target.GetType().GetMethods( flags ) )
{
if( methodInfo.Name == this.MethodName )
{
ParameterInfo[] parameters = methodInfo.GetParameters();
if( parameters.Length == 1 )
m_methods.Add( new Method( methodInfo, parameters[0] ) );
}
}
// Order the methods so that methods with most derived parameters are ordered first.
// This will help us pick the most appropriate method in the call to Invoke.
m_methods = m_methods.OrderByDescending<Method, int>( method =>
{
int rank = 0;
for( Type type = method.ParameterInfo.ParameterType; type != typeof( object ); type = type.BaseType )
++rank;
return rank;
} ).ToList<Method>();
}
private List<Method> m_methods = new List<Method>();
// Holds info on the list of possible methods we can call.
private class Method
{
public Method( MethodInfo methodInfo, ParameterInfo paramInfo )
{
this.MethodInfo = methodInfo;
this.ParameterInfo = paramInfo;
}
public MethodInfo MethodInfo { get; private set; }
public ParameterInfo ParameterInfo { get; private set; }
}
}
}
然后您可以在 XAML 中使用它,就像通常在 CallMethodAction
中一样。您只需要引入适当的 XAML 命名空间。
...
xmlns:local="clr-namespace:LocalActions"
...
<ei:DataTrigger
Binding="{
Binding SomeVar,
ElementName=SomeElement,
FallbackValue=False,
Mode=OneWay}"
Value="True">
<local:CallUnaryMethodAction
TargetObject="{
Binding Mode=OneWay,
Path=Children,
Source={StaticResource Foo}}"
MethodName="Add"
Parameter="{StaticResource Bar}"/>
</ei:DataTrigger>
这是假设您的 DoubleAnimationUsingKeyFrames
确实是一种资源(我根据您对 x:Key
的使用进行猜测)。如果这不合适,那么您需要根据需要调整绑定。
鉴于以下情况:
<Storyboard x:Key="Foo" AutoReverse="True" RepeatBehavior="3x">
<Storyboard.Children/>
</Storyboard>
<DoubleAnimationUsingKeyFrames x:Key = "Bar"/>
<ei:DataTrigger
Binding="{
Binding SomeVar,
ElementName=SomeElement,
FallbackValue=False,
Mode=OneWay}"
Value="True">
<ei:CallMethodAction
TargetObject="{
Binding Mode=OneWay,
Path=Children,
Source={StaticResource Foo}}"
MethodName="Add"/>
</ei:DataTrigger>
有什么方法可以将 Bar
作为参数传递给方法调用 Children.Add
?
您可以尝试使用来自 Interactivity
的 InvokeCommandActionxmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
<ei:DataTrigger
Binding="{
Binding SomeVar,
ElementName=SomeElement,
FallbackValue=False,
Mode=OneWay}"
Value="True">
<i:InvokeCommandAction Command="{Binding SomeCommand, Source={StaticResource SomeViewModel}}" CommandParameter="Bar"/>
</ei:DataTrigger>
CallMethodAction
只能用于调用不带参数的方法或具有两个参数的方法,其中第一个参数是对象类型,第二个可以分配给 EventArgs 类型的变量。
鉴于此,您将无法使用 CallMethodAction
做您想做的事。但是,您可以制作自己的触发操作,该操作将调用您的方法并传入您指定的值。我只对此做了一些简单的测试,但它应该非常接近您的需要。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Interactivity;
namespace LocalActions
{
public class CallUnaryMethodAction : TargetedTriggerAction<DependencyObject>
{
// The name of the method to invoke.
public static readonly DependencyProperty MethodNameProperty =
DependencyProperty.Register( "MethodName",
typeof( string ),
typeof( CallUnaryMethodAction ),
new PropertyMetadata( OnNeedsMethodInfoUpdated ) );
public string MethodName
{
get { return (string)GetValue( MethodNameProperty ); }
set { SetValue( MethodNameProperty, value ); }
}
// Flag that lets us determine if we want to search non-public methods in our target object.
public static readonly DependencyProperty AllowNonPublicMethodsProperty =
DependencyProperty.Register( "AllowNonPublicMethods",
typeof( bool ),
typeof( CallUnaryMethodAction ),
new PropertyMetadata( OnNeedsMethodInfoUpdated ) );
public bool AllowNonPublicMethods
{
get { return (bool)GetValue( AllowNonPublicMethodsProperty ); }
set { SetValue( AllowNonPublicMethodsProperty, value ); }
}
// Parameter we want to pass to our method. If this has not been set, then the value passed
// to the trigger action's Invoke method will be used instead.
public static readonly DependencyProperty ParameterProperty =
DependencyProperty.Register( "Parameter",
typeof( object ),
typeof( CallUnaryMethodAction ) );
public object Parameter
{
get { return GetValue( ParameterProperty ); }
set { SetValue( ParameterProperty, value ); }
}
private static void OnNeedsMethodInfoUpdated( DependencyObject d, DependencyPropertyChangedEventArgs e )
{
var action = d as CallUnaryMethodAction;
if( action != null )
action.UpdateMethodInfo();
}
protected override void OnAttached()
{
UpdateMethodInfo();
}
protected override void OnTargetChanged( DependencyObject oldTarget, DependencyObject newTarget )
{
UpdateMethodInfo();
}
protected override void Invoke( object parameter )
{
object target = this.TargetObject ?? this.AssociatedObject;
if( target == null )
return;
// Determine what we are going to pass to our method.
object methodParam = ReadLocalValue( ParameterProperty ) == DependencyProperty.UnsetValue ?
parameter : this.Parameter;
// Pick the best method to call given the parameter we want to pass.
Method methodToCall = m_methods.FirstOrDefault( method =>
(methodParam != null) && method.ParameterInfo.ParameterType.IsAssignableFrom( methodParam.GetType() ) );
if( methodToCall == null )
throw new InvalidOperationException( "No suitable method found." );
methodToCall.MethodInfo.Invoke( target, new object[] { methodParam } );
}
private void UpdateMethodInfo()
{
m_methods.Clear();
object target = this.TargetObject ?? this.AssociatedObject;
if( target == null || string.IsNullOrEmpty( this.MethodName ) )
return;
// Find all unary methods with the given name.
BindingFlags flags = BindingFlags.Public | BindingFlags.Instance;
if( this.AllowNonPublicMethods )
flags |= BindingFlags.NonPublic;
foreach( MethodInfo methodInfo in target.GetType().GetMethods( flags ) )
{
if( methodInfo.Name == this.MethodName )
{
ParameterInfo[] parameters = methodInfo.GetParameters();
if( parameters.Length == 1 )
m_methods.Add( new Method( methodInfo, parameters[0] ) );
}
}
// Order the methods so that methods with most derived parameters are ordered first.
// This will help us pick the most appropriate method in the call to Invoke.
m_methods = m_methods.OrderByDescending<Method, int>( method =>
{
int rank = 0;
for( Type type = method.ParameterInfo.ParameterType; type != typeof( object ); type = type.BaseType )
++rank;
return rank;
} ).ToList<Method>();
}
private List<Method> m_methods = new List<Method>();
// Holds info on the list of possible methods we can call.
private class Method
{
public Method( MethodInfo methodInfo, ParameterInfo paramInfo )
{
this.MethodInfo = methodInfo;
this.ParameterInfo = paramInfo;
}
public MethodInfo MethodInfo { get; private set; }
public ParameterInfo ParameterInfo { get; private set; }
}
}
}
然后您可以在 XAML 中使用它,就像通常在 CallMethodAction
中一样。您只需要引入适当的 XAML 命名空间。
...
xmlns:local="clr-namespace:LocalActions"
...
<ei:DataTrigger
Binding="{
Binding SomeVar,
ElementName=SomeElement,
FallbackValue=False,
Mode=OneWay}"
Value="True">
<local:CallUnaryMethodAction
TargetObject="{
Binding Mode=OneWay,
Path=Children,
Source={StaticResource Foo}}"
MethodName="Add"
Parameter="{StaticResource Bar}"/>
</ei:DataTrigger>
这是假设您的 DoubleAnimationUsingKeyFrames
确实是一种资源(我根据您对 x:Key
的使用进行猜测)。如果这不合适,那么您需要根据需要调整绑定。