WPF 将 UI 事件绑定到 ViewModel 中的命令

Posted

技术标签:

【中文标题】WPF 将 UI 事件绑定到 ViewModel 中的命令【英文标题】:WPF Binding UI events to commands in ViewModel 【发布时间】:2011-06-21 07:57:40 【问题描述】:

我正在重构一个简单的应用程序以遵循 MVVM,我的问题是如何将 SelectionChanged 事件从我的代码中移出到 viewModel?我看过一些将元素绑定到命令的示例,但并没有完全掌握。任何人都可以帮助解决这个问题。谢谢!

谁能提供使用以下代码的解决方案?非常感谢!

public partial class MyAppView : Window 

    public MyAppView()
    
        InitializeComponent();

        this.DataContext = new MyAppViewModel ();

        // Insert code required on object creation below this point.
    

    private void contactsList_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
    
        //TODO: Add event handler implementation here.           
        //for each selected contact get the labels and put in collection 

        ObservableCollection<AggregatedLabelModel> contactListLabels = new ObservableCollection<AggregatedLabelModel>();

        foreach (ContactListModel contactList in contactsList.SelectedItems)
        
            foreach (AggregatedLabelModel aggLabel in contactList.AggLabels)
            
                contactListLabels.Add(aggLabel);
            
        
        //aggregate the contactListLabels by name
        ListCollectionView selectedLabelsView = new ListCollectionView(contactListLabels);

        selectedLabelsView.GroupDescriptions.Add(new PropertyGroupDescription("Name"));
        tagsList.ItemsSource = selectedLabelsView.Groups;
    

【问题讨论】:

【参考方案1】:

您应该将EventTrigger 与Windows.Interactivity 命名空间中的InvokeCommandAction 结合使用。这是一个例子:

<ListBox ...>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="SelectionChanged">
            <i:InvokeCommandAction Command="Binding SelectedItemChangedCommand"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</ListBox>

您可以通过 Add reference &gt; Assemblies &gt; Extensions 引用 System.Windows.Interactivity

完整的i 命名空间是:xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

【讨论】:

谢谢。我是编程新手,请原谅我 - 你能用我提供的代码举个例子吗? 您基本上需要在 ViewModel 中创建一个名为“SelectedItemChangedCommand”的命令属性。命令类似于事件,但一个命令只能有一个回调函数,不像事件。查看文档:msdn.microsoft.com/en-us/library/ms752308.aspx 如果您没有 Expression Blend,则需要 SDK:microsoft.com/downloads/en/… 您还需要以 .net framework 4.0 为目标。此行为动作不在 3.5 框架中,即使您使用的是 Blend4 如果使用 nuget 包 Microsoft.Xaml.Behaviors.Wpf,则不再需要 @datchung 棱镜,因为它可以访问 InvokeCommandAction 类的更新版本,该类现在具有一个名为“PassEventArgsToCommand”的属性。这是source【参考方案2】:

这个问题有类似的问题。

WPF MVVM : Commands are easy. How to Connect View and ViewModel with RoutedEvent

我处理这个问题的方法是在 ViewModel 中有一个 SelectedItem 属性,然后将 ListBox 的 SelectedItem 或其他任何东西绑定到该属性。

【讨论】:

有效点,但不适用于所有场景...我有一个场景,我正在按照您的建议进行操作,但也需要使用 SelectionChanged 事件【参考方案3】:

要重构这一点,您需要转变思路。您将不再处理“选择更改”事件,而是将所选项目存储在您的视图模型中。然后,您将使用双向数据绑定,以便当用户选择一个项目时,您的视图模型会更新,而当您更改所选项目时,您的视图会更新。

【讨论】:

我认为转变思维部分是我作为一个相当新手的程序员所苦苦挣扎的地方!【参考方案4】:

考虑Microsoft.Xaml.Behaviors.Wpf,它的所有者是Microsoft,您可以在该页面中看到它。

System.Windows.Interactivity.WPF所有者是mthamil,有人能告诉我它可靠吗?

Microsoft.Xaml.Behaviors.Wpf 的示例:

<UserControl ...
             xmlns:behaviors="http://schemas.microsoft.com/xaml/behaviors"
             ...>

