MVVM模式下如何将EventArgs作为命令参数传递

75
我正在使用Microsoft Expression Blend 4, 我有一个浏览器.., [XAML] ConnectionView “Empty Code Behind”
        <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 class

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类 这里

最后:
将命令与绑定配合使用,弹出消息框工作正常。


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

13个回答

79

这并不容易。这里有一篇文章,介绍了如何将EventArgs作为命令参数传递。

你可能想要考虑使用MVVMLight - 它直接支持在命令中使用EventArgs;你的情况会类似于这样:

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

2
那么就没有直接的方法吗?我讨厌使用总是有 bug 的模板...等等,所以我喜欢从头开始编码。 - Ahmed Ghoneim
63
这是一个相当有趣的说法。 - H.B.
4
确实,只需使用MVVM Light即可。它更简单,而且你只需要使用RelayCommand和EventToCommand类。 - Mike Post
1
Silverlight/WPF一般来说并不容易,是吧? - Trident D'Gao
3
“cmd”命名空间是什么? - Stepagrus
1
cmd 命名空间如下:xmlns:cmd="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Platform" - S_Mindcore

51

我尽量保持对依赖项的使用最小化,因此我自己实现了这个功能,而不是使用MVVMLight的EventToCommand。目前对我来说效果很好,但欢迎提供反馈。

Xaml:

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

ViewModel:

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);
        }
    }
}

2
一个可以接受的模板代码水平,以避免采用另一个框架。这对我也很有效,干杯! - jeebs
1
有趣的解决方案。我唯一对此有问题的是它将UI相关的代码放在ViewModel中。DragEventArgs来自System.Windows.Forms,而ActionCommand也可以说是与UI相关的。我倾向于将我的ViewModels极度分离到它们自己的程序集中,没有任何UI相关的引用。这可以防止我意外地越过“界线”。这是个人偏好,每个开发者都可以自行决定要遵循MVVM模式的严格程度。 - Matthew
Matthew,命令在MVVM模式中是完全有效的,并且属于ViewModel。可以争论EventArgs不属于其中,但如果您不喜欢它,您可能希望在问题上发表评论,而不是在解决方案上。顺便说一下,DragEventArgs在WPF的System.Windows命名空间中。 - Mike Fuchs
@Matthew 我认为我们可以创建一个单独的项目,并在其中添加EventToCommandBehavior和ActionCommand类。这样,您可以在需要时使用System.Windows,并避免引用托管行为的System.Windows.Interactivity命名空间。 - Adarsha
@adabyron,你曾经尝试过在多个事件中使用这个吗?我能否在XAML中放置多个此行为的实例? - Walter Williams
@WalterWilliams 是的,我已经完成了,没有任何问题。 - Mike Fuchs

24

我一直都来到这里寻找答案,因此我想制作一个简短而简单的回答。

有多种方法可以实现这一点:

1. 使用 WPF 工具。最简单的。

添加命名空间:

  • System.Windows.Interactivity
  • Microsoft.Expression.Interactions

XAML:

使用 EventName 来调用您想要的事件,然后在 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 包。

enter image description here

获取以下命名空间:

  • 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>

实现委托的代码: 源代码

您需要获取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 的代码:来源

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 包

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
}

http://stackoverflow.com/questions/28448319/how-to-pass-argument-to-the-method-present-in-event-trigger-wpf - AzzamAziz
@DavidNichols 第二个取决于MVVM Light。 - AzzamAziz
2
在这里使用选项1可以极大地简化生活。我不同意“使用MVVMLight是最困难但最佳实践”的说法。如果它增加了额外的复杂性,并且MS已经包含了维护关注点分离的MVVM功能,为什么要再添加两个包呢? - Conrad
同意。这里涉及的“最佳实践”是关于 PRISM 的,不适用于此处。我会更新答案,谢谢! - AzzamAziz
3
选项1没有向方法传递参数,这个选项是不完整的还是不可能的? - luis_laurent
显示剩余7条评论

17

对于刚看到这篇文章的人,你应该知道,在更新的版本中(不确定确切的版本,因为官方文档在这个主题上很少),如果没有指定 CommandParameter,InvokeCommandAction 的默认行为是将其附加到的事件参数作为 CommandParameter 传递。因此,原始帖子的 XAML 可以简单地编写如下:

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

然后在您的命令中,您可以接受一个类型为NavigationEventArgs(或者其他适当的事件参数类型)的参数,它将自动提供。


3
嗨,看起来事情似乎不是那样运作的。嗯,那将会太容易了。 :) - Pompair
1
我在Windows 10 UWP应用程序中使用了这种技术,不确定它是否适用于所有情况。 - joshb
你肯定需要使用Prism来实现这个行为。 - IgorMF
再次强调,Prism非常棒。此外,我应该在浏览网页之前先去看手册。谢谢! - Informagic
2
这段代码是可行的,但在 InvokeCommandAction 中缺少 PassEventArgsToCommand="True"。添加该属性后,代码可以正常工作。 - Aaron. S

13

我知道这是一个相当老的问题,但今天我遇到了同样的问题,我并不是很想引用整个MVVMLight只为了使用带有事件参数的事件触发器。过去我使用过MVVMLight,它是一个很棒的框架,但我不想再在我的项目中使用它了。

