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

25
我正在开发一个应用程序的第二个版本,作为重写的一部分,我必须转移到MVVM架构。 我受到压力要将每一行代码都放在视图模型类中 - 在代码后台文件中使用C#被认为是不好的做法。(我知道,我知道...我明白代码后台并不是坏事,但这不是我这次决定的。)
对于实现命令接口的对象而言,这很容易。我已经能够找到大量关于如何将这些对象的Command绑定到视图模型中的ICommand的信息。 问题是对于没有此接口的对象,例如:
<ListBox
   x:Name="myListBox"
   MouseDoubleClick="myCallbackFunction">

<!-- ... -->

</ListBox>

我想知道如何将 Listbox 的 MouseDoubleClick 事件绑定到我的回调函数 myCallbackFunction,该函数实现在视图模型中。这是否可能?

谢谢!


1
重复问题 https://dev59.com/r3NA5IYBdhLWcg3wfd8m - Navid Rahmani
可能是将命令绑定到事件?的重复问题。 - StayOnTarget
5个回答

11

这并不是直接可以实现的。但是可以通过附加属性或行为来完成,尽管找到并调用适当的方法仍然有点棘手(可以通过反射相对容易地完成)。

话虽如此,通常通过 ICommand 来处理 - 例如,MVVM Light具有出色的 EventToCommand 行为,将任何事件映射到ViewModel上的ICommand。使用ICommand的好处是你仍然可以使用数据绑定,因为ICommand作为一个属性被公开。


7

自 .NET 4.5 起,WPF 支持事件上的标记扩展。利用这种能力,我实现了一个多功能方法绑定扩展并在此处进行了介绍:

http://www.singulink.com/CodeIndex/post/building-the-ultimate-wpf-event-method-binding-extension

可以使用完整的属性路径语法绑定到方法,支持使用绑定和其他标记扩展作为参数,并自动路由到与提供的参数签名匹配的方法。以下是一些用法示例:

<!--  Basic usage  -->
<Button Click="{data:MethodBinding OpenFromFile}" Content="Open" />

<!--  Pass in a binding as a method argument  -->
<Button Click="{data:MethodBinding Save, {Binding CurrentItem}}" Content="Save" />

<!--  Another example of a binding, but this time to a property on another element  -->
<ComboBox x:Name="ExistingItems" ItemsSource="{Binding ExistingItems}" />
<Button Click="{data:MethodBinding Edit, {Binding SelectedItem, ElementName=ExistingItems}}" />

<!--  Pass in a hard-coded method argument, XAML string automatically converted to the proper type  -->
<ToggleButton Checked="{data:MethodBinding SetWebServiceState, True}"
                Content="Web Service"
                Unchecked="{data:MethodBinding SetWebServiceState, False}" />
                
<!--  Pass in sender, and match method signature automatically -->
<Canvas PreviewMouseDown="{data:MethodBinding SetCurrentElement, {data:EventSender}, ThrowOnMethodMissing=False}">
    <controls:DesignerElementTypeA />
    <controls:DesignerElementTypeB />
    <controls:DesignerElementTypeC />
</Canvas>

    <!--  Pass in EventArgs  -->
<Canvas MouseDown="{data:MethodBinding StartDrawing, {data:EventArgs}}"
        MouseMove="{data:MethodBinding AddDrawingPoint, {data:EventArgs}}"
        MouseUp="{data:MethodBinding EndDrawing, {data:EventArgs}}" />

<!-- Support binding to methods further in a property path -->
<Button Content="SaveDocument" Click="{data:MethodBinding CurrentDocument.DocumentService.Save, {Binding CurrentDocument}}" />

查看模型方法签名:

public void OpenFromFile();
public void Save(DocumentModel model);
public void Edit(DocumentModel model);

public void SetWebServiceState(bool state);

public void SetCurrentElement(DesignerElementTypeA element);
public void SetCurrentElement(DesignerElementTypeB element);
public void SetCurrentElement(DesignerElementTypeC element);

public void StartDrawing(MouseEventArgs e);
public void AddDrawingPoint(MouseEventArgs e);
public void EndDrawing(MouseEventArgs e);

public class Document
{
    // Fetches the document service for handling this document
    public DocumentService DocumentService { get; }
}

public class DocumentService
{
    public void Save(Document document);
}