<Button x:Name="button">
    <behaviors:Interaction.Triggers>
        <behaviors:EventTrigger EventName="Click" SourceObject="Binding ElementName=button">
            <behaviors:InvokeCommandAction Command="Binding ClickCommand" />
        </behaviors:EventTrigger>
    </behaviors:Interaction.Triggers>
</Button>

</UserControl>

【讨论】:

System.Windows.Interactivity 由 Micrososft Blend/Expression 团队维护,该团队近年来将其开源并重新打包为 nuget Microsoft.Xaml.Behaviours.Wpf,查看This blog了解更多 正确的 nuget 是 Microsoft.Xaml.Behaviors【参考方案5】:

您最好的选择是使用Windows.Interactivity。使用EventTriggersICommand 附加到任何RoutedEvent

这里有一篇文章可以帮助您入门:Silverlight and WPF Behaviours and Triggers

【讨论】:

【参考方案6】:
<ListBox SelectionChanged="eb:EventBinding Command=SelectedItemChangedCommand, CommandParameter=$e">

</ListBox>

命令

eb:EventBinding(查找命令的简单命名模式)

eb:EventBinding Command=CommandName

命令参数

$e (EventAgrs)

$this 或 $this.Property

字符串

https://github.com/JonghoL/EventBindingMarkup

【讨论】:

【参考方案7】:

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

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

那就这样用吧,

<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 或对象作为模板。如果您使用 object 作为参数类型,只需将其转换为适当的事件类型。

命令看起来像这样,

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

命令方法看起来像这样,

private void OnOpenClicked(object parameter)

    Logger.Info(parameter?.GetType().Name);

“参数”将是路由事件对象。

还有日志以防你好奇,

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

如您所见,记录的 TypeName 是 RoutedEventArgs

RelayCommand 的实现可以在这里找到。

Why RelayCommand

PS:您可以绑定到任何控件的任何事件。像Window的Closing事件一样,你会得到相应的事件。

【讨论】:

【参考方案8】:

我会关注这个question中的最佳答案

基本上,您的视图模型将包含所有项目的列表和选定项目的列表。然后,您可以将一个行为附加到您的列表框,以管理您的选定项目列表。

这样做意味着您在后面的代码中没有任何内容,并且 xaml 很容易遵循,而且该行为可以在您的应用程序的其他地方重复使用。

<ListBox ItemsSource="Binding AllItems" Demo:SelectedItems.Items="Binding SelectedItems" SelectionMode="Multiple" />

【讨论】:

【参考方案9】:

当需要绑定自定义用户控件的事件时,有时通过交互触发器将事件绑定到命令的解决方案不起作用。 在这种情况下,您可以使用自定义行为。

声明绑定行为,如:

public class PageChangedBehavior

    #region Attached property

    public static ICommand PageChangedCommand(DependencyObject obj)
    
        return (ICommand)obj.GetValue(PageChangedCommandProperty);
    
    public static void SetPageChangedCommand(DependencyObject obj, ICommand value)
    
        obj.SetValue(PageChangedCommandProperty, value);
    

    public static readonly DependencyProperty PageChangedCommandProperty =
        DependencyProperty.RegisterAttached("PageChangedCommand", typeof(ICommand), typeof(PageChangedBehavior),
            new PropertyMetadata(null, OnPageChanged));

    #endregion

    #region Attached property handler

    private static void OnPageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    
        var control = d as PageControl;
        if (control != null)
        
            if (e.NewValue != null)
            
                control.PageChanged += PageControl_PageChanged;
            
            else
            
                control.PageChanged -= PageControl_PageChanged;
            
        
    

    static void PageControl_PageChanged(object sender, int page)
    
        ICommand command = PageChangedCommand(sender as DependencyObject);

        if (command != null)
        
            command.Execute(page);
        
    

    #endregion

然后将其绑定到 xaml 中的命令:

        <controls:PageControl
            Grid.Row="2"
            CurrentPage="Binding Path=UsersSearchModel.Page,Mode=TwoWay, UpdateSourceTrigger=PropertyChanged"
            PerPage="Binding Path=UsersSearchModel.PageSize,Mode=TwoWay, UpdateSourceTrigger=PropertyChanged"
            Count="Binding Path=UsersSearchModel.SearchResults.TotalItemCount"
            behaviors:PageChangedBehavior.PageChangedCommand="Binding PageChangedCommand">
        </controls:PageControl>