我解决这个问题的方法是创建了一个 超级 简单,非常 适应性强的自定义触发操作,它允许我绑定到命令并提供一个事件参数转换器,将参数传递给命令的CanExecute和Execute函数。你不想直接传递事件参数,因为这会导致将视图层类型发送到视图模型层(在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);
            }
        }
    }
}

该类具有两个依赖属性。一个允许绑定到您的视图模型命令,另一个允许您在事件参数转换过程中绑定事件源(如果需要)。如果需要,您还可以提供区域设置(默认为当前 UI 区域设置)。

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

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 的答案有些相似,但它使用了事件触发器而不是行为。这种解决方案还提供了事件参数转换功能,虽然 @adabyron 的解决方案也可以做到这一点。我真的没有任何充分的理由为什么我更喜欢触发器而不是行为,这只是个人选择。在我看来,任何一种策略都是合理的选择。


非常完美的解决方案,太棒了。 - damccull

6

补充一下joshb已经说过的 - 这对我来说完全有效。确保在你的xaml中添加对Microsoft.Expression.Interactions.dll和System.Windows.Interactivity.dll的引用,然后做如下操作:

    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>

当你想要的参数可以通过绑定访问时(OP想要_EventArgs_),这个方法非常有效,并且只需要使用Interactivity命名空间。对于我来说,明确指定CommandParameter和元素的SelectedItem之间的绑定是关键,因为我曾尝试过仅输入字符串“SelectedItem”,但显然不起作用。干杯! - Paul

4
Prism的InvokeCommandAction默认会传递事件参数,如果未设置CommandParameter
下面是一个示例。请注意使用prism:InvokeCommandAction而不是i:InvokeCommandAction的用法。 https://learn.microsoft.com/en-us/previous-versions/msp-n-p/gg405494(v=pandp.40)#passing-eventargs-parameters-to-the-command
<i:Interaction.Triggers>
    <i:EventTrigger EventName="Sorting">
        <prism:InvokeCommandAction Command="{Binding SortingCommand}"/>
    </i:EventTrigger>
</i:Interaction.Triggers>

视图模型(ViewModel)
    private DelegateCommand<EventArgs> _sortingCommand;

    public DelegateCommand<EventArgs> SortingCommand => _sortingCommand ?? (_sortingCommand = new DelegateCommand<EventArgs>(OnSortingCommand));

    private void OnSortingCommand(EventArgs obj)
    {
        //do stuff
    }

有一份Prismlibrary文档的新版本。


这似乎是最简单的解决方案。它可能并不总是这样。 - Tomas Kosar

3

我认为使用InvokeCommandAction不容易实现这个功能 - 我建议看看来自MVVMLight或类似的EventToCommand


2

我知道有点晚了,但是微软已经将他们的Xaml.Behaviors开源了,现在只需要一个命名空间就可以更容易地使用交互性。

  1. 首先将Microsoft.Xaml.Behaviors.Wpf Nuget包添加到您的项目中。
    https://www.nuget.org/packages/Microsoft.Xaml.Behaviors.Wpf/
  2. 将xmlns:behaviours="http://schemas.microsoft.com/xaml/behaviors" 命名空间添加到您的xaml中。

然后像这样使用它,

<Button Width="150" Style="{DynamicResource MaterialDesignRaisedDarkButton}">
   <behaviours:Interaction.Triggers>
       <behaviours:EventTrigger EventName="Click">
           <behaviours:InvokeCommandAction Command="{Binding OpenCommand}" PassEventArgsToCommand="True"/>
       </behaviours:EventTrigger>
    </behaviours:Interaction.Triggers>
    Open
</Button>

将PassEventArgsToCommand="True"设置为True,并且您实现的RelayCommand可以采用RoutedEventArgs或对象作为模板。如果使用对象作为参数类型,只需将其转换为适当的事件类型。

命令将类似于以下内容,

OpenCommand = new RelayCommand<object>(OnOpenClicked, (o) => { return true; });

命令方法将类似于以下内容,

private void OnOpenClicked(object parameter)
{
    Logger.Info(parameter?.GetType().Name);
}

'Parameter'将是路由事件对象。

如果您好奇,以下是日志记录内容:

2020-12-15 11:40:36.3600|INFO|MyApplication.ViewModels.MainWindowViewModel|RoutedEventArgs

如您所见,记录的TypeName为RoutedEventArgs。

RelayCommand实现可在此处找到。

Why RelayCommand

附注:您可以绑定到任何控件的任何事件。例如,窗口的Closing事件,您将获得相应的事件。


是的,这个答案应该获得更多的赞同,因为它目前是最简单的解决方案(因为大多数项目都包含了行为),而且谷歌将这个页面显示在与问题相关的搜索结果中的首位。 - undefined

1
使用 Blend for Visual Studio 2013 中的 Behaviors 和 Actions,您可以使用 InvokeCommandAction。我在 Drop 事件中尝试了这个操作,尽管在 XAML 中没有指定 CommandParameter,但令我惊讶的是,执行操作参数包含 DragEventArgs。我推测这会发生在其他事件上,但还没有测试过。

2
你能提供一个代码(XAML和VM)的例子吗?按照描述,它对我不起作用(WPF,.NET 4.5)。 - Pete Stensønes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接