4
直接回答您的问题,请参考为什么要避免在WPF MVVM模式中使用codebehind?它提出了两个可能需要的建议。
然而,为什么您想把ListBox的MouseDoubleClick绑定到viewmodel中的ICommand呢?
另一种方法是编写一个代码后台方法来注册MouseDoubleClick。由于以下事实,这并不是坏事情。
1.有意义的数据绑定是视图和视图模型之间的交互。例如,当用户在TextBox中输入文本时,viewmodel也会更新。相反,如果viewmodel从数据库获取数据,则将显示在视图中。但是,在这种情况下,viewmodel中的ICommand与视图没有绑定关系。
2.当然,ICommand的CanExcute对于viewmodel非常重要,但在许多情况下,它与viewmodel无关或不相关。在这种情况下,ICommand绑定和编写代码后台之间的区别在于MouseDoubleClick事件是与ICommand绑定还是注册事件处理程序。

ListBox上的MouseDoubleClick事件只是遗留代码中存在的一个示例,我可能不会保留该特定事件。然而,我想知道是否有一种方法可以避免像这样的任何事件的代码后台。我的代码目前在代码后台中具有简单的事件处理程序,该处理程序调用视图模型中的方法。但由于办公室政治原因,我被指示找到一种避免在代码后台编写代码的方法。 - RobotNerd
正如我在回答中所提到的,@Reed Copsey也提到了,有两种可能要使用的东西,即附加属性或行为。它们在我的问题中有描述。然而,我想知道为什么鼓励避免在代码后台编写代码。我认为@slugster在我的问题的答案会对你有所帮助。 - Jin-Wook Chung
理念是业务逻辑需要与HMI完全解耦(我正在与“纯粹主义者”合作)。然而,我尝试了您上面链接的解决方案,但我还没有能够使它们中的任何一个工作。每个潜在的解决方案都比仅使用代码后台作为中介要不优雅得多。我已经决定坚持我的原始代码,并看看是否可以在我的下一个同行评审中推动它。 - RobotNerd

1

一种方法是在代码后台处理事件,并从代码后台调用视图模型的适当方法

您还可以选择一些现成的命令库,例如this教程,该教程使用ACB


0

尝试使用EventBinder,它将允许您直接将方法绑定到任何事件,包括自己的事件,而无需将方法包装在ICommand容器中。

https://github.com/Serg046/EventBinder
https://www.nuget.org/packages/EventBinder

支持.NET Framework 3.0+、.NET Core 3.0+和Avalonia。

特点:

  • 绑定到没有ICommand的方法
  • 绑定到有返回类型的方法
  • 绑定到异步方法
  • 使用 . 分隔符、属性和字段绑定到嵌套对象
  • 传递int、double、decimal或string类型的用户参数
  • 使用$符号和位置编号($0$1等)传递事件参数
  • 将默认的{Binding}作为参数传递

用法:

public class ViewModel
{
    public MetadataViewModel Metadata { get; } = new MetadataViewModel();

    public async Task ShowMessage(string msg, decimal centenary, double year)
    {
        await Task.Delay(0);
        MessageBox.Show(msg + centenary + year);
    }

    public class MetadataViewModel
    {
        public void ShowInfo(Window window, double windowWidth, ViewModel viewModel, object sender, MouseButtonEventArgs eventArgs)
        {
            var sb = new StringBuilder("Window width: ")
                .AppendLine(windowWidth.ToString())
                .Append("View model type: ").AppendLine(viewModel.GetType().Name)
                .Append("Sender type: ").AppendLine(sender.GetType().Name)
                .Append("Clicked button: ").AppendLine(eventArgs.ChangedButton.ToString())
                .Append("Mouse X: ").AppendLine(eventArgs.GetPosition(window).X.ToString())
                .Append("Mouse Y: ").AppendLine(eventArgs.GetPosition(window).Y.ToString());
            MessageBox.Show(sb.ToString());
        }
    }
}

绑定:

<Window xmlns:e="clr-namespace:EventBinder;assembly=EventBinder" Name="Wnd">
    <Rectangle Fill="LightGray" Name="Rct"
        MouseLeftButtonDown="{e:EventBinding ShowMessage, `Happy `, 20m, 20.0 }"
        MouseRightButtonDown="{e:EventBinding Metadata.ShowInfo, {Binding ElementName=Wnd},
            {Binding ElementName=Wnd, Path=ActualWidth}, {Binding}, $0, $1 }" />
</Window>

或者

EventBinding.Bind(Rct, nameof(Rct.MouseLeftButtonDown),
    nameof(ViewModel.ShowMessage),
    "`Happy `", 20m, 20.0);
EventBinding.Bind(Rct, nameof(Rct.MouseRightButtonDown),
    nameof(ViewModel.Metadata) + "." + nameof(ViewModel.Metadata.ShowInfo),
    new Binding { ElementName = nameof(Wnd)},
    new Binding("ActualWidth") { ElementName = nameof(Wnd) },
    new Binding(),
    "$0", "$1");

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