【讨论】:

【参考方案10】:

正如@Cameron MacFarland 所提到的,我会简单地双向绑定到 viewModel 上的属性。在属性设置器中,您可以执行所需的任何逻辑,例如添加到联系人列表中,具体取决于您的要求。

但是,我不一定将属性称为“SelectedItem”,因为 viewModel 不应该知道视图层以及它如何与其属性交互。我会称它为 CurrentContact 之类的东西。

显然,除非您只是想创建命令作为练习等。

【讨论】:

我不同意视图模型不应该知道视图层。毕竟,它是视图的模型。它不应该操纵视图中的对象,但这只是为了在单元测试中可以独立于视图进行实例化。除非视图模型的集合被命名为 Items,否则我不会调用属性 SelectedItem,但这是一个不同的问题。 看看你的意思,但我倾向于不将 vm 视为“视图模型”,更像是一个适配器,它不对 UI 做任何假设,而只是暴露它的状态和通过命令和通知的行为。正如您所说,这种分离不仅“仅”用于单元测试,而且还意味着 UI 可以在需要时轻松切换和修改,而无需修改 VM。【参考方案11】:

这是一个使用MarkupExtension 的实现。尽管具有低级性质(在此方案中是必需的),但 XAML 代码非常简单:

XAML

<SomeControl Click="local:EventBinding EventToCommand" CommandParameter="local:Int32 12345" />

Marup 扩展

public class EventBindingExtension : MarkupExtension

    private static readonly MethodInfo EventHandlerImplMethod = typeof(EventBindingExtension).GetMethod(nameof(EventHandlerImpl), new[]  typeof(object), typeof(string) );
    public string Command  get; set; 

    public EventBindingExtension()
    
    
    public EventBindingExtension(string command) : this()
    
        Command = command;
    

    // Do not use!!
    public static void EventHandlerImpl(object sender, string commandName)
    
        if (sender is FrameworkElement frameworkElement)
        
            object dataContext = frameworkElement.DataContext;

            if (dataContext?.GetType().GetProperty(commandName)?.GetValue(dataContext) is ICommand command)
            
                object commandParameter = (frameworkElement as ICommandSource)?.CommandParameter;
                if (command.CanExecute(commandParameter)) command.Execute(commandParameter);
            
        
    

    public override object ProvideValue(IServiceProvider serviceProvider)
    
        if (serviceProvider.GetService(typeof(IProvideValueTarget)) is IProvideValueTarget targetProvider &&
            targetProvider.TargetObject is FrameworkElement targetObject &&
            targetProvider.TargetProperty is MemberInfo memberInfo)
        
            Type eventHandlerType;
            if (memberInfo is EventInfo eventInfo) eventHandlerType = eventInfo.EventHandlerType;
            else if (memberInfo is MethodInfo methodInfo) eventHandlerType = methodInfo.GetParameters()[1].ParameterType;
            else return null;

            MethodInfo handler = eventHandlerType.GetMethod("Invoke");
            DynamicMethod method = new DynamicMethod("", handler.ReturnType, new[]  typeof(object), typeof(object) );

            ILGenerator ilGenerator = method.GetILGenerator();
            ilGenerator.Emit(OpCodes.Ldarg, 0);
            ilGenerator.Emit(OpCodes.Ldstr, Command);
            ilGenerator.Emit(OpCodes.Call, EventHandlerImplMethod);
            ilGenerator.Emit(OpCodes.Ret);

            return method.CreateDelegate(eventHandlerType);
        
        else
        
            throw new InvalidOperationException("Could not create event binding.");
        
    

【讨论】:

以上是关于WPF 将 UI 事件绑定到 ViewModel 中的命令的主要内容,如果未能解决你的问题,请参考以下文章

WPF 事件绑定到 ViewModel(对于非命令类)

将事件绑定到 ViewModel

从事件更新 Viewmodel - PropertyChangedEventHandler 为空?

WPF:MVVM模式下ViewModel关闭View

Kendo UI - 如何使用 Kendo MVVM 将选中的属性(属性)和处理复选框的单击事件绑定到 viewModel

WPF MVVM 从 ViewModel 触发事件的正确方法