MVVM将EventArgs作为命令参数传递

问题描述 投票:54回答:12

我正在使用Microsoft Expression Blend 4 我有一个浏览器..,

[XAML] ConnectionView“后面的空代码”

        <WebBrowser local:AttachedProperties.BrowserSource="{Binding Source}">
            <i:Interaction.Triggers>
                <i:EventTrigger>
                    <i:InvokeCommandAction Command="{Binding LoadedEvent}"/>
                </i:EventTrigger>
                <i:EventTrigger EventName="Navigated">
                    <i:InvokeCommandAction Command="{Binding NavigatedEvent}" CommandParameter="??????"/>
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </WebBrowser>  

[C#] AttachedProperties类

public static class AttachedProperties
    {
        public static readonly DependencyProperty BrowserSourceProperty = DependencyProperty . RegisterAttached ( "BrowserSource" , typeof ( string ) , typeof ( AttachedProperties ) , new UIPropertyMetadata ( null , BrowserSourcePropertyChanged ) );

        public static string GetBrowserSource ( DependencyObject _DependencyObject )
        {
            return ( string ) _DependencyObject . GetValue ( BrowserSourceProperty );
        }

        public static void SetBrowserSource ( DependencyObject _DependencyObject , string Value )
        {
            _DependencyObject . SetValue ( BrowserSourceProperty , Value );
        }

        public static void BrowserSourcePropertyChanged ( DependencyObject _DependencyObject , DependencyPropertyChangedEventArgs _DependencyPropertyChangedEventArgs )
        {
            WebBrowser _WebBrowser = _DependencyObject as WebBrowser;
            if ( _WebBrowser != null )
            {
                string URL = _DependencyPropertyChangedEventArgs . NewValue as string;
                _WebBrowser . Source = URL != null ? new Uri ( URL ) : null;
            }
        }
    }

[C#] ConnectionViewModel类

public class ConnectionViewModel : ViewModelBase
    {
            public string Source
            {
                get { return Get<string> ( "Source" ); }
                set { Set ( "Source" , value ); }
            }

            public void Execute_ExitCommand ( )
            {
                Application . Current . Shutdown ( );
            }

            public void Execute_LoadedEvent ( )
            {
                MessageBox . Show ( "___Execute_LoadedEvent___" );
                Source = ...... ;
            }

            public void Execute_NavigatedEvent ( )
            {
                MessageBox . Show ( "___Execute_NavigatedEvent___" );
            }
    }

[C#] ViewModelBase类Here

最后: 使用命令绑定工作正常,并显示MessageBoxes


我的问题 : 如何在导航事件发生时将NavigationEventArgs作为命令参数传递?

wpf browser expression-blend eventargs
12个回答
60
投票

它不容易支持。这里是an article,其中包含有关如何将EventArgs作为命令参数传递的说明。

你可能想要研究使用MVVMLight - 它直接在命令中支持EventArgs;你的情况看起来像这样:

 <i:Interaction.Triggers>
    <i:EventTrigger EventName="Navigated">
        <cmd:EventToCommand Command="{Binding NavigatedEvent}"
            PassEventArgsToCommand="True" />
    </i:EventTrigger>
 </i:Interaction.Triggers>

0
投票

这是@adabyron的答案版本,可以防止泄漏的EventArgs抽象。

首先,修改后的EventToCommandBehavior类(现在是一个通用的抽象类,并使用ReSharper代码清理格式化)。请注意新的GetCommandParameter虚拟方法及其默认实现:

public abstract class EventToCommandBehavior<TEventArgs> : Behavior<FrameworkElement>
    where TEventArgs : EventArgs
{
    public static readonly DependencyProperty EventProperty = DependencyProperty.Register("Event", typeof(string), typeof(EventToCommandBehavior<TEventArgs>), new PropertyMetadata(null, OnEventChanged));
    public static readonly DependencyProperty CommandProperty = DependencyProperty.Register("Command", typeof(ICommand), typeof(EventToCommandBehavior<TEventArgs>), new PropertyMetadata(null));
    public static readonly DependencyProperty PassArgumentsProperty = DependencyProperty.Register("PassArguments", typeof(bool), typeof(EventToCommandBehavior<TEventArgs>), new PropertyMetadata(false));
    private Delegate _handler;
    private EventInfo _oldEvent;

    public string Event
    {
        get { return (string)GetValue(EventProperty); }
        set { SetValue(EventProperty, value); }
    }

    public ICommand Command
    {
        get { return (ICommand)GetValue(CommandProperty); }
        set { SetValue(CommandProperty, value); }
    }

    public bool PassArguments
    {
        get { return (bool)GetValue(PassArgumentsProperty); }
        set { SetValue(PassArgumentsProperty, value); }
    }

    protected override void OnAttached()
    {
        AttachHandler(Event);
    }

    protected virtual object GetCommandParameter(TEventArgs e)
    {
        return e;
    }

    private void AttachHandler(string eventName)
    {
        _oldEvent?.RemoveEventHandler(AssociatedObject, _handler);

        if (string.IsNullOrEmpty(eventName))
        {
            return;
        }

        EventInfo eventInfo = AssociatedObject.GetType().GetEvent(eventName);

        if (eventInfo != null)
        {
            MethodInfo methodInfo = typeof(EventToCommandBehavior<TEventArgs>).GetMethod("ExecuteCommand", BindingFlags.Instance | BindingFlags.NonPublic);

            _handler = Delegate.CreateDelegate(eventInfo.EventHandlerType, this, methodInfo);
            eventInfo.AddEventHandler(AssociatedObject, _handler);
            _oldEvent = eventInfo;
        }
        else
        {
            throw new ArgumentException($"The event '{eventName}' was not found on type '{AssociatedObject.GetType().FullName}'.");
        }
    }

    private static void OnEventChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var behavior = (EventToCommandBehavior<TEventArgs>)d;

        if (behavior.AssociatedObject != null)
        {
            behavior.AttachHandler((string)e.NewValue);
        }
    }

    // ReSharper disable once UnusedMember.Local
    // ReSharper disable once UnusedParameter.Local
    private void ExecuteCommand(object sender, TEventArgs e)
    {
        object parameter = PassArguments ? GetCommandParameter(e) : null;

        if (Command?.CanExecute(parameter) == true)
        {
            Command.Execute(parameter);
        }
    }
}

接下来,隐藏DragCompletedEventArgs的示例派生类。有些人担心将EventArgs抽象泄漏到他们的视图模型组件中。为了防止这种情况,我创建了一个代表我们关心的值的接口。界面可以在视图模型程序集中与UI程序集中的私有实现一起使用:

// UI assembly
public class DragCompletedBehavior : EventToCommandBehavior<DragCompletedEventArgs>
{
    protected override object GetCommandParameter(DragCompletedEventArgs e)
    {
        return new DragCompletedArgs(e);
    }

    private class DragCompletedArgs : IDragCompletedArgs
    {
        public DragCompletedArgs(DragCompletedEventArgs e)
        {
            Canceled = e.Canceled;
            HorizontalChange = e.HorizontalChange;
            VerticalChange = e.VerticalChange;
        }

        public bool Canceled { get; }
        public double HorizontalChange { get; }
        public double VerticalChange { get; }
    }
}

// View model assembly
public interface IDragCompletedArgs
{
    bool Canceled { get; }
    double HorizontalChange { get; }
    double VerticalChange { get; }
}

将命令参数强制转换为IDragCompletedArgs,类似于@adabyron的回答。


0
投票

作为@Mike Fuchs回答的改编,这是一个更小的解决方案。我正在使用Fody.AutoDependencyPropertyMarker减少一些锅炉板。

班级

public class EventCommand : TriggerAction<DependencyObject>
{
    [AutoDependencyProperty]
    public ICommand Command { get; set; }

    protected override void Invoke(object parameter)
    {
        if (Command != null)
        {
            if (Command.CanExecute(parameter))
            {
                Command.Execute(parameter);
            }
        }
    }
}

EventArgs

public class VisibleBoundsArgs : EventArgs
{
    public Rect VisibleVounds { get; }

    public VisibleBoundsArgs(Rect visibleBounds)
    {
        VisibleVounds = visibleBounds;
    }
}

XAML

<local:ZoomableImage>
   <i:Interaction.Triggers>
      <i:EventTrigger EventName="VisibleBoundsChanged" >
         <local:EventCommand Command="{Binding VisibleBoundsChanged}" />
      </i:EventTrigger>
   </i:Interaction.Triggers>
</local:ZoomableImage>

ViewModel

public ICommand VisibleBoundsChanged => _visibleBoundsChanged ??
                                        (_visibleBoundsChanged = new RelayCommand(obj => SetVisibleBounds(((VisibleBoundsArgs)obj).VisibleVounds)));

0
投票

如果没有设置InvokeCommandAction,Prism的CommandParameter将默认通过事件args。

https://docs.microsoft.com/en-us/previous-versions/msp-n-p/gg405494(v=pandp.40)#passing-eventargs-parameters-to-the-command

这是一个例子。注意使用prism:InvokeCommandAction而不是i:InvokeCommandAction

<i:Interaction.Triggers>
    <i:EventTrigger EventName="Sorting">
        <prism:InvokeCommandAction Command="{Binding SortingCommand}"/>
    </i:EventTrigger>
</i:Interaction.Triggers>

39
投票

我尝试将我的依赖项保持在最低限度,所以我自己实现了这个,而不是使用MVVMLight的EventToCommand。到目前为止对我有用,但欢迎提供反馈。

XAML:

<i:Interaction.Behaviors>
    <beh:EventToCommandBehavior Command="{Binding DropCommand}" Event="Drop" PassArguments="True" />
</i:Interaction.Behaviors>

视图模型:

public ActionCommand<DragEventArgs> DropCommand { get; private set; }

this.DropCommand = new ActionCommand<DragEventArgs>(OnDrop);

private void OnDrop(DragEventArgs e)
{
    // ...
}

EventToCommandBehavior:

/// <summary>
/// Behavior that will connect an UI event to a viewmodel Command,
/// allowing the event arguments to be passed as the CommandParameter.
/// </summary>
public class EventToCommandBehavior : Behavior<FrameworkElement>
{
    private Delegate _handler;
    private EventInfo _oldEvent;

    // Event
    public string Event { get { return (string)GetValue(EventProperty); } set { SetValue(EventProperty, value); } }
    public static readonly DependencyProperty EventProperty = DependencyProperty.Register("Event", typeof(string), typeof(EventToCommandBehavior), new PropertyMetadata(null, OnEventChanged));

    // Command
    public ICommand Command { get { return (ICommand)GetValue(CommandProperty); } set { SetValue(CommandProperty, value); } }
    public static readonly DependencyProperty CommandProperty = DependencyProperty.Register("Command", typeof(ICommand), typeof(EventToCommandBehavior), new PropertyMetadata(null));

    // PassArguments (default: false)
    public bool PassArguments { get { return (bool)GetValue(PassArgumentsProperty); } set { SetValue(PassArgumentsProperty, value); } }
    public static readonly DependencyProperty PassArgumentsProperty = DependencyProperty.Register("PassArguments", typeof(bool), typeof(EventToCommandBehavior), new PropertyMetadata(false));


    private static void OnEventChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var beh = (EventToCommandBehavior)d;

        if (beh.AssociatedObject != null) // is not yet attached at initial load
            beh.AttachHandler((string)e.NewValue);
    }

    protected override void OnAttached()
    {
        AttachHandler(this.Event); // initial set
    }

    /// <summary>
    /// Attaches the handler to the event
    /// </summary>
    private void AttachHandler(string eventName)
    {
        // detach old event
        if (_oldEvent != null)
            _oldEvent.RemoveEventHandler(this.AssociatedObject, _handler);

        // attach new event
        if (!string.IsNullOrEmpty(eventName))
        {
            EventInfo ei = this.AssociatedObject.GetType().GetEvent(eventName);
            if (ei != null)
            {
                MethodInfo mi = this.GetType().GetMethod("ExecuteCommand", BindingFlags.Instance | BindingFlags.NonPublic);
                _handler = Delegate.CreateDelegate(ei.EventHandlerType, this, mi);
                ei.AddEventHandler(this.AssociatedObject, _handler);
                _oldEvent = ei; // store to detach in case the Event property changes
            }
            else
                throw new ArgumentException(string.Format("The event '{0}' was not found on type '{1}'", eventName, this.AssociatedObject.GetType().Name));
        }
    }

    /// <summary>
    /// Executes the Command
    /// </summary>
    private void ExecuteCommand(object sender, EventArgs e)
    {
        object parameter = this.PassArguments ? e : null;
        if (this.Command != null)
        {
            if (this.Command.CanExecute(parameter))
                this.Command.Execute(parameter);
        }
    }
}

ActionCommand:

public class ActionCommand<T> : ICommand
{
    public event EventHandler CanExecuteChanged;
    private Action<T> _action;

    public ActionCommand(Action<T> action)
    {
        _action = action;
    }

    public bool CanExecute(object parameter) { return true; }

    public void Execute(object parameter)
    {
        if (_action != null)
        {
            var castParameter = (T)Convert.ChangeType(parameter, typeof(T));
            _action(castParameter);
        }
    }
}

19
投票

我总是回到这里寻找答案,所以我想做一个简单的简单的去。

有多种方法可以做到这一点:

1.使用WPF工具。最容易的。

添加命名空间:

  • System.Windows.Interactivitiy
  • Microsoft.Expression.Interactions

XAML:

使用EventName调用您想要的事件,然后在Method中指定您的MethodName名称。

<Window>
    xmlns:wi="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
    xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions">

    <wi:Interaction.Triggers>
        <wi:EventTrigger EventName="SelectionChanged">
            <ei:CallMethodAction
                TargetObject="{Binding}"
                MethodName="ShowCustomer"/>
        </wi:EventTrigger>
    </wi:Interaction.Triggers>
</Window>

码:

public void ShowCustomer()
{
    // Do something.
}

2.使用MVVMLight。最难的。

安装GalaSoft NuGet包。

获取命名空间:

  • System.Windows.Interactivity
  • GalaSoft.MvvmLight.Platform

XAML:

使用EventName调用您想要的事件,然后在绑定中指定您的Command名称。如果要传递方法的参数,请将PassEventArgsToCommand标记为true。

<Window>
    xmlns:wi="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
    xmlns:cmd="http://www.galasoft.ch/mvvmlight">

    <wi:Interaction.Triggers>
       <wi:EventTrigger EventName="Navigated">
           <cmd:EventToCommand Command="{Binding CommandNameHere}"
               PassEventArgsToCommand="True" />
       </wi:EventTrigger>
    </wi:Interaction.Triggers>
</Window>

代码实现代表:Source

你必须得到Prism MVVM NuGet包。

using Microsoft.Practices.Prism.Commands;

// With params.
public DelegateCommand<string> CommandOne { get; set; }
// Without params.
public DelegateCommand CommandTwo { get; set; }

public MainWindow()
{
    InitializeComponent();

    // Must initialize the DelegateCommands here.
    CommandOne = new DelegateCommand<string>(executeCommandOne);
    CommandTwo = new DelegateCommand(executeCommandTwo);
}

private void executeCommandOne(string param)
{
    // Do something here.
}

private void executeCommandTwo()
{
    // Do something here.
}

没有DelegateCommand的代码:Source

using GalaSoft.MvvmLight.CommandWpf

public MainWindow()
{
    InitializeComponent();

    CommandOne = new RelayCommand<string>(executeCommandOne);
    CommandTwo = new RelayCommand(executeCommandTwo);
}

public RelayCommand<string> CommandOne { get; set; }

public RelayCommand CommandTwo { get; set; }

private void executeCommandOne(string param)
{
    // Do something here.
}

private void executeCommandTwo()
{
    // Do something here.
}

3.使用Telerik EventToCommandBehavior。这是一个选择。

你必须下载它的NuGet Package

XAML

<i:Interaction.Behaviors>
    <telerek:EventToCommandBehavior
         Command="{Binding DropCommand}"
         Event="Drop"
         PassArguments="True" />
</i:Interaction.Behaviors>

码:

public ActionCommand<DragEventArgs> DropCommand { get; private set; }

this.DropCommand = new ActionCommand<DragEventArgs>(OnDrop);

private void OnDrop(DragEventArgs e)
{
    // Do Something
}

12
投票

我知道这是一个相当古老的问题,但我今天遇到了同样的问题并且对引用所有MVVMLight并不太感兴趣,所以我可以使用带有事件args的事件触发器。我过去使用过MVVMLight,它是一个很棒的框架,但我不想再将它用于我的项目了。

我为解决这个问题所做的是创建一个ULTRA最小的,极其适应性的自定义触发器动作,它允许我绑定到命令并提供一个事件args转换器来将args传递给命令的CanExecute和Execute函数。您不希望逐字传递事件args,因为这会导致视图层类型被发送到视图模型层(在MVVM中永远不会发生)。

这是我提出的EventCommandExecuter类:

public class EventCommandExecuter : TriggerAction<DependencyObject>
{
    #region Constructors

    public EventCommandExecuter()
        : this(CultureInfo.CurrentCulture)
    {
    }

    public EventCommandExecuter(CultureInfo culture)
    {
        Culture = culture;
    }

    #endregion

    #region Properties

    #region Command

    public ICommand Command
    {
        get { return (ICommand)GetValue(CommandProperty); }
        set { SetValue(CommandProperty, value); }
    }

    public static readonly DependencyProperty CommandProperty =
        DependencyProperty.Register("Command", typeof(ICommand), typeof(EventCommandExecuter), new PropertyMetadata(null));

    #endregion

    #region EventArgsConverterParameter

    public object EventArgsConverterParameter
    {
        get { return (object)GetValue(EventArgsConverterParameterProperty); }
        set { SetValue(EventArgsConverterParameterProperty, value); }
    }

    public static readonly DependencyProperty EventArgsConverterParameterProperty =
        DependencyProperty.Register("EventArgsConverterParameter", typeof(object), typeof(EventCommandExecuter), new PropertyMetadata(null));

    #endregion

    public IValueConverter EventArgsConverter { get; set; }

    public CultureInfo Culture { get; set; }

    #endregion

    protected override void Invoke(object parameter)
    {
        var cmd = Command;

        if (cmd != null)
        {
            var param = parameter;

            if (EventArgsConverter != null)
            {
                param = EventArgsConverter.Convert(parameter, typeof(object), EventArgsConverterParameter, CultureInfo.InvariantCulture);
            }

            if (cmd.CanExecute(param))
            {
                cmd.Execute(param);
            }
        }
    }
}

此类有两个依赖项属性,一个允许绑定到视图模型的命令,另一个允许您在事件args转换期间绑定事件源(如果需要)。如果需要,您还可以提供文化设置(它们默认为当前的UI文化)。

此类允许您调整事件参数,以便视图模型的命令逻辑可以使用它们。但是,如果您只想逐字传递事件args,则不要指定事件args转换器。

XAML中此触发器操作的最简单用法如下:

<i:Interaction.Triggers>
    <i:EventTrigger EventName="NameChanged">
        <cmd:EventCommandExecuter Command="{Binding Path=Update, Mode=OneTime}" EventArgsConverter="{x:Static c:NameChangedArgsToStringConverter.Default}"/>
    </i:EventTrigger>
</i:Interaction.Triggers>

如果您需要访问事件源,则可以绑定到事件的所有者

<i:Interaction.Triggers>
    <i:EventTrigger EventName="NameChanged">
        <cmd:EventCommandExecuter 
            Command="{Binding Path=Update, Mode=OneTime}" 
            EventArgsConverter="{x:Static c:NameChangedArgsToStringConverter.Default}"
            EventArgsConverterParameter="{Binding ElementName=SomeEventSource, Mode=OneTime}"/>
    </i:EventTrigger>
</i:Interaction.Triggers>

(这假设您附加触发器的XAML节点已被分配x:Name="SomeEventSource"

此XAML依赖于导入一些必需的命名空间

xmlns:cmd="clr-namespace:MyProject.WPF.Commands"
xmlns:c="clr-namespace:MyProject.WPF.Converters"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

并创建一个IValueConverter(在本例中称为NameChangedArgsToStringConverter)来处理实际的转换逻辑。对于基本转换器,我通常会创建一个默认的static readonly转换器实例,然后我可以直接在XAML中引用,如上所述。

此解决方案的好处是,您实际上只需要向任何项目添加单个类以使用交互框架,就像使用InvokeCommandAction一样。添加单个类(大约75行)应该比整个库更优选以实现相同的结果。

注意

这有点类似于@adabyron的答案,但它使用事件触发器而不是行为。此解决方案还提供了事件args转换功能,而不是@adabyron的解决方案也不能这样做。我真的没有任何理由为什么我更喜欢触发行为,只是个人选择。 IMO要么战略是一个合理的选择。


11
投票

对于刚刚发现这篇文章的人,您应该知道在较新的版本中(由于官方文档在这个主题上很渺茫,不确定确切版本)InvokeCommandAction的默认行为,如果没有指定CommandParameter,则传递的是args它作为CommandParameter附加到它的事件。因此,原件海报的XAML可以简单地写成:

<i:Interaction.Triggers>
  <i:EventTrigger EventName="Navigated">
    <i:InvokeCommandAction Command="{Binding NavigatedEvent}"/>
  </i:EventTrigger>
</i:Interaction.Triggers>

然后在您的命令中,您可以接受NavigationEventArgs类型的参数(或任何适合的事件args类型),它将自动提供。


5
投票

添加到joshb已经说明的内容 - 这对我来说很好。确保添加对Microsoft.Expression.Interactions.dll和System.Windows.Interactivity.dll的引用,并在您的xaml中执行:

    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"

我最终根据自己的需要使用了这样的东西。这表明您还可以传递自定义参数:

<i:Interaction.Triggers>
            <i:EventTrigger EventName="SelectionChanged">

                <i:InvokeCommandAction Command="{Binding Path=DataContext.RowSelectedItem, RelativeSource={RelativeSource AncestorType={x:Type Window}}}" 
                                       CommandParameter="{Binding Path=SelectedItem, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=DataGrid}}" />
            </i:EventTrigger>
</i:Interaction.Triggers>

3
投票

我不认为你可以用InvokeCommandAction轻松做到这一点 - 我会看一下MVVMLight或类似的EventToCommand


1
投票

使用Blend for Visual Studio 2013中的行为和操作,您可以使用InvokeCommandAction。我尝试使用Drop事件,尽管在XAML中没有指定CommandParameter,但令我惊讶的是,Execute Action参数包含DragEventArgs。我认为这会发生在其他事件上,但没有测试过它们。


0
投票

我所做的是使用InvokeCommandAction将控件加载的事件绑定到视图模型中的命令,给出控件ax:Xaml中的Name并传递为CommandParameter,然后在所述加载的命令钩子视图模型处理程序中直到我需要的事件得到事件args。

© www.soinside.com 2019 - 2024. All rights reserved.