【问题标题】:Binding Event to a Method of the ViewModel without ICommand在没有 ICommand 的情况下将事件绑定到 ViewModel 的方法
【发布时间】:2015-11-24 20:37:38
【问题描述】:

问题:我想通过 XAML 将事件绑定到 ViewModel 的公共方法。

臭名昭著的解决方案是在ViewModel 上创建一个公共ICommand 属性,该属性返回RelayCommandDelegateCommand,然后在XAML 中使用Windows.Interactivity 中的EventTriggerInvokeCommandAction 进行绑定事件到命令。非常相似的替代方法是使用 MVVMLight 的EventToCommand,它甚至提供了将EventArgs 作为命令参数传递的可能性。

这种解决方案存在过于冗长的缺陷,因此使代码难以重构和维护。

我想使用 MarkupExtension 将事件绑定到 ViewModel 的公共方法直接。来自this blog postEventBindingExtension 提供了这种可能性。

XAML 中的示例用法:

<Button Content="Click me" Click="{my:EventBinding OnClick}" />

其中ViewModel有如下方法:

public void OnClick(object sender, EventArgs e)
{
    MessageBox.Show("Hello world!");
}

我对这种方法有几个问题:

  1. 我已经对其进行了测试,它对我来说就像一个魅力,但由于我不是专家,我想问一下这个解决方案是否存在一些缺陷或可能的意外行为。
  2. 这是否符合 MVVM 模式?
  3. EventBindingExtension 需要它绑定的公共方法来匹配事件的参数。如何扩展它以允许省略 object source 参数?
  4. 在 WPF 或 NuGet 包的框架中还有哪些其他类似的 MarkupExtensions 可用?

【问题讨论】:

  • 纯粹出于好奇,为什么必须使用事件?由于它在 ViewModel 中,它显然不处理 UI 更新等,所以我想知道在这种情况下一个事件有什么命令没有。
  • @Kilazur 首先,有很多事件没有对应的命令,例如ListViewItem 类根本没有命令,所以如果你想处理 MouseDoubleClick 事件,这是唯一的机会。其次,即使对于确实有对应命令的ButtonClick 事件,这种方法对我来说似乎更简洁,因为样板代码几乎为零(与使用命令时的大量样板代码相反)。

标签: c# .net wpf xaml mvvm


【解决方案1】:

这是我对 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);
}

【讨论】:

    【解决方案2】:

    1) ICommand 的好处之一是您可以通过简单地相应地修改绑定来更轻松地在应用程序中路由命令。通过直接绑定到处理程序,您将失去此功能并且必须自己实现它。在您的特定情况下,这可能不是问题,但无论如何它都是不必要的层。

    2)这可能是一个有点主观的话题,但我个人认为虽然这不是对 MVVM 的技术违规,但它不符合整体理念。 WPF,尤其是 MVVM,被设计成数据驱动的;绑定到一个方法有点回到旧的事件驱动的做事方式(至少对我来说)。在任何情况下,虽然绑定到一个方法可能仍然符合 MVVM 的条件,但至少在技术上,将 UI 对象作为发送者传递绝对不会!

    3) 您需要修改 GetHandler 函数来构造、编译和返回一个 LINQ 表达式或一个接受预期参数的 IL 委托,删除第一个并将其余的传递给绑定目标的方法。这应该足以让您入门:

    static Delegate GetHandler(object dataContext, EventInfo eventInfo, string eventHandlerName)
    {
        // get the vm handler we're binding to
        var eventParams = GetParameterTypes(eventInfo.EventHandlerType);
        var method = dataContext.GetType().GetMethod(eventHandlerName, eventParams.Skip(1).ToArray());
        if (method == null)
            return null;
    
        // construct an expression that calls it
        var instance = Expression.Constant(dataContext);
        var paramExpressions = eventParams.Select(p => Expression.Parameter(p)).ToArray();
        var call = Expression.Call(instance, method, paramExpressions.Skip(1));
    
        // wrap it in a lambda and compile it
        return Expression.Lambda(eventInfo.EventHandlerType, call, paramExpressions).Compile();
    }
    

    4) 有点笼统的问题,我经常使用的唯一一个是Translate for localization

    【讨论】:

      【解决方案3】:

      ViewModel 不应有任何对视图控件的引用。

      事件处理程序通过object sender 提供此访问权限。

      此外,command 允许您管理它是否可以执行,但简单的方法 - 不能。要解决此功能 - 您必须定义一个功能来管理控件的启用/禁用。

      当你实现它时,你会想到如何封装共享功能——你将拥有另一个 Command 接口。

      【讨论】:

      • 这就是为什么我建议删除第3点中的object sender参数。那么这种方法是否符合MVVM?
      • 我看到了ICommand 具有CanExecute 方法的好处,但是您通常不需要管理命令是否可以执行并且总是返回true。其次,在处理没有对应命令的事件时(如here),整个CanExecute 功能变得完全无用。
      • @Martin 这就是把我带到这里的原因:) 恕我直言,一个命令应该只被那些可以使用它的人使用。无论命令是否可执行,都会引发事件(只要您自己没有实现)。这使得 ICommand 的整个使用在语义上是不正确的,并且 IMO 成为一种解决方法,因为缺少对事件的绑定支持。
      猜你喜欢
      • 2015-11-13
      • 2018-07-04
      • 2017-04-07
      • 2012-06-11
      • 2020-04-26
      • 2011-12-17
      • 1970-01-01
      • 2017-12-14
      • 1970-01-01
      相关资源
      最近更新 更多