4

The problem: I would like to bind an event to a public method of the ViewModel via XAML.

The notorious solution is to create a public ICommand property on the ViewModel that returns a RelayCommand or a DelegateCommand and then to use EventTrigger and InvokeCommandAction from Windows.Interactivity in XAML to bind the event to the command. Very similar alternative is to use MVVMLight's EventToCommand, which even provides the possibility to pass EventArgs as the Command's parameter.

This solution has the pitfall of being too verbose, and hence makes the code hard to refactor and maintain.

I would like to use an MarkupExtension for binding an event to a public method of the ViewModel directly. Such a possibility is provided by the EventBindingExtension from this blog post.

Example usage in XAML:

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

Where the ViewModel has the following method:

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

I have a few questions regarding this approach:

  1. I have tested it and it works like a charm for me but since I am no expert I would like to ask if this solution does have some pitfalls or possibly unexpected behaviors.
  2. Is this in compliance with the MVVM pattern?
  3. EventBindingExtension requires the public method it is bound to to match the parameters of the event. How could it be extended to allow the object source parameter to be omitted?
  4. What other MarkupExtensions similar to this are there available in frameworks for WPF or NuGet packages?
DeanOC
  • 7,142
  • 6
  • 42
  • 56
Martin
  • 189
  • 1
  • 7
  • By pure curiosity, why do you have to use an event? Since it's in the ViewModel, it clearly doesn't deal with UI updates and such, so I wonder what an event has in that case that a command doesn't. – Kilazur Aug 30 '15 at 21:12
  • @Kilazur Firstly, there are a lot of events that do not have a command counterpart, e.g. `ListViewItem` class has no commands at all, so if you want to handle the `MouseDoubleClick` event, this is the only chance. Secondly, even for the `Button`'s `Click` event that does have a command counterpart, this approach seems neater to me, since there is almost zero boilerplate code (in opposiotion to a lot of boilerplate code when using commands). – Martin Aug 30 '15 at 21:25

3 Answers3

3

Here is my full featured implementation of a method binding for WPF events:

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

Supports multiple arguments, bindings and other extensions to provide argument values, method resolution based on argument types, etc.

Usage:

<!--  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}}" />

View model method signatures:

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);
}
Mike Marynowski
  • 3,156
  • 22
  • 32
1

1) One of the benefits of ICommand is that you can more easily route commands around your application by simply modifying the bindings accordingly. By binding directly to a handler you lose this functionality and would have to implement it yourself. That may not be an issue in your specific case but it's an unnecessary layer regardless.

2) This is probably a bit of a subjective topic but I personally think that although it's not a technical violation of MVVM it's not in keeping with the overall philosophy. WPF, and especially MVVM, are designed to be data-driven; binding to a method smacks of going back to the old event-driven way of doing things (at least to me). In any case while binding to a method might still qualify as MVVM, at least technically, passing a UI object in as the sender most definitely would not!

3) You'll need to modify the GetHandler function to construct, compile and return either a LINQ expression or an IL delegate that accepts the expected parameters, removes the first and passes the rest to the binding target's method. This should be enough to get you started:

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) Bit of a generic question, the only one I use frequently is Translate for localization.

Mark Feldman
  • 15,731
  • 3
  • 31
  • 58
0

ViewModel should not have any references to view's controls.

Event handler gives this access via object sender.

In addition, command allows you to manage if it can be executed, but the simple method - not. To workaround this function - you have to define a functionality for managing control's enable/disable.

When you implement this, you'll think how to encapsulate shared functionality - and you will have another Command interface.

Alex Lebedev
  • 601
  • 5
  • 14
  • That's why I suggested to remove the `object sender` parameter in Point 3. Would this approach be in compliance with MVVM then? – Martin Aug 30 '15 at 21:05
  • I see the benefits of `ICommand` having the `CanExecute` method, but you often do not need to manage if the command can be executed and just always return true. Secondly, when handling events which do not have a command counterpart (like [here](http://stackoverflow.com/questions/7877532/wpf-event-binding-from-view-to-viewmodel/7877807#7877807)), the whole `CanExecute` functionality becomes completely useless. – Martin Aug 30 '15 at 22:34
  • @Martin That's the point that brought me here :) IMHO a Command should only be consumed by those who can Consume it. An Event (as long as not implemented yourself) is thrown regardless of a command being executeable. This makes the whole use of `ICommand` semantically incorrect and IMO to a workaround for the lack of Bindingsupport on Events. – LuckyLikey Apr 24 '17 at 15